78ea059f08
- WelcomeScreen: first-time name entry with cute onboarding - identify WS message: sets user_id, loads saved prefs from Honcho - set_preference WS message: saves scene/outfit/accessory to Honcho metadata - Preferences auto-load on return visits via localStorage + Honcho peer meta - Kira uses the user's name in greeting and prompts - Backend: get/set preference methods in KiraMemory service - Frontend: optimistic preference updates, synced to backend on change
232 lines
7.5 KiB
Python
232 lines
7.5 KiB
Python
"""Honcho memory service for Kira.
|
|
|
|
Integrates Honcho persistent memory into Kira's conversation pipeline:
|
|
- User context retrieval before LLM calls
|
|
- Message storage after each exchange
|
|
- Cross-session memory for personalized responses
|
|
"""
|
|
|
|
import logging
|
|
from honcho import Honcho
|
|
from honcho.peer import Peer
|
|
from honcho.session import Session
|
|
from config import settings
|
|
|
|
logger = logging.getLogger("kira.memory")
|
|
|
|
|
|
class KiraMemory:
|
|
"""Manages Honcho memory for Kira conversations."""
|
|
|
|
def __init__(self):
|
|
self._honcho: Honcho | None = None
|
|
self._user_peer: Peer | None = None
|
|
self._kira_peer: Peer | None = None
|
|
self._session: Session | None = None
|
|
self._initialized = False
|
|
|
|
def init(self) -> bool:
|
|
"""Initialize Honcho connection. Returns False if not configured."""
|
|
api_key = settings.honcho_api_key
|
|
base_url = settings.honcho_base_url
|
|
|
|
if not api_key:
|
|
logger.warning("HONCHO_API_KEY not set — memory disabled")
|
|
return False
|
|
|
|
if not base_url:
|
|
self._honcho = Honcho(
|
|
api_key=api_key,
|
|
workspace_id="kira",
|
|
environment="production",
|
|
)
|
|
else:
|
|
self._honcho = Honcho(
|
|
api_key=api_key,
|
|
base_url=base_url,
|
|
workspace_id="kira",
|
|
)
|
|
|
|
logger.info(f"Honcho connected to workspace 'kira'")
|
|
self._initialized = True
|
|
return True
|
|
|
|
@property
|
|
def enabled(self) -> bool:
|
|
return self._initialized and self._honcho is not None
|
|
|
|
def ensure_peers(self, user_id: str = "default-user") -> None:
|
|
"""Get or create Honcho peers for the user and Kira."""
|
|
if not self.enabled:
|
|
return
|
|
|
|
self._user_peer = self._honcho.peer(user_id)
|
|
self._kira_peer = self._honcho.peer("kira")
|
|
|
|
logger.info(f"Peers ready: user={user_id}, kira")
|
|
|
|
def ensure_session(self, session_id: str) -> None:
|
|
"""Get or create a Honcho session for this conversation."""
|
|
if not self.enabled:
|
|
return
|
|
|
|
self._session = self._honcho.session(session_id)
|
|
|
|
# Add peers to session if not already members
|
|
if self._user_peer and self._kira_peer:
|
|
self._session.add_peers([self._user_peer, self._kira_peer])
|
|
|
|
logger.info(f"Session ready: {session_id}")
|
|
|
|
def get_user_context(self) -> str:
|
|
"""Query Honcho for context about the user.
|
|
|
|
Returns a string summary of what Honcho knows about the user,
|
|
to inject into the LLM system prompt. Empty string if no context.
|
|
"""
|
|
if not self.enabled or not self._user_peer:
|
|
return ""
|
|
|
|
try:
|
|
# Query Honcho's dialectic reasoning about the user
|
|
context = self._user_peer.chat(
|
|
"What should Kira know about this user? "
|
|
"Summarize their preferences, current projects, mood, "
|
|
"and any important context in 2-3 sentences."
|
|
)
|
|
if context:
|
|
return f"\n[Memory: {context}]"
|
|
return ""
|
|
except Exception as e:
|
|
logger.warning(f"Failed to get user context: {e}")
|
|
return ""
|
|
|
|
def get_kira_context(self) -> str:
|
|
"""Get what the user knows about Kira (relationship context)."""
|
|
if not self.enabled or not self._user_peer:
|
|
return ""
|
|
|
|
try:
|
|
context = self._user_peer.chat(
|
|
"What is the user's relationship with Kira? "
|
|
"How do they feel about their focus sessions? "
|
|
"Summarize in 1-2 sentences.",
|
|
target="kira",
|
|
)
|
|
if context:
|
|
return f"\n[Kira Context: {context}]"
|
|
return ""
|
|
except Exception as e:
|
|
logger.warning(f"Failed to get relationship context: {e}")
|
|
return ""
|
|
|
|
def build_system_prompt_suffix(self) -> str:
|
|
"""Build a context suffix to append to Kira's system prompt."""
|
|
if not self.enabled:
|
|
return ""
|
|
|
|
user_ctx = self.get_user_context()
|
|
kira_ctx = self.get_kira_context()
|
|
|
|
parts = [s for s in [user_ctx, kira_ctx] if s]
|
|
if not parts:
|
|
return ""
|
|
|
|
return "\n\n---\n### What Kira remembers:" + "".join(parts)
|
|
|
|
def store_messages(
|
|
self,
|
|
user_message: str,
|
|
kira_message: str,
|
|
) -> None:
|
|
"""Store a conversation exchange in Honcho."""
|
|
if not self.enabled or not self._session:
|
|
return
|
|
|
|
try:
|
|
messages = []
|
|
if self._user_peer:
|
|
messages.append(
|
|
self._user_peer.message(user_message)
|
|
)
|
|
if self._kira_peer:
|
|
messages.append(
|
|
self._kira_peer.message(kira_message)
|
|
)
|
|
|
|
if messages:
|
|
self._session.add_messages(messages)
|
|
logger.debug("Stored conversation exchange in Honcho")
|
|
except Exception as e:
|
|
logger.warning(f"Failed to store messages: {e}")
|
|
|
|
def store_user_message(self, text: str) -> None:
|
|
"""Store a single user message."""
|
|
if not self.enabled or not self._session or not self._user_peer:
|
|
return
|
|
try:
|
|
self._session.add_messages([self._user_peer.message(text)])
|
|
except Exception as e:
|
|
logger.warning(f"Failed to store user message: {e}")
|
|
|
|
def store_kira_message(self, text: str) -> None:
|
|
"""Store a single Kira message."""
|
|
if not self.enabled or not self._session or not self._kira_peer:
|
|
return
|
|
try:
|
|
self._session.add_messages([self._kira_peer.message(text)])
|
|
except Exception as e:
|
|
logger.warning(f"Failed to store Kira message: {e}")
|
|
|
|
# ─── User preferences (stored in Honcho peer metadata) ───
|
|
|
|
DEFAULT_PREFERENCES: dict[str, str] = {
|
|
"name": "",
|
|
"scene": "cozy-room",
|
|
"outfit": "cozy-hoodie",
|
|
"accessory": "",
|
|
}
|
|
|
|
def get_user_preferences(self, user_id: str) -> dict[str, str]:
|
|
"""Load user preferences from Honcho peer metadata."""
|
|
if not self.enabled:
|
|
return dict(self.DEFAULT_PREFERENCES)
|
|
|
|
try:
|
|
peer = self._honcho.peer(user_id)
|
|
meta = peer.metadata or {}
|
|
prefs = dict(self.DEFAULT_PREFERENCES)
|
|
for key in prefs:
|
|
val = meta.get(key)
|
|
if val is not None:
|
|
prefs[key] = str(val)
|
|
logger.info(f"Loaded preferences for {user_id}: {prefs}")
|
|
return prefs
|
|
except Exception as e:
|
|
logger.warning(f"Failed to load preferences: {e}")
|
|
return dict(self.DEFAULT_PREFERENCES)
|
|
|
|
def set_user_preference(
|
|
self, user_id: str, key: str, value: str
|
|
) -> bool:
|
|
"""Save a single user preference to Honcho peer metadata."""
|
|
if key not in self.DEFAULT_PREFERENCES:
|
|
logger.warning(f"Unknown preference key: {key}")
|
|
return False
|
|
|
|
try:
|
|
peer = self._honcho.peer(user_id)
|
|
# Merge with existing metadata
|
|
meta = dict(peer.metadata or {})
|
|
meta[key] = value
|
|
peer.set_metadata(meta)
|
|
logger.info(f"Saved preference {user_id}.{key} = {value}")
|
|
return True
|
|
except Exception as e:
|
|
logger.warning(f"Failed to save preference: {e}")
|
|
return False
|
|
|
|
|
|
# Singleton instance for the app
|
|
kira_memory = KiraMemory()
|