diff --git a/bot/static/js/memories.js b/bot/static/js/memories.js index 1187119..7353347 100644 --- a/bot/static/js/memories.js +++ b/bot/static/js/memories.js @@ -113,11 +113,15 @@ async function loadFacts() { data.facts.forEach((fact, i) => { const source = fact.metadata?.source || 'unknown'; const when = fact.metadata?.when ? new Date(fact.metadata.when * 1000).toLocaleString() : 'unknown'; + const persona = fact.metadata?.persona || 'miku'; + const personaBadge = persona === 'evil_miku' + ? '๐Ÿ˜ˆ Evil Miku' + : '๐ŸŽค Miku'; const factDataJson = escapeJsonForAttribute(fact); html += `
-
${escapeHtml(fact.content)}
+
${escapeHtml(fact.content)}${personaBadge}
Source: ${escapeHtml(source)} ยท ${when}
@@ -155,11 +159,15 @@ async function loadEpisodicMemories() { data.memories.forEach((mem, i) => { const source = mem.metadata?.source || 'unknown'; const when = mem.metadata?.when ? new Date(mem.metadata.when * 1000).toLocaleString() : 'unknown'; + const persona = mem.metadata?.persona || 'miku'; + const personaBadge = persona === 'evil_miku' + ? '๐Ÿ˜ˆ Evil Miku' + : '๐ŸŽค Miku'; const memDataJson = escapeJsonForAttribute(mem); html += `
-
${escapeHtml(mem.content)}
+
${escapeHtml(mem.content)}${personaBadge}
Source: ${escapeHtml(source)} ยท ${when}
diff --git a/cat-plugins/discord_bridge/discord_bridge.py b/cat-plugins/discord_bridge/discord_bridge.py index d77ae8f..80bf196 100644 --- a/cat-plugins/discord_bridge/discord_bridge.py +++ b/cat-plugins/discord_bridge/discord_bridge.py @@ -97,8 +97,12 @@ def before_cat_stores_episodic_memory(doc, cat): if author_name: doc.metadata['author_name'] = author_name + # Tag with persona so Evil Miku and Normal Miku know whose memory this is + evil_mode = cat.working_memory.get('evil_mode', False) + doc.metadata['persona'] = 'evil_miku' if evil_mode else 'miku' + print(f"๐Ÿ’พ [Discord Bridge] Storing memory (unconsolidated): {message[:50]}...") - print(f" User: {cat.user_id}, Guild: {guild_id}, Author: {author_name}") + print(f" User: {cat.user_id}, Guild: {guild_id}, Author: {author_name}, Persona: {doc.metadata['persona']}") return doc diff --git a/cat-plugins/memory_consolidation/memory_consolidation.py b/cat-plugins/memory_consolidation/memory_consolidation.py index e863f53..a10ea6a 100644 --- a/cat-plugins/memory_consolidation/memory_consolidation.py +++ b/cat-plugins/memory_consolidation/memory_consolidation.py @@ -77,19 +77,34 @@ def agent_prompt_prefix(prefix, cat): ) if results: - high_confidence_facts = [ - item[0].page_content - for item in results - if item[1] > 0.5 - ] + # Build list of (fact_text, persona) tuples from results + high_confidence_facts = [] + seen_personas = set() + for item in results: + if item[1] > 0.5: + doc = item[0] + fact_text = doc.page_content + fact_persona = doc.metadata.get('persona', 'miku') + high_confidence_facts.append((fact_text, fact_persona)) + seen_personas.add(fact_persona) if high_confidence_facts: + # Determine which persona is currently active + current_evil = cat.working_memory.get('evil_mode', False) + current_persona = 'evil_miku' if current_evil else 'miku' + + # Build the facts section with persona annotations facts_text = "\n\n## Personal Facts About the User:\n" - for fact in high_confidence_facts: - facts_text += f"- {fact}\n" + for fact, fact_persona in high_confidence_facts: + # Annotate facts that came from the OTHER persona + if fact_persona != current_persona: + source_label = "Evil Miku" if fact_persona == 'evil_miku' else "Miku" + facts_text += f"- {fact} (learned as {source_label})\n" + else: + facts_text += f"- {fact}\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") + print(f"[Declarative] Injected {len(high_confidence_facts)} facts into prompt (personas: {seen_personas}, current: {current_persona})") except Exception as e: print(f"[Declarative] Error: {e}") @@ -142,11 +157,16 @@ def before_cat_sends_message(message, cat): miku_response = str(message) if miku_response and len(miku_response.strip()) > 3: + # Determine which persona is active so the memory is tagged correctly + evil_mode = cat.working_memory.get('evil_mode', False) + persona = 'evil_miku' if evil_mode else 'miku' + metadata = { 'source': cat.user_id, 'when': datetime.now().timestamp(), 'stored_at': datetime.now().isoformat(), 'speaker': 'miku', + 'persona': persona, 'consolidated': False, 'guild_id': cat.working_memory.get('guild_id', 'dm'), 'channel_id': cat.working_memory.get('channel_id'), @@ -160,7 +180,7 @@ def before_cat_sends_message(message, cat): vector=vector, metadata=metadata ) - print(f"[Miku Memory] Stored response: {miku_response[:50]}...") + print(f"[Miku Memory] Stored response ({persona}): {miku_response[:50]}...") except Exception as e: print(f"[Miku Memory] Error storing response: {e}") @@ -211,7 +231,9 @@ def trigger_consolidation_sync(cat): to_delete = [] to_mark_consolidated = [] # Group user messages by source (user_id) for per-user fact extraction + # Also track which persona was active for each user's messages user_messages_by_source = {} + user_persona_by_source = {} # source -> set of personas seen for point in memories: content = point.payload.get('page_content', '').strip() @@ -235,7 +257,11 @@ def trigger_consolidation_sync(cat): source = metadata.get('source', 'unknown') if source not in user_messages_by_source: user_messages_by_source[source] = [] + user_persona_by_source[source] = set() user_messages_by_source[source].append(point.id) + # Track which persona was active when this message was stored + msg_persona = metadata.get('persona', 'miku') + user_persona_by_source[source].add(msg_persona) # Delete trivial memories if to_delete: @@ -255,8 +281,11 @@ def trigger_consolidation_sync(cat): # Extract facts per user total_facts = 0 for source_user_id, memory_ids in user_messages_by_source.items(): - print(f"[Consolidation] Extracting facts for user '{source_user_id}' from {len(memory_ids)} messages...") - facts = extract_and_store_facts(client, memory_ids, cat, source_user_id) + # Determine the dominant persona for this user's messages + personas = user_persona_by_source.get(source_user_id, {'miku'}) + dominant_persona = 'evil_miku' if 'evil_miku' in personas else 'miku' + print(f"[Consolidation] Extracting facts for user '{source_user_id}' from {len(memory_ids)} messages (persona: {dominant_persona})...") + facts = extract_and_store_facts(client, memory_ids, cat, source_user_id, dominant_persona) total_facts += facts print(f"[Consolidation] Extracted {facts} facts for user '{source_user_id}'") @@ -275,10 +304,10 @@ def trigger_consolidation_sync(cat): # FACT EXTRACTION # =================================================================== -def extract_and_store_facts(client, memory_ids, cat, user_id): +def extract_and_store_facts(client, memory_ids, cat, user_id, persona='miku'): """ Extract declarative facts from user memories using LLM and store them. - Facts are scoped to the specific user_id. + Facts are scoped to the specific user_id and tagged with the source persona. Uses Cat's embedder to ensure vector compatibility. Deduplicates against existing facts before storing. """ @@ -300,8 +329,16 @@ def extract_and_store_facts(client, memory_ids, cat, user_id): for mem in batch ]) - extraction_prompt = f"""Analyze these user messages and extract ONLY factual personal information. + # Add persona context to the extraction prompt so the LLM knows + # which version of Miku was active during these conversations + persona_context = "" + if persona == 'evil_miku': + persona_context = "\nNOTE: These messages were exchanged with Evil Miku (the cruel, sadistic alter-ego).\n" + else: + persona_context = "\nNOTE: These messages were exchanged with Normal Miku (the cheerful virtual idol).\n" + extraction_prompt = f"""Analyze these user messages and extract ONLY factual personal information. +{persona_context} User messages: {conversation_context} @@ -395,13 +432,14 @@ IMPORTANT: 'when': datetime.now().timestamp(), 'fact_type': fact_type, 'fact_value': fact_value, + 'persona': persona, } } }] ) facts_stored += 1 - print(f"[Fact Stored] [{user_id}] {fact_text}") + print(f"[Fact Stored] [{user_id}] ({persona}) {fact_text}") except Exception as e: print(f"[LLM Extract] Error: {e}")