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:
Christian Gick
2026-02-23 11:02:03 +02:00
parent e84260f839
commit 1ec63b93f2

View File

@@ -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,