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:
+78
-52
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user