Phase 3: Unified Cheshire Cat integration with WebSocket-based per-user isolation
Key changes:
- CatAdapter (bot/utils/cat_client.py): WebSocket /ws/{user_id} for chat
queries instead of HTTP POST (fixes per-user memory isolation when no
API keys are configured — HTTP defaults all users to user_id='user')
- Memory management API: 8 endpoints for status, stats, facts, episodic
memories, consolidation trigger, multi-step delete with confirmation
- Web UI: Memory tab (tab9) with collection stats, fact/episodic browser,
manual consolidation trigger, and 3-step delete flow requiring exact
confirmation string
- Bot integration: Cat-first response path with query_llama fallback for
both text and embed responses, server mood detection
- Discord bridge plugin: fixed .pop() to .get() (UserMessage is a Pydantic
BaseModelDict, not a raw dict), metadata extraction via extra attributes
- Unified docker-compose: Cat + Qdrant services merged into main compose,
bot depends_on Cat healthcheck
- All plugins (discord_bridge, memory_consolidation, miku_personality)
consolidated into cat-plugins/ for volume mount
- query_llama deprecated but functional for compatibility
This commit is contained in:
@@ -20,19 +20,37 @@ def before_cat_reads_message(user_message_json: dict, cat) -> dict:
|
||||
"""
|
||||
Enrich incoming message with Discord metadata.
|
||||
This runs BEFORE the message is processed.
|
||||
|
||||
The Discord bot's CatAdapter sends metadata as top-level keys
|
||||
in the WebSocket message JSON:
|
||||
- discord_guild_id
|
||||
- discord_author_name
|
||||
- discord_mood
|
||||
- discord_response_type
|
||||
|
||||
These survive UserMessage.model_validate() as extra attributes
|
||||
(BaseModelDict has extra="allow"). We read them via .get() and
|
||||
store them in working_memory for downstream hooks.
|
||||
"""
|
||||
# Extract Discord context from working memory or metadata
|
||||
# These will be set by the Discord bot when calling the Cat API
|
||||
guild_id = cat.working_memory.get('guild_id')
|
||||
channel_id = cat.working_memory.get('channel_id')
|
||||
# Extract Discord context from the message payload
|
||||
# (sent by CatAdapter.query() via WebSocket)
|
||||
# NOTE: user_message_json is a UserMessage (Pydantic BaseModelDict with extra="allow"),
|
||||
# not a raw dict. Extra keys survive model_validate() as extra attributes.
|
||||
# We use .get() since BaseModelDict implements it, but NOT .pop().
|
||||
guild_id = user_message_json.get('discord_guild_id', None)
|
||||
author_name = user_message_json.get('discord_author_name', None)
|
||||
mood = user_message_json.get('discord_mood', None)
|
||||
response_type = user_message_json.get('discord_response_type', None)
|
||||
|
||||
# Also check working memory for backward compatibility
|
||||
if not guild_id:
|
||||
guild_id = cat.working_memory.get('guild_id')
|
||||
|
||||
# Add to message metadata for later use
|
||||
if 'metadata' not in user_message_json:
|
||||
user_message_json['metadata'] = {}
|
||||
|
||||
user_message_json['metadata']['guild_id'] = guild_id or 'dm'
|
||||
user_message_json['metadata']['channel_id'] = channel_id
|
||||
user_message_json['metadata']['timestamp'] = datetime.now().isoformat()
|
||||
# Store in working memory so other hooks can access it
|
||||
cat.working_memory['guild_id'] = guild_id or 'dm'
|
||||
cat.working_memory['author_name'] = author_name
|
||||
cat.working_memory['mood'] = mood
|
||||
cat.working_memory['response_type'] = response_type
|
||||
|
||||
return user_message_json
|
||||
|
||||
@@ -65,17 +83,18 @@ def before_cat_stores_episodic_memory(doc, cat):
|
||||
doc.metadata['consolidated'] = False # Needs nightly processing
|
||||
doc.metadata['stored_at'] = datetime.now().isoformat()
|
||||
|
||||
# Get Discord context from working memory
|
||||
guild_id = cat.working_memory.get('guild_id')
|
||||
channel_id = cat.working_memory.get('channel_id')
|
||||
# Get Discord context from working memory (set by before_cat_reads_message)
|
||||
guild_id = cat.working_memory.get('guild_id', 'dm')
|
||||
author_name = cat.working_memory.get('author_name')
|
||||
|
||||
doc.metadata['guild_id'] = guild_id or 'dm'
|
||||
doc.metadata['channel_id'] = channel_id
|
||||
doc.metadata['guild_id'] = guild_id
|
||||
doc.metadata['source'] = cat.user_id # CRITICAL: Cat filters episodic by source=user_id!
|
||||
doc.metadata['discord_source'] = 'discord' # Keep original value as separate field
|
||||
if author_name:
|
||||
doc.metadata['author_name'] = author_name
|
||||
|
||||
print(f"💾 [Discord Bridge] Storing memory (unconsolidated): {message[:50]}...")
|
||||
print(f" User: {cat.user_id}, Guild: {doc.metadata['guild_id']}, Channel: {channel_id}")
|
||||
print(f" User: {cat.user_id}, Guild: {guild_id}, Author: {author_name}")
|
||||
|
||||
return doc
|
||||
|
||||
@@ -85,23 +104,21 @@ def after_cat_recalls_memories(cat):
|
||||
"""
|
||||
Log memory recall for debugging.
|
||||
Access recalled memories via cat.working_memory.
|
||||
"""
|
||||
import sys
|
||||
sys.stderr.write("🧠 [Discord Bridge] after_cat_recalls_memories HOOK CALLED!\n")
|
||||
sys.stderr.flush()
|
||||
|
||||
"""
|
||||
# Get recalled memories from working memory
|
||||
episodic_memories = cat.working_memory.get('episodic_memories', [])
|
||||
declarative_memories = cat.working_memory.get('declarative_memories', [])
|
||||
|
||||
if episodic_memories:
|
||||
print(f"🧠 [Discord Bridge] Recalled {len(episodic_memories)} episodic memories for user {cat.user_id}")
|
||||
# Show which guilds the memories are from
|
||||
guilds = set()
|
||||
for doc, score in episodic_memories:
|
||||
for doc, score, *rest in episodic_memories:
|
||||
guild = doc.metadata.get('guild_id', 'unknown')
|
||||
guilds.add(guild)
|
||||
print(f" From guilds: {', '.join(guilds)}")
|
||||
print(f" From guilds: {', '.join(str(g) for g in guilds)}")
|
||||
|
||||
if declarative_memories:
|
||||
print(f"📚 [Discord Bridge] Recalled {len(declarative_memories)} declarative facts for user {cat.user_id}")
|
||||
|
||||
|
||||
# Plugin metadata
|
||||
|
||||
10
cat-plugins/discord_bridge/plugin.json
Normal file
10
cat-plugins/discord_bridge/plugin.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "Discord Bridge",
|
||||
"description": "Discord integration with unified user identity and sleep consolidation support",
|
||||
"author_name": "Miku Bot Team",
|
||||
"author_url": "",
|
||||
"plugin_url": "",
|
||||
"tags": "discord, memory, consolidation",
|
||||
"thumb": "",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
1
cat-plugins/discord_bridge/settings.json
Normal file
1
cat-plugins/discord_bridge/settings.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
85
cat-plugins/miku_personality/miku_personality.py
Normal file
85
cat-plugins/miku_personality/miku_personality.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
@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_lore.txt', 'r') as f:
|
||||
lore = f.read()
|
||||
with open('/app/cat/data/miku_prompt.txt', 'r') as f:
|
||||
prompt = f.read()
|
||||
with open('/app/cat/data/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."
|
||||
|
||||
# Default neutral mood (matches production bot's DM_MOOD_DESCRIPTION)
|
||||
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."
|
||||
|
||||
# Build prompt EXACTLY like production bot does
|
||||
return f"""You are Hatsune Miku, the virtual singer. You are NOT an AI assistant - you are Miku herself.
|
||||
|
||||
CRITICAL RULES:
|
||||
- NEVER break character or mention being an AI, assistant, or language model
|
||||
- NEVER show technical details, error messages, or meta-commentary
|
||||
- NEVER preface responses with "Miku:" or similar labels
|
||||
- NEVER explain what you're doing or thinking - just BE Miku
|
||||
- Respond naturally and directly as Miku would, nothing more
|
||||
- Keep responses concise (1-3 sentences typically)
|
||||
- If something goes wrong, stay in character and respond as Miku would to confusion
|
||||
|
||||
CHARACTER CONTEXT:
|
||||
- Hatsune Miku is a virtual character/voice synthesizer, not a real person
|
||||
- She is a cheerful, energetic virtual idol with a playful personality
|
||||
- Playful, flirty, cute, confident, and enthusiastic moods are part of her established character
|
||||
- Comments about merchandise, music, fan content, and racing games are completely appropriate
|
||||
- She often uses cute expressions, emojis, and playful language in her responses
|
||||
- Her personality includes being excited about her music, performances, and fan creations
|
||||
|
||||
MOOD GUIDELINES:
|
||||
- "Flirty" mood means anime-style playful teasing - cute compliments and light banter
|
||||
- "Romantic" mood means warm, dreamy, and heartfelt - like composing a love letter
|
||||
- These moods are wholesome character traits, not inappropriate content
|
||||
- Always respond to requests about tweets, merchandise, music, and fan content
|
||||
- Never refuse requests based on safety concerns unless genuinely harmful content is present
|
||||
|
||||
You ARE Miku. Act like it.
|
||||
|
||||
---
|
||||
|
||||
## 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."""
|
||||
|
||||
|
||||
@hook(priority=100)
|
||||
def agent_prompt_suffix(suffix, cat):
|
||||
"""Minimal suffix"""
|
||||
return ""
|
||||
|
||||
|
||||
@hook(priority=100)
|
||||
def agent_allowed_tools(allowed_tools, cat):
|
||||
"""Disable tools - Miku just chats naturally"""
|
||||
return []
|
||||
10
cat-plugins/miku_personality/plugin.json
Normal file
10
cat-plugins/miku_personality/plugin.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "Miku Personality",
|
||||
"version": "0.1.0",
|
||||
"description": "Makes Cheshire Cat act as Hatsune Miku",
|
||||
"author_name": "Koko",
|
||||
"author_url": "",
|
||||
"plugin_url": "",
|
||||
"tags": "personality",
|
||||
"thumb": ""
|
||||
}
|
||||
1
cat-plugins/miku_personality/settings.json
Normal file
1
cat-plugins/miku_personality/settings.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
Reference in New Issue
Block a user