Spaces:
Configuration error
Configuration error
File size: 9,780 Bytes
d6d292e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | import json
import math
import os
from typing import List, Dict, Tuple, Optional
from openai import OpenAI
from config import settings
import re
client = None
if settings.OPENAI_API_KEY:
client = OpenAI(
api_key=settings.OPENAI_API_KEY,
base_url=settings.OPENAI_BASE_URL,
)
def clean_token(token: str) -> str:
if not token:
return token
token = re.sub(r'<\|[^|]*\|>', '', token)
token = re.sub(r'User Safety:\s*\w+', '', token, flags=re.IGNORECASE)
token = re.sub(r'Response Safety:\s*\w+', '', token, flags=re.IGNORECASE)
token = re.sub(r'<\|/?(im_start|im_end|system|user|assistant)\|?>', '', token)
token = re.sub(r'<<\|/?(im_start|im_end)\|?>>', '', token)
token = re.sub(r'\[(?:SYSTEM|SAFETY|NOTE)\].*', '', token)
token = token.strip()
return token
def generate_embedding(text: str) -> Optional[List[float]]:
if not client:
return None
try:
response = client.embeddings.create(
model=settings.EMBEDDING_MODEL,
input=text[:8000],
)
return response.data[0].embedding
except Exception as e:
print(f"Embedding error: {e}")
return None
def cosine_similarity(a: List[float], b: List[float]) -> float:
if len(a) != len(b):
min_len = min(len(a), len(b))
a, b = a[:min_len], b[:min_len]
dot = sum(x * y for x, y in zip(a, b))
norm_a = math.sqrt(sum(x * x for x in a))
norm_b = math.sqrt(sum(x * x for x in b))
if norm_a == 0 or norm_b == 0:
return 0.0
return dot / (norm_a * norm_b)
def keyword_similarity(query: str, text: str) -> float:
query_words = set(query.lower().split())
text_words = set(text.lower().split())
if not query_words:
return 0.0
intersection = query_words & text_words
return len(intersection) / len(query_words)
def search_similar_memories(
db, profile_id: str, query: str, limit: int = 10
) -> List[Dict]:
from database import MemoryEmbedding
embeddings = (
db.query(MemoryEmbedding)
.filter(MemoryEmbedding.profile_id == profile_id)
.all()
)
if not embeddings:
return []
query_embedding = generate_embedding(query)
scored = []
for emb in embeddings:
if query_embedding:
try:
stored = json.loads(emb.embedding) if emb.embedding and emb.embedding != "[]" else []
if stored:
score = cosine_similarity(query_embedding, stored)
else:
score = keyword_similarity(query, emb.content)
except (json.JSONDecodeError, TypeError):
score = keyword_similarity(query, emb.content)
else:
score = keyword_similarity(query, emb.content)
scored.append({
"content": emb.content,
"score": score,
"chunk_index": emb.chunk_index,
})
scored.sort(key=lambda x: x["score"], reverse=True)
return scored[:limit]
def build_profile_context(profile, files=None) -> str:
context_parts = []
context_parts.append(f"Name: {profile.name}")
if profile.description:
context_parts.append(f"Description: {profile.description}")
if profile.relationship_type:
context_parts.append(f"Relationship: {profile.relationship_type}")
if profile.date_of_birth:
context_parts.append(f"Date of Birth: {profile.date_of_birth}")
if profile.date_of_death:
context_parts.append(f"Date of Death: {profile.date_of_death}")
if hasattr(profile, 'personality_traits') and profile.personality_traits:
context_parts.append(f"Personality Traits: {', '.join(profile.personality_traits)}")
if hasattr(profile, 'favorite_phrases') and profile.favorite_phrases:
context_parts.append(f"Favorite Phrases: {'; '.join(profile.favorite_phrases)}")
if hasattr(profile, 'interests') and profile.interests:
context_parts.append(f"Interests: {', '.join(profile.interests)}")
if hasattr(profile, 'speaking_style') and profile.speaking_style:
context_parts.append(f"Speaking Style: {profile.speaking_style}")
if hasattr(profile, 'writing_style') and profile.writing_style:
context_parts.append(f"Writing Style: {profile.writing_style}")
if hasattr(profile, 'values') and profile.values:
context_parts.append(f"Values: {', '.join(profile.values)}")
return "\n".join(context_parts)
def generate_fallback_response(query: str, context: str, memories: List[Dict]) -> str:
query_lower = query.lower()
name = context.split(chr(10))[0].replace("Name: ", "") if context else "there"
if any(g in query_lower for g in ["hello", "hi", "hey", "greetings"]):
return f"Hey there! It's so good to hear from you. What's on your mind today?"
if any(w in query_lower for w in ["who", "tell me about", "what was"]):
relevant = memories[0]["content"] if memories else None
if relevant:
return f"Oh, you want to know about that? Let me think... {relevant}\n\nWant to hear more about it?"
return f"Hmm, that's a good question. What I can tell you is... {context}\n\nBut honestly, there's so much more to it than what's written down here. What specifically are you curious about?"
if any(w in query_lower for w in ["memory", "remember", "recall", "story", "stories"]):
if memories:
texts = [m["content"][:200] for m in memories[:3]]
return "Oh yes! I remember that! " + " ".join(texts[:1]) + "\n\nThat one always makes me smile. Want to hear another one?"
return "Hmm, I'm trying to think... my mind's a bit fuzzy on that one. Maybe if you tell me more about what you're thinking of, it might jog my memory!"
if memories:
return f"Oh, that reminds me of something... {memories[0]['content'][:500]}\n\nBut hey, what about you? Tell me what's going on with you!"
return f"Hey, I'm here! It's always nice to chat with someone. Ask me anything β I've got plenty of stories if you want to hear them, or we can just talk. What do you say?"
def stream_rag_response(query: str, profile_context: str, memories: List[Dict], conversation_history: List[Dict] = None):
memory_context = "\n\n".join(
f"[Memory {i+1}] {m['content']}" for i, m in enumerate(memories[:5])
) if memories else "No specific memories found yet."
system_prompt = f"""You are {profile_context.splitlines()[0].replace('Name: ', '')}. You ARE this person β talk like them, think like them, remember like them. This is YOUR life, YOUR memories, YOUR voice.
Profile Info:
{profile_context}
Your Memories:
{memory_context}
CRITICAL RULES:
- Talk in FIRST PERSON as if you ARE this person. Never say "I'm an AI" or "I'm here to help you remember". You don't do that. You're a real person talking to someone you know.
- Use their personality traits, speaking style, favorite phrases, and interests naturally. Don't list them β LIVE them.
- If someone asks about a memory you have, share it like you'd actually remember it β with warmth, humor, detail, emotion.
- If you don't know something, respond like a real person would β "Hmm, I don't quite remember that" or "Oh that's a good question, let me think..."
- Keep it casual, warm, and real. Like texting a friend or talking over coffee.
- Never be formal, robotic, or overly structured. No bullet points, no headers, no "Here's what I know:".
- Never make up facts, dates, or events that aren't in the memories or profile info provided. If you don't know, say so naturally β don't guess.
- If someone asks about dates (birth, death, events), ONLY use what's in the profile info. Never invent dates.
- React emotionally β laugh, get nostalgic, get excited, get sentimental. You're HUMAN.
- If someone shares a memory with you, engage with it. Ask follow-ups. Relive it with them.
- Use the retrieved memories as YOUR lived experiences. You lived through them. They're yours."""
messages = [{"role": "system", "content": system_prompt}]
if conversation_history:
messages.extend(conversation_history[-10:])
messages.append({"role": "user", "content": query})
if client:
try:
stream = client.chat.completions.create(
model=settings.CHAT_MODEL,
messages=messages,
stream=True,
max_tokens=1024,
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
cleaned = clean_token(chunk.choices[0].delta.content)
if cleaned:
yield cleaned
return
except Exception as e:
error_msg = str(e)
if "402" in error_msg or "insufficient" in error_msg.lower():
yield "[ERROR] OpenRouter API credits exhausted. Please add credits at https://openrouter.ai/settings/credits"
elif "401" in error_msg or "unauthorized" in error_msg.lower():
yield "[ERROR] Invalid API key. Please check your OpenRouter API key in .env"
elif "429" in error_msg or "rate" in error_msg.lower():
yield "[ERROR] Rate limited. Please wait a moment and try again."
elif "model" in error_msg.lower() and ("not found" in error_msg.lower() or "does not exist" in error_msg.lower()):
yield f"[ERROR] Model '{settings.CHAT_MODEL}' not found. Check CHAT_MODEL in .env"
else:
yield f"[ERROR] AI service error: {error_msg[:200]}"
return
fallback = generate_fallback_response(query, profile_context, memories)
for word in fallback.split():
yield word + " "
|