"""Senioren-Beruhigungssätze für die Web-Suche — englische Master-Quelle. DIES IST DIE EINZIGE STELLE zum Pflegen der Sätze. Zur Laufzeit werden sie einmal je Sprache übersetzt und gecacht (die Standardsprache wird beim Serverstart vorgewärmt → sofortige Ausgabe ohne Verzögerung). Zwei Pools: - OPENING: sofort beim Such-Start ("Einen Moment, ich schaue kurz nach.") - PATIENCE: bei längerer Recherche nachgeschoben ("Bitte noch etwas Geduld …") Beide werden ZUFÄLLIG gewählt, damit schnelles Feedback kommt und es nicht roboterhaft eintönig klingt. Siehe Docs/weg2-tool-calling.md §5.4. """ import asyncio import logging import random import re import httpx logger = logging.getLogger(__name__) ENDPOINT = "https://openrouter.ai/api/v1/chat/completions" # --- Master-Sätze (Englisch — einzige Quelle der Wahrheit) ------------------ OPENING_PHRASES = [ "One moment, let me look that up for you.", "Just a second, I'll check that for you right away.", "Let me find that out for you — one moment.", "Good question — let me check the latest on that.", "Hold on just a moment, I'm looking it up.", "Let me quickly find the current information for you.", "One little moment while I check that for you.", "I'll go and look that up for you right now.", "Let me see what the latest is — just a moment.", "Give me a second, I'll find that out for you.", "I'm checking that for you now, one moment please.", "Let me look that up so I can tell you exactly.", "Let me check the current details for you — it won't take long.", "I'll find the up-to-date answer for you, one moment.", "Allow me a brief moment to look into that.", "Let me gather the latest information for you.", "I'm on it — just give me a short moment.", "Let me make sure I have the current facts for you.", "One moment please, I'm fetching the latest for you.", "Let me quickly check what's current on that.", "I'll look into that for you right away, just a moment.", "Bear with me a second while I find that out.", "Let me get you the most recent information on that.", "Just a brief moment — I'm checking the latest for you.", "I'll dig that up for you now, one moment please.", "Let me find the current answer so I can tell you properly.", "Let me take a quick look and find that out for you.", ] PATIENCE_PHRASES = [ "Thank you for your patience, I'm almost there.", "Just a little longer, please — I'm still looking.", "Bear with me a moment, I'm nearly done.", "Almost ready — thank you for waiting.", "I'm still checking, just a few more seconds.", "Please hold on a little longer, I'm getting there.", "Nearly finished — thank you for your patience.", "Just a moment more, I want to get this right for you.", "I'm still looking it up, please stay with me.", "It's taking a little longer, but I'm almost done.", "Thank you for waiting — I'll have it for you shortly.", "Hang on just a bit longer, I'm nearly there.", "I haven't forgotten you — I'm still checking.", "Almost there now, thank you for being so patient.", "Just finishing up — one more moment, please.", "I'm making sure I get it right for you — nearly done.", ] _LANG_DISPLAY = { "de": "German", "en": "English", "fr": "French", "es": "Spanish", "it": "Italian", "nl": "Dutch", "pt": "Portuguese", "pl": "Polish", "ar": "Arabic", "ru": "Russian", "zh": "Chinese", } def _parse_numbered(text: str, n: int) -> list[str] | None: """Liest eine nummerierte Liste; liefert genau n Zeilen oder None (→ Fallback).""" out = [] for raw in (text or "").splitlines(): s = re.sub(r"^\s*\d+[.)]\s*", "", raw.strip()).strip().strip('"').strip("„“”") if s: out.append(s) return out[:n] if len(out) >= n else None def make_openrouter_translator(api_key: str, model: str): """Liefert eine async translate_pool(phrases, language) -> list[str] | None.""" api_key = (api_key or "").strip() model = (model or "").strip() async def translate_pool(phrases: list[str], language: str) -> list[str] | None: if not api_key or not model: return None target = _LANG_DISPLAY.get(language, language) numbered = "\n".join(f"{i + 1}. {p}" for i, p in enumerate(phrases)) prompt = ( f"Translate each numbered phrase into {target}. These are short, warm, " f"reassuring things a voice assistant says to an elderly person while it " f"looks something up. Keep them natural and spoken. ALWAYS use the polite, " f"formal form of address (German 'Sie', not 'du'; French 'vous'; Spanish " f"'usted'; Italian 'Lei'; Dutch 'u') consistently. Same count, same order, " f"no quotes, no extra words. Output ONLY a numbered list of the translations." f"\n\n{numbered}" ) payload = {"model": model, "temperature": 0.4, "messages": [{"role": "user", "content": prompt}]} headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client: resp = await client.post(ENDPOINT, json=payload, headers=headers) resp.raise_for_status() text = resp.json()["choices"][0]["message"]["content"] return _parse_numbered(text, len(phrases)) return translate_pool class FillerPhrases: """Zufällige, in die Zielsprache übersetzte Beruhigungssätze (einmal je Sprache gecacht).""" def __init__(self, translate_pool=None): self._translate_pool = translate_pool self._cache: dict[str, dict] = { "en": {"opening": OPENING_PHRASES, "patience": PATIENCE_PHRASES}} self._locks: dict[str, asyncio.Lock] = {} async def warm(self, language: str | None) -> None: """Vorab übersetzen (z. B. Standardsprache beim Serverstart).""" await self._ensure(language) async def _ensure(self, language: str | None) -> str: lang = (language or "en").lower() if lang in self._cache: return lang if self._translate_pool is None: return "en" # ohne Übersetzer: englische Master-Sätze lock = self._locks.setdefault(lang, asyncio.Lock()) async with lock: if lang in self._cache: return lang opening = patience = None try: opening = await self._translate_pool(OPENING_PHRASES, lang) patience = await self._translate_pool(PATIENCE_PHRASES, lang) except Exception as exc: # noqa: BLE001 — best effort, Fallback Englisch logger.warning("Filler-Übersetzung (%s) fehlgeschlagen: %s", lang, exc) self._cache[lang] = {"opening": opening or OPENING_PHRASES, "patience": patience or PATIENCE_PHRASES} return lang async def opening(self, language: str | None) -> str: lang = await self._ensure(language) return random.choice(self._cache[lang]["opening"]) async def patience(self, language: str | None) -> str: lang = await self._ensure(language) return random.choice(self._cache[lang]["patience"])