feat: user personalization with Honcho-backed preferences

- 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
This commit is contained in:
2026-06-04 11:00:58 -04:00
parent 97424cb98f
commit 78ea059f08
6 changed files with 396 additions and 80 deletions
+78 -52
View File
@@ -6,6 +6,7 @@ Real-time speech-to-speech pipeline:
Honcho memory integration:
Cross-session user context injected into LLM prompts,
conversation exchanges stored for continuous learning.
User preferences (name, scene, outfit, accessory) persisted in peer metadata.
"""
import json
@@ -65,10 +66,8 @@ def build_system_prompt(user_id: str) -> dict:
"""Build system prompt with Honcho memory context injected."""
base = BASE_SYSTEM_PROMPT
# Append memory context if Honcho is available
if kira_memory.enabled:
try:
# Get user-specific context from Honcho
kira_memory.ensure_peers(user_id)
memory_suffix = kira_memory.build_system_prompt_suffix()
if memory_suffix:
@@ -79,35 +78,89 @@ def build_system_prompt(user_id: str) -> dict:
return {"role": "system", "content": base}
def handle_identify(msg: dict, session_id: str) -> dict | None:
"""Handle user identification. Returns user preferences or None."""
user_id = msg.get("user_id", "").strip()
if not user_id:
return {"type": "error", "message": "user_id is required"}
user_name = msg.get("name", "").strip()
if user_name:
kira_memory.set_user_preference(user_id, "name", user_name)
prefs = kira_memory.get_user_preferences(user_id)
logger.info(f"[{session_id}] Identified as {user_id} (name={user_name or prefs.get('name', '')})")
return {
"type": "identified",
"user_id": user_id,
"preferences": prefs,
}
def handle_set_preference(msg: dict, session_id: str, user_id: str) -> dict | None:
"""Handle preference update. Returns success status."""
if not user_id or user_id == "default-user":
return {"type": "error", "message": "Must identify first"}
key = msg.get("key", "").strip()
value = msg.get("value", "").strip()
if not key:
return {"type": "error", "message": "key is required"}
ok = kira_memory.set_user_preference(user_id, key, value)
return {
"type": "preference_saved",
"key": key,
"success": ok,
}
@app.websocket("/api/ws")
async def conversation_ws(websocket: WebSocket):
await websocket.accept()
session_id = str(uuid.uuid4())[:8]
user_id = "default-user"
identified = False
logger.info(f"[{session_id}] WebSocket connected")
# Audio buffer accumulates chunks from one utterance
audio_buffer = bytearray()
conversation_history: list[dict] = []
# Initialize Honcho for this session
if kira_memory.enabled:
try:
kira_memory.ensure_peers(user_id)
kira_memory.ensure_session(session_id)
logger.info(f"[{session_id}] Honcho session ready")
except Exception as e:
logger.warning(f"[{session_id}] Honcho setup failed: {e}")
try:
first_message = True
while True:
raw = await websocket.receive_text()
msg = json.loads(raw)
msg_type = msg.get("type", "")
# Build system prompt fresh each turn to get latest Honcho context
# ── Identity & Preferences ──
if msg_type == "identify":
response = handle_identify(msg, session_id)
if response:
await websocket.send_json(response)
if response["type"] == "identified":
user_id = response["user_id"]
identified = True
# Set up Honcho for this user
if kira_memory.enabled:
try:
kira_memory.ensure_peers(user_id)
kira_memory.ensure_session(session_id)
logger.info(f"[{session_id}] Honcho session ready for {user_id}")
except Exception as e:
logger.warning(f"[{session_id}] Honcho setup failed: {e}")
continue
if msg_type == "set_preference":
response = handle_set_preference(msg, session_id, user_id)
if response:
await websocket.send_json(response)
continue
# ── Conversation ──
system_prompt = build_system_prompt(user_id)
if msg_type == "audio_chunk":
@@ -121,7 +174,6 @@ async def conversation_ws(websocket: WebSocket):
logger.info(f"[{session_id}] Transcribing {len(audio_buffer)} bytes...")
# 1. Speech-to-text
transcript = await transcribe_audio(bytes(audio_buffer))
audio_buffer.clear()
@@ -129,45 +181,27 @@ async def conversation_ws(websocket: WebSocket):
await websocket.send_json({"type": "error", "message": "Could not transcribe audio"})
continue
# Echo transcript
await websocket.send_json({
"type": "transcript",
"text": transcript,
})
await websocket.send_json({"type": "transcript", "text": transcript})
# 2. LLM call
logger.info(f"[{session_id}] User: {transcript}")
user_msg = {"role": "user", "content": transcript}
conversation_history.append(user_msg)
conversation_history.append({"role": "user", "content": transcript})
messages = [system_prompt] + conversation_history[-10:]
kira_text = await get_kira_response(messages)
assistant_msg = {"role": "assistant", "content": kira_text}
conversation_history.append(assistant_msg)
conversation_history.append({"role": "assistant", "content": kira_text})
logger.info(f"[{session_id}] Kira: {kira_text}")
# 3. Store in Honcho
if kira_memory.enabled:
if kira_memory.enabled and identified:
try:
kira_memory.store_messages(transcript, kira_text)
except Exception as e:
logger.warning(f"[{session_id}] Failed to store messages: {e}")
# 4. TTS
await websocket.send_json({
"type": "speaking_start",
"text": kira_text,
})
await websocket.send_json({"type": "speaking_start", "text": kira_text})
audio_bytes = await synthesize_speech(kira_text)
audio_b64 = base64.b64encode(audio_bytes).decode("utf-8")
await websocket.send_json({
"type": "audio",
"data": audio_b64,
"text": kira_text,
})
await websocket.send_json({"type": "audio", "data": audio_b64, "text": kira_text})
await websocket.send_json({"type": "speaking_end"})
elif msg_type == "ping":
@@ -179,32 +213,24 @@ async def conversation_ws(websocket: WebSocket):
continue
logger.info(f"[{session_id}] User (text): {user_text}")
user_msg = {"role": "user", "content": user_text}
conversation_history.append(user_msg)
conversation_history.append({"role": "user", "content": user_text})
messages = [system_prompt] + conversation_history[-10:]
kira_text = await get_kira_response(messages)
assistant_msg = {"role": "assistant", "content": kira_text}
conversation_history.append(assistant_msg)
conversation_history.append({"role": "assistant", "content": kira_text})
logger.info(f"[{session_id}] Kira: {kira_text}")
# Store in Honcho
if kira_memory.enabled:
if kira_memory.enabled and identified:
try:
kira_memory.store_messages(user_text, kira_text)
except Exception as e:
logger.warning(f"[{session_id}] Failed to store messages: {e}")
# TTS
await websocket.send_json({"type": "speaking_start", "text": kira_text})
audio_bytes = await synthesize_speech(kira_text)
audio_b64 = base64.b64encode(audio_bytes).decode("utf-8")
await websocket.send_json({
"type": "audio",
"data": audio_b64,
"text": kira_text,
})
await websocket.send_json({"type": "audio", "data": audio_b64, "text": kira_text})
await websocket.send_json({"type": "speaking_end"})
except WebSocketDisconnect:
+48
View File
@@ -178,6 +178,54 @@ class KiraMemory:
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()