Fix reply-context speaker confusion with structured metadata pipeline

Previously, when a user replied to Miku's message via Discord's reply
feature, Miku's quoted words were embedded directly into the user's
message text using the format:
  [Replying to your message: "Miku's words"] User's response

This caused two problems:
1. The LLM had to parse "your message" to determine the quoted text
   was MIKU's words — fragile and frequently misattributed
2. When stored in episodic memory as [User]: ..., Miku's quoted words
   were permanently mislabeled under the user's speaker prefix

Now reply context flows through as structured metadata:
- bot/bot.py captures the replied-to text WITHOUT embedding it in prompt
- cat_client.py passes it as discord_reply_context in the WebSocket payload
- discord_bridge.py injects it as agent_input['reply_context'] — a
  CLEARLY LABELED note: [The user is replying to what you (Miku) said — ...]
- miku_personality.py + evil_miku_personality.py render it via
  {reply_context} placeholder in the prompt suffix, between memory
  context and conversation history

This keeps Miku's words as a separate context note, never mixed into
the user's HumanMessage. Episodic memory only stores the user's actual
words. The fallback path (when Cat is unavailable) also uses a cleaner
format with explicit speaker labels.
This commit is contained in:
2026-06-03 22:50:03 +03:00
parent 9d2c14fa0b
commit 486acb5c14
5 changed files with 39 additions and 5 deletions

View File

@@ -44,6 +44,7 @@ def before_cat_reads_message(user_message_json: dict, cat) -> dict:
evil_mode = user_message_json.get('discord_evil_mode', False)
media_type = user_message_json.get('discord_media_type', None)
activity = user_message_json.get('discord_activity', None)
reply_context = user_message_json.get('discord_reply_context', None)
# Also check working memory for backward compatibility
if not guild_id:
@@ -57,6 +58,7 @@ def before_cat_reads_message(user_message_json: dict, cat) -> dict:
cat.working_memory['evil_mode'] = evil_mode
cat.working_memory['media_type'] = media_type
cat.working_memory['activity'] = activity
cat.working_memory['reply_context'] = reply_context
return user_message_json
@@ -375,7 +377,21 @@ Please respond in a way that reflects this emotional tone."""
print(f" [Discord Bridge] Error building system prefix: {e}")
system_prefix = cat.working_memory.get('full_system_prefix', '[system prefix not available]')
full_prompt = f"{system_prefix}\n\n# Context\n\n{episodic_mem}\n\n{declarative_mem}\n\n{tools_output}\n\n# Conversation until now:\nHuman: {user_input}"
# Build reply context note if the user is replying to Miku's message.
# This injects Miku's quoted words as a SEPARATE clearly-labeled context note
# (not embedded in the user's message text). Keeps speaker boundaries intact
# and prevents the LLM from misattributing Miku's words to the user.
# Uses a colon+space delimiter (no nested quotes) to avoid formatting issues
# when the replied message itself contains double-quote characters.
reply_context = cat.working_memory.get('reply_context')
if reply_context:
reply_context_note = f'[The user is replying to what you (Miku) said — you said: {reply_context}]'
agent_input['reply_context'] = reply_context_note
else:
reply_context_note = ''
agent_input['reply_context'] = ''
full_prompt = f"{system_prefix}\n\n# Context\n\n{episodic_mem}\n\n{declarative_mem}\n\n{tools_output}\n\n{reply_context_note}\n\n# Conversation until now:\nHuman: {user_input}"
cat.working_memory['last_full_prompt'] = full_prompt
return agent_input