feat: Tarife einstellbar + 15 weitere Filler + Mitteilung bei Leer-Antwort

- Strompreis (EUR/kWh), Notruf-Kosten (EUR/Alarm), Cartesia ($/Zeichen) jetzt im
  Admin live einstellbar (RUNTIME_SETTABLE).
- OPENING-Beruhigungssaetze 12 -> 27 (random.choice; formale "Sie"-Uebersetzung
  ersetzt beim Neustart den alten "dich"-Cache).
- Leer-/kein-Treffer-Antwort: statt Stille eine freundliche lokalisierte
  Mitteilung ("Dazu konnte ich keine Informationen finden."), gesprochen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-30 13:53:46 +02:00
commit f875e421de
3 changed files with 44 additions and 4 deletions

View file

@ -10,6 +10,21 @@ from app.metrics import timer, metrics
# zwischen Geduldssätzen, bei noch laufender Recherche.
FILLER_PATIENCE_INTERVAL = 10.0
# Freundliche Mitteilung statt Stille, wenn keine Antwort zustande kommt
# (z. B. Web-Suche ohne Treffer). Senior wartet sonst vergeblich. Fallback Deutsch.
_NO_RESULT = {
"de": "Dazu konnte ich leider keine Informationen finden.",
"en": "I'm sorry, I couldn't find any information about that.",
"nl": "Daarover kon ik helaas geen informatie vinden.",
"fr": "Je suis désolé, je n'ai trouvé aucune information à ce sujet.",
"es": "Lo siento, no he encontrado información al respecto.",
"it": "Mi dispiace, non ho trovato informazioni in merito.",
}
def _no_result_message(language: str | None) -> str:
return _NO_RESULT.get((language or "de").lower(), _NO_RESULT["de"])
def _stage(name: str):
return timer("stage_duration_seconds", {"stage": name})
@ -135,8 +150,8 @@ class Orchestrator:
history=history,
language=effective_language,
)
if not trace.semantic_response:
raise RuntimeError("LLM returned an empty response")
if not (trace.semantic_response or "").strip():
trace.semantic_response = _no_result_message(effective_language)
trace.spoken_response = await self.spoken_adapter.run(
trace.semantic_response,
@ -330,9 +345,16 @@ class Orchestrator:
for sentence in chunker.feed(result):
await _dispatch(sentence)
trace.semantic_response = "".join(parts)
trace.semantic_response = "".join(parts).strip()
if not trace.semantic_response:
raise RuntimeError("LLM returned an empty response")
# Keine Antwort (z. B. Suche ohne Treffer) -> freundliche Mitteilung
# statt Stille, damit der Nutzer nicht vergeblich wartet.
trace.semantic_response = _no_result_message(effective_language)
if on_token:
await on_token(trace.semantic_response)
if chunker:
for sentence in chunker.feed(trace.semantic_response):
await _dispatch(sentence)
trace.citations = collected_citations
trace.web_searches = search_count[0]

View file

@ -35,6 +35,21 @@ OPENING_PHRASES = [
"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 = [

View file

@ -35,6 +35,9 @@ RUNTIME_SETTABLE: dict[str, tuple[str, str, str]] = {
"memory_extraction_every_n_turns": ("Extraktion alle N Turns", "int", "z.B. 3"),
"daily_request_limit": ("Tageskontingent (global)", "int", "0 = unbegrenzt"),
"cost_daily_limit_usd": ("Kostenbudget/Tag (global, $)", "float", "0 = unbegrenzt — pro Nutzer überschreibbar"),
"electricity_price_eur_per_kwh": ("Strompreis (€/kWh)", "float", "Kosten lokaler LLM-Anfragen (z.B. 0.33)"),
"emergency_alert_eur": ("Notruf-Kosten (€/Alarm)", "float", "SMS+Anruf pro Alarm (z.B. 0.20)"),
"cartesia_usd_per_char": ("Cartesia-TTS ($/Zeichen)", "float", "z.B. 0.00005 (100k Credits ≈ 100.000 Zeichen)"),
"emergency_cooldown_minutes": ("Notruf-Sperre (Minuten)", "int", "Erneuter Alarm für N Minuten blockiert (Standard 5, 0 = aus)"),
}