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:
69
voice.py
69
voice.py
@@ -45,11 +45,14 @@ STRIKTE Regeln:
|
|||||||
- Beantworte nur was gefragt wird
|
- Beantworte nur was gefragt wird
|
||||||
- Wenn niemand etwas fragt, sage nur kurz Hallo
|
- 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")
|
- 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."""
|
- 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:
|
def _build_voice_prompt(model: str = "claude-sonnet",
|
||||||
tz_name = os.environ.get("VOICE_TIMEZONE", "Europe/Berlin")
|
timezone: str | None = None) -> str:
|
||||||
|
tz_name = timezone or os.environ.get("VOICE_TIMEZONE", "Europe/Berlin")
|
||||||
try:
|
try:
|
||||||
tz = zoneinfo.ZoneInfo(tz_name)
|
tz = zoneinfo.ZoneInfo(tz_name)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -196,6 +199,22 @@ async def _brave_search(query: str, count: int = 5) -> str:
|
|||||||
return f"Search failed: {exc}"
|
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,
|
async def _store_voice_exchange(user_text: str, agent_text: str,
|
||||||
user_id: str, room_id: str) -> None:
|
user_id: str, room_id: str) -> None:
|
||||||
"""Store the full conversation exchange as memory (no LLM extraction)."""
|
"""Store the full conversation exchange as memory (no LLM extraction)."""
|
||||||
@@ -541,17 +560,39 @@ class VoiceSession:
|
|||||||
if remote_identity:
|
if remote_identity:
|
||||||
logger.info("Linking to remote participant: %s", 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 = ""
|
memory_section = ""
|
||||||
|
user_timezone = None
|
||||||
if self._memory and self._caller_user_id:
|
if self._memory and self._caller_user_id:
|
||||||
try:
|
try:
|
||||||
mems = await self._memory.query(self._caller_user_id, "voice call", top_k=10)
|
mems = await self._memory.query(self._caller_user_id, "voice call", top_k=10)
|
||||||
if mems:
|
if mems:
|
||||||
|
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" + \
|
memory_section = "\n\nFrühere Gespräche mit diesem Nutzer:\n" + \
|
||||||
"\n---\n".join(m['fact'] for m in mems)
|
"\n---\n".join(conversations)
|
||||||
logger.info("Loaded %d memories for %s", len(mems), self._caller_user_id)
|
logger.info("Loaded %d memories for %s (tz=%s)",
|
||||||
|
len(mems), self._caller_user_id, user_timezone)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("Memory query failed: %s", 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)
|
# Voice pipeline — Jack Marlowe (native German male)
|
||||||
self._http_session = aiohttp.ClientSession()
|
self._http_session = aiohttp.ClientSession()
|
||||||
@@ -634,9 +675,23 @@ class VoiceSession:
|
|||||||
logger.info("SEARCH_RESULT: %s", result[:200])
|
logger.info("SEARCH_RESULT: %s", result[:200])
|
||||||
return result
|
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(
|
agent = _NoiseFilterAgent(
|
||||||
instructions=_build_voice_prompt(model=self.model) + memory_section,
|
instructions=_build_voice_prompt(model=self.model, timezone=user_timezone) + memory_section,
|
||||||
tools=[search_web],
|
tools=[search_web, set_user_timezone],
|
||||||
)
|
)
|
||||||
io_opts = room_io.RoomOptions(
|
io_opts = room_io.RoomOptions(
|
||||||
participant_identity=remote_identity,
|
participant_identity=remote_identity,
|
||||||
|
|||||||
Reference in New Issue
Block a user