fix(memory): prevent stale name facts from overriding Discord display name
Two bugs were causing Miku to call users by wrong names:
BUG 1 - No authoritative source:
Declarative name facts ('The user's name is Lily') were injected into
the prompt without any counterweight. If an old consolidation run
extracted a wrong name, Miku would believe it forever.
Fix: agent_prompt_prefix now appends the user's Discord display name
as AUTHORITATIVE context, with explicit instruction to prefer it over
any contradictory name facts.
BUG 2 - Dedup prevented name updates:
_is_duplicate_fact() used vector similarity to detect duplicates.
'The user's name is Lily' and 'The user's name is koko210Serve' are
~80% identical text, giving high cosine similarity (>0.85 threshold).
New correct name facts were silently rejected as 'duplicates'.
Fix: name facts now use _find_existing_fact() to compare fact_value
directly. If the name changed, old fact is deleted and new one stored.
Also: the extraction prompt now includes the user's Discord display
name as a hint, so the LLM knows the authoritative name when extracting
facts during consolidation.
This commit is contained in:
@@ -266,6 +266,9 @@ def agent_prompt_prefix(prefix, cat):
|
||||
current_evil = cat.working_memory.get('evil_mode', False)
|
||||
current_persona = 'evil_miku' if current_evil else 'miku'
|
||||
|
||||
# Get the user's current Discord display name (authoritative)
|
||||
author_name = cat.working_memory.get('author_name', '')
|
||||
|
||||
# Build the facts section with persona annotations
|
||||
facts_text = "\n\n## Personal Facts About the User:\n"
|
||||
for fact, fact_persona in high_confidence_facts:
|
||||
@@ -275,6 +278,12 @@ def agent_prompt_prefix(prefix, cat):
|
||||
facts_text += f"- {fact} (learned as {source_label})\n"
|
||||
else:
|
||||
facts_text += f"- {fact}\n"
|
||||
|
||||
# Add authoritative Discord display name — this OVERRIDES any stale name facts
|
||||
if author_name:
|
||||
facts_text += f"\n**AUTHORITATIVE: The user's current Discord display name is \"{author_name}\".**\n"
|
||||
facts_text += "Use THIS name when addressing them. If any name fact above contradicts this, the display name is the truth.\n"
|
||||
|
||||
facts_text += "\n(Use these facts when answering the user's question)\n"
|
||||
prefix += facts_text
|
||||
print(f"[Declarative] Injected {len(high_confidence_facts)} facts into prompt (personas: {seen_personas}, current: {current_persona})")
|
||||
@@ -549,8 +558,16 @@ def extract_and_store_facts(client, memory_ids, cat, user_id, persona='miku'):
|
||||
else:
|
||||
persona_context = "\nNOTE: These messages were exchanged with Normal Miku (the cheerful virtual idol).\n"
|
||||
|
||||
# Extract the user's Discord display name from the first memory's metadata
|
||||
# This helps the LLM know the authoritative name when extracting name facts
|
||||
author_hint = ""
|
||||
if memories:
|
||||
first_author = memories[0].payload.get('metadata', {}).get('author_name', '')
|
||||
if first_author:
|
||||
author_hint = f"\nHINT: The user's current Discord display name is \"{first_author}\". Use this when determining their name.\n"
|
||||
|
||||
extraction_prompt = f"""Analyze these user messages and extract ONLY factual personal information.
|
||||
{persona_context}
|
||||
{persona_context}{author_hint}
|
||||
User messages:
|
||||
{conversation_context}
|
||||
|
||||
@@ -623,10 +640,26 @@ IMPORTANT:
|
||||
fact_type = 'education'
|
||||
fact_value = fact_text.split("graduated from")[-1].strip()
|
||||
|
||||
# Duplicate detection
|
||||
if _is_duplicate_fact(client, cat, fact_text, fact_type, user_id):
|
||||
print(f"[Fact Skip] Duplicate: {fact_text}")
|
||||
continue
|
||||
# Duplicate detection — with special handling for name facts
|
||||
# Name facts with different values replace old ones (don't skip)
|
||||
if fact_type == 'name':
|
||||
existing_name = _find_existing_fact(client, cat, fact_type, user_id)
|
||||
if existing_name:
|
||||
old_value = existing_name['payload']['metadata'].get('fact_value', '')
|
||||
if old_value.lower() != fact_value.lower():
|
||||
# Different name — delete old, store new
|
||||
client.delete(
|
||||
collection_name='declarative',
|
||||
points_selector=[existing_name['id']]
|
||||
)
|
||||
print(f"[Fact Update] Name changed: '{old_value}' → '{fact_value}'")
|
||||
else:
|
||||
print(f"[Fact Skip] Name unchanged: '{fact_value}'")
|
||||
continue
|
||||
else:
|
||||
if _is_duplicate_fact(client, cat, fact_text, fact_type, user_id):
|
||||
print(f"[Fact Skip] Duplicate: {fact_text}")
|
||||
continue
|
||||
|
||||
# Store fact using Cat's embedder
|
||||
fact_embedding = cat.embedder.embed_query(fact_text)
|
||||
@@ -661,6 +694,39 @@ IMPORTANT:
|
||||
return facts_stored
|
||||
|
||||
|
||||
def _find_existing_fact(client, cat, fact_type, user_id):
|
||||
"""
|
||||
Find an existing fact of a specific type for a user.
|
||||
Returns a dict with 'id' and 'payload' keys, or None.
|
||||
Used by name-fact update logic to replace old names with new ones.
|
||||
"""
|
||||
try:
|
||||
dummy_embedding = cat.embedder.embed_query("find fact")
|
||||
|
||||
results = client.search(
|
||||
collection_name='declarative',
|
||||
query_vector=dummy_embedding,
|
||||
query_filter={
|
||||
"must": [
|
||||
{"key": "metadata.source", "match": {"value": user_id}},
|
||||
{"key": "metadata.fact_type", "match": {"value": fact_type}},
|
||||
]
|
||||
},
|
||||
limit=1,
|
||||
score_threshold=0.0
|
||||
)
|
||||
|
||||
if results:
|
||||
point = results[0]
|
||||
return {'id': point.id, 'payload': {'metadata': point.payload.get('metadata', {})}}
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Find Fact] Error: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _is_duplicate_fact(client, cat, fact_text, fact_type, user_id):
|
||||
"""
|
||||
Check if a similar fact already exists for this user.
|
||||
|
||||
Reference in New Issue
Block a user