Files
miku-discord/cat-plugins/miku_personality/miku_personality.py
koko210Serve 486acb5c14 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.
2026-06-03 22:50:03 +03:00

104 lines
3.8 KiB
Python

"""
Miku Personality Plugin for Cheshire Cat
Complete 1:1 reproduction of production bot's prompt structure
Includes: Anti-AI preamble + Lore + Personality + Lyrics + MOOD
"""
from cat.mad_hatter.decorators import hook
from cat.log import log
@hook(priority=100)
def agent_prompt_prefix(prefix, cat):
"""Override system prompt with COMPLETE production bot structure including mood"""
# Read the three knowledge files
try:
with open('/app/cat/data/miku/miku_lore.txt', 'r') as f:
lore = f.read()
with open('/app/cat/data/miku/miku_prompt.txt', 'r') as f:
prompt = f.read()
with open('/app/cat/data/miku/miku_lyrics.txt', 'r') as f:
lyrics = f.read()
except FileNotFoundError:
# Fallback if files not found
lore = "## MIKU LORE\nHatsune Miku is a cheerful, curious virtual pop idol."
prompt = "You are Hatsune Miku. Be cheerful and friendly."
lyrics = "## SONGS\nWorld is Mine, Senbonzakura, etc."
# Get mood from working memory (set by discord_bridge)
mood_name = cat.working_memory.get('mood', 'neutral')
log.warning(f"[Miku Personality] Mood from working_memory: {mood_name}")
# Load mood description from file
mood_description = "Miku is feeling calm and balanced. Her tone is polite, friendly, and natural. She responds clearly and thoughtfully without strong emotional bias. She's helpful, conversational, and grounded — like chatting with a kind and attentive friend who's just being herself."
try:
# Construct path to mood file
mood_file_path = f'/app/moods/{mood_name}.txt'
with open(mood_file_path, 'r', encoding='utf-8') as f:
mood_description = f.read().strip()
log.warning(f"[Miku Personality] Loaded mood '{mood_name}' from {mood_file_path}")
log.warning(f"[Miku Personality] Mood description: {mood_description[:100]}...")
except FileNotFoundError:
log.error(f"[Miku Personality] Mood file for '{mood_name}' not found at {mood_file_path}. Using default neutral mood.")
# Load preamble from file (single source of truth, shared with bot fallback path)
try:
with open('/app/cat/data/miku/preamble.txt', 'r', encoding='utf-8') as f:
preamble = f.read()
except FileNotFoundError:
log.error("[Miku Personality] preamble.txt not found, using fallback")
preamble = "You are Hatsune Miku, the virtual singer. You are NOT an AI assistant - you are Miku herself.\n\nYou ARE Miku. Act like it."
# Build prompt EXACTLY like production bot does
full_prefix = f"""{preamble}
---
## MIKU LORE (Complete Original)
{lore}
## MIKU PERSONALITY & GUIDELINES (Complete Original)
{prompt}
## MIKU SONG LYRICS (Complete Original)
{lyrics}
## CURRENT SITUATION
Miku is currently feeling: {mood_description}
Please respond in a way that reflects this emotional tone."""
# Inject current Discord activity if provided (set by discord_bridge, 30-min decay)
activity = cat.working_memory.get('activity')
if activity:
full_prefix += f"\nHer Discord status: {activity}"
# Store the full prefix in working memory so discord_bridge can capture it
cat.working_memory['full_system_prefix'] = full_prefix
return full_prefix
@hook(priority=100)
def agent_prompt_suffix(suffix, cat):
"""Keep memory context (episodic + declarative) but simplify conversation header"""
return """
# Context
{episodic_memory}
{declarative_memory}
{tools_output}
{reply_context}
# Conversation until now:
(Note: In the conversation below, "Human" = the person you're talking to, "AI" = you, Miku. Pay attention to who said what.)"""
@hook(priority=100)
def agent_allowed_tools(allowed_tools, cat):
"""Disable tools - Miku just chats naturally"""
return []