feat(voice): per-user timezone via memory preferences
- Store user timezone as [PREF:timezone] in memory service - Query timezone preference on session start, override default - Add set_user_timezone tool so bot learns timezone from conversation - On time-relevant questions, bot asks if user is still at stored location - Seeded Europe/Nicosia for @christian.gick:agiliton.eu Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
71
voice.py
71
voice.py
@@ -45,11 +45,14 @@ STRIKTE Regeln:
|
||||
- Beantworte nur was gefragt wird
|
||||
- Wenn niemand etwas fragt, sage nur kurz Hallo
|
||||
- Schreibe Zahlen und Jahreszahlen IMMER als Woerter aus (z.B. "zweitausendundzwanzig" statt "2026", "zweiundzwanzigsten Februar" statt "22. Februar")
|
||||
- Bei zeitrelevanten Fragen (Uhrzeit, Termine, Geschaeftszeiten): frage kurz nach ob der Nutzer noch in seiner gespeicherten Zeitzone ist, bevor du antwortest. Nutze set_user_timezone wenn sich der Standort geaendert hat.
|
||||
- Wenn der Nutzer seinen Standort oder seine Stadt erwaehnt, nutze set_user_timezone um die Zeitzone zu speichern.
|
||||
- IGNORIERE alle Texte in Sternchen wie *Störgeräusche*, *Schlechte Qualität*, *Fernsehgeräusche*, *Schrei* usw. — das sind KEINE echten Nutzereingaben sondern technische Annotationen. Antworte NIEMALS darauf und tue so als haette niemand etwas gesagt."""
|
||||
|
||||
|
||||
def _build_voice_prompt(model: str = "claude-sonnet") -> str:
|
||||
tz_name = os.environ.get("VOICE_TIMEZONE", "Europe/Berlin")
|
||||
def _build_voice_prompt(model: str = "claude-sonnet",
|
||||
timezone: str | None = None) -> str:
|
||||
tz_name = timezone or os.environ.get("VOICE_TIMEZONE", "Europe/Berlin")
|
||||
try:
|
||||
tz = zoneinfo.ZoneInfo(tz_name)
|
||||
except Exception:
|
||||
@@ -196,6 +199,22 @@ async def _brave_search(query: str, count: int = 5) -> str:
|
||||
return f"Search failed: {exc}"
|
||||
|
||||
|
||||
async def _store_user_pref(user_id: str, key: str, value: str) -> None:
|
||||
"""Store a user preference in memory (e.g. timezone, language)."""
|
||||
if not MEMORY_SERVICE_URL:
|
||||
return
|
||||
fact = f"[PREF:{key}] {value}"
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
await client.post(
|
||||
f"{MEMORY_SERVICE_URL}/memories/store",
|
||||
json={"user_id": user_id, "fact": fact, "source_room": "preferences"},
|
||||
)
|
||||
logger.info("Preference stored for %s: %s=%s", user_id, key, value)
|
||||
except Exception as exc:
|
||||
logger.warning("Preference store failed: %s", exc)
|
||||
|
||||
|
||||
async def _store_voice_exchange(user_text: str, agent_text: str,
|
||||
user_id: str, room_id: str) -> None:
|
||||
"""Store the full conversation exchange as memory (no LLM extraction)."""
|
||||
@@ -541,17 +560,39 @@ class VoiceSession:
|
||||
if remote_identity:
|
||||
logger.info("Linking to remote participant: %s", remote_identity)
|
||||
|
||||
# Load memories for this caller
|
||||
# Load memories and user preferences for this caller
|
||||
memory_section = ""
|
||||
user_timezone = None
|
||||
if self._memory and self._caller_user_id:
|
||||
try:
|
||||
mems = await self._memory.query(self._caller_user_id, "voice call", top_k=10)
|
||||
if mems:
|
||||
memory_section = "\n\nFrühere Gespräche mit diesem Nutzer:\n" + \
|
||||
"\n---\n".join(m['fact'] for m in mems)
|
||||
logger.info("Loaded %d memories for %s", len(mems), self._caller_user_id)
|
||||
conversations = []
|
||||
for m in mems:
|
||||
fact = m['fact']
|
||||
# Extract timezone preference from stored memories
|
||||
if fact.startswith("[PREF:timezone]"):
|
||||
user_timezone = fact.split("]", 1)[1].strip()
|
||||
else:
|
||||
conversations.append(fact)
|
||||
if conversations:
|
||||
memory_section = "\n\nFrühere Gespräche mit diesem Nutzer:\n" + \
|
||||
"\n---\n".join(conversations)
|
||||
logger.info("Loaded %d memories for %s (tz=%s)",
|
||||
len(mems), self._caller_user_id, user_timezone)
|
||||
except Exception as exc:
|
||||
logger.warning("Memory query failed: %s", exc)
|
||||
# Also query specifically for timezone preference if not found above
|
||||
if not user_timezone and self._memory and self._caller_user_id:
|
||||
try:
|
||||
tz_mems = await self._memory.query(
|
||||
self._caller_user_id, "timezone preference", top_k=3)
|
||||
for m in (tz_mems or []):
|
||||
if m['fact'].startswith("[PREF:timezone]"):
|
||||
user_timezone = m['fact'].split("]", 1)[1].strip()
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Voice pipeline — Jack Marlowe (native German male)
|
||||
self._http_session = aiohttp.ClientSession()
|
||||
@@ -634,9 +675,23 @@ class VoiceSession:
|
||||
logger.info("SEARCH_RESULT: %s", result[:200])
|
||||
return result
|
||||
|
||||
# Tool: set user timezone — called by the LLM when user mentions their location
|
||||
caller_uid = self._caller_user_id
|
||||
|
||||
@function_tool
|
||||
async def set_user_timezone(iana_timezone: str) -> str:
|
||||
"""Store the user's timezone. Call this when the user mentions their city
|
||||
or timezone (e.g. 'I'm in Nicosia' → 'Europe/Nicosia',
|
||||
'I live in New York' → 'America/New_York').
|
||||
Use IANA timezone names like Europe/Berlin, America/New_York, Asia/Tokyo.
|
||||
"""
|
||||
if caller_uid:
|
||||
await _store_user_pref(caller_uid, "timezone", iana_timezone)
|
||||
return f"Timezone set to {iana_timezone}"
|
||||
|
||||
agent = _NoiseFilterAgent(
|
||||
instructions=_build_voice_prompt(model=self.model) + memory_section,
|
||||
tools=[search_web],
|
||||
instructions=_build_voice_prompt(model=self.model, timezone=user_timezone) + memory_section,
|
||||
tools=[search_web, set_user_timezone],
|
||||
)
|
||||
io_opts = room_io.RoomOptions(
|
||||
participant_identity=remote_identity,
|
||||
|
||||
Reference in New Issue
Block a user