my_voice_assistant_v3_jamulix/Docs/weg2-tool-calling.md
dschlueter 96dcd87ca8 feat(fillers): zufällige, übersetzte Senioren-Beruhigungssätze + Geduld-Schleife
Ersetzt die statischen 3-Satz-Filler durch einen kreativen, weniger
roboterhaften Mechanismus für schnelles Feedback waehrend der Web-Suche.

- app/pipeline/fillers.py: EINZIGE Pflege-Stelle. Englische Master-Listen
  (12 OPENING + 16 PATIENCE). Zur Laufzeit einmal je Sprache uebersetzt und
  gecacht (durchgaengig hoefliche Anrede "Sie"/"vous"/"usted"...), Fallback
  Englisch. Zufaellige Auswahl statt fester Reihenfolge.
- Geduld-Schleife: bei laengerer Recherche schiebt ein Hintergrund-Task alle
  FILLER_PATIENCE_INTERVAL Sekunden einen zufaelligen PATIENCE-Satz nach;
  Abbruch beim ersten Antwort-Delta und im finally (keine verwaisten Tasks).
- Ephemer wie bisher: laeuft ueber on_token/_dispatch, nie in
  semantic_response/History.
- Standardsprache wird beim Start vorgewaermt (warmup.py) -> erster Such-Turn
  ohne Uebersetzungs-Latenz. dependencies: get_fillers()-Singleton.

Doc: Docs/weg2-tool-calling.md §5.4. Tests: 290 gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:34:15 +02:00

8.8 KiB
Raw Blame History

Design: Web-Suche per Tool-Calling (Weg 2)

Stand: Entwurf. Beschreibt das Konzept für die Frische-/Web-Such-Funktion auf Basis von agentischem Tool-Calling. Modellwahl und Schwellen sind durch den Eval-Harness (eval/tool_calling/) belegt; Implementierung folgt diesem Dokument.

1. Ziel & Leitidee

Das zentrale LLM kennt nur Wissen bis zu seinem Trainings-Cutoff. Fragt der Nutzer nach etwas, das sich seither geändert haben könnte (aktuelle Amtsträger, Preise, Wetter, Nachrichten, „lebt X noch", Öffnungszeiten …), soll das Modell selbst eine Web-Suche auslösen, die frischen Fakten holen und die Antwort in der Alexis-Persona formulieren.

Weg 2 = Tool-Calling + Anreichern: Das Antwortmodell entscheidet via Werkzeug, ob es sucht; die Suche liefert nur Fakten, das Modell formuliert. Vorteil: durchgängige Stimme, Persona/Safety/History bleiben beim Hauptmodell. Bewusst verworfen wurde der separate Vorab-Klassifikator („Weg 1"), weil das gewählte Modell sich zuverlässig genug selbst triggert (siehe §7).

2. Modellwahl (belegt)

mistralai/mistral-small-3.2-24b-instruct über OpenRouter. Eval-Ergebnis (69 Fälle × 5 Läufe): Trigger-Recall 93 %, Vorrang-Treue 100 %, Wohlgeformtheit 100 %, gefährliche Kategorien (Amtsträger/Preise/Nachrichten/ Sport/Releases) 100 %, Wetter inkl. implizitem Ort 98 %. Günstig und klein → niedrige Latenz (im Senioren-Kontext doppelt wertvoll). Details und Vergleich (Flash/Pro/alte Baseline) in der Memory-Notiz bzw. eval/tool_calling/.

Achtung: Die bisherige TOML-Baseline mistral-small-24b-instruct-2501 kann kein Tool-Calling (OpenRouter 404 „No endpoints found that support tool use"). Ein Revert darauf würde Weg 2 abschalten.

3. Keystone: Der Tool-Loop lebt im LLM-Provider

Ein neuer Wrapper ToolCallingLLM implementiert die bestehende LLMProvider-Schnittstelle (complete()/stream()), wickelt aber intern die Agenten-Schleife ab. Der Orchestrator ruft weiter nur self.llm.stream(...) und weiß nichts von Tools. Folge: ein neuer LLM_REGISTRY-Eintrag, keine Kern-Änderung, volle Kompatibilität mit resolve_route, Fallback-Ketten und Profilen. Das ist das Leitprinzip „jede Achse austauschbar".

ToolCallingLLM enthält einen eigenen tool-fähigen OpenRouter-Client (Keim: eval/tool_calling/client.py) — nicht den plain OpenRouterLLMProvider, da dessen complete() keine tools kennt.

4. Datenfluss

Kein-Tool-Turn (Normalfall, kein Latenz-Regress):

stream() → Modell mit tools=[…] → Text-Deltas → 1:1 an on_token/SentenceChunker

Such-Turn (augment):

(0) Koreferenz-Vorstufe: Pronomen aus History auflösen (gegated)
(1) Modell → tool_call("web_search", query)
(2) on_tool_start(language) → Filler SOFORT sprechen/anzeigen
(3) SonarTool(query) → Fakten   (~13 s, vom Filler überdeckt)
(4) Modell-Runde 2 mit tool-Ergebnis → finale Antwort, gestreamt, in Persona

5. Komponenten

5.1 SonarTool

Ruft perplexity/sonar über OpenRouter (gleiche Chat-API wie OpenRouterLLMProvider). Prompt an Sonar: knapp, Zielsprache, reine Fakten. Rückgabe als tool-Message ans Modell. Citations als strukturierte Metadaten an die UI-Bubble (nicht ins TTS). Timeout/Fehler → Sentinel „keine aktuellen Daten verfügbar", damit das Modell ehrlich punktet statt zu hängen.

5.2 ToolCallingLLM

Begrenzte Schleife (max. ~3 Runden gegen Runaway). System-Prompt = Senioren-Persona (SYSTEM_PROMPT aus openrouter.py) + kalibrierte Trigger-Leitlinie + Vorrang-Regel („Tool-Ergebnis ist maßgeblich und neuer als dein Wissen; bei Widerspruch folge dem Tool, nicht mischen"). Implementiert complete() (für chat_text/translate) und stream().

5.3 Koreferenz-Vorstufe (in v1)

Schließt die einzige nicht-triviale Eval-Restkante (ctx_alive_nl: nl + Pronomen-aus-History; benannt + de-Pronomen sind 5/5). Ein dekontextualisierender Rewrite macht die letzte Äußerung mit Hilfe des Verlaufs selbstständig („hij" → „Rutger Hauer"). Gegated: nur bei kurzer Folgefrage mit Pronomen und vorhandener History, damit er nicht jeden Turn kostet.

5.4 Filler / Beruhigung

stream() erhält einen on_tool_start(language)-Callback. Der Orchestrator verdrahtet ihn auf zwei Pools (app/pipeline/fillers.py): OPENING (sofort beim Such-Start) und PATIENCE (bei längerer Recherche nachgeschoben). Beide werden zufällig gewählt (schnelles Feedback, nicht roboterhaft) und gehen über spoken_adapter/tts_normalizer/ttson_audio und on_token.

Eine Pflege-Stelle, Englisch: Die Master-Sätze stehen als englische Listen in fillers.py (OPENING_PHRASES/PATIENCE_PHRASES). Zur Laufzeit werden sie einmal je Sprache übersetzt und gecacht (FillerPhrases, Übersetzer = OpenRouter-Modell, durchgängig höfliche Anrede „Sie"/„vous"/„usted"…). Die Standardsprache wird beim Start vorgewärmt (warmup.py) → erster Such-Turn ohne Übersetzungs-Latenz; andere Sprachen werden beim ersten Bedarf einmalig übersetzt. Fällt die Übersetzung aus → Fallback Englisch.

Geduld-Schleife: Beim Tool-Start läuft ein Hintergrund-Task, der alle FILLER_PATIENCE_INTERVAL Sekunden einen zufälligen PATIENCE-Satz nachschiebt; er wird beim ersten Antwort-Delta (Antwort beginnt) und im finally abgeräumt.

Ephemeralität (Invariante): Filler laufen über den Callback, nicht über den Delta-Stream — sie landen nicht in trace.semantic_response und nicht im gespeicherten History-Turn. Server-TTS ist verdrahtet; „Im Gerät" ist Fast-follow (eigener Event-Typ → Browser-speak(), Gesten-/Voices-Absicherung).

5.5 Konfiguration & Opt-out

web_search_enabled: bool = Trueglobal an per Default, pro Nutzer/Profil abschaltbar (Opt-out greift über die bestehende Präzedenz Defaults < Profil < Nutzer-Prefs). Der Schalter ist die nutzerfreundliche Admin-/Profil-Option; intern wählt build_orchestrator daraufhin den tool-fähigen vs. den plain Provider — beide mit demselben konfigurierten Modell (openrouter_llm_model), damit der Modellwechsel an einer Stelle bleibt.

5.6 Resilienz & Metriken

Fallback: Sonar-Ausfall → honest-punt im Loop; totaler Modellausfall → bestehende llm_fallback-Kette. Metriken (app/metrics.py): Tool-Calls und Sonar-Calls separat zählen — Kostensicht und Live-Beobachtung der Trigger-Rate. Kein eigenes Sonar-Kontingent in v1 (erst Metrik-Sicht; quota.py-Anbindung später bei Bedarf).

6. Die zwei harten Stellen (bewusst benannt)

  • Streaming + Tool-Erkennung. Beim gestreamten ersten Call kommen tool_call-Deltas fragmentiert. Der Wrapper setzt sie zusammen und unterscheidet: Text-Deltas → durchreichen (kein Regress im Normalfall); materialisiert sich ein Tool-Call → Stream verwerfen, Filler, Sonar, zweite Runde streamen. mistral emittiert entweder Tool-Call oder Text — das macht es handhabbar; das Delta-Zusammensetzen ist die eigentliche Arbeit.
  • Filler-Ephemeralität. Spielt in Bubble und TTS, darf aber nie in semantic_response/Store/history. Der Callback-Weg löst das by-design — beim Implementieren strikt einhalten.

7. Warum nicht Weg 1 (separater Klassifikator)

Der Eval zeigte: das Modell self-triggert auf den gefährlichen, konfident- veraltbaren Kategorien zu 100 %; die Recall-Lücke (→93 %) liegt nur in low-/medium-harm-Slices (Logistik honest-punt, eine nl-Koreferenz-Kante). Tool-Calling spart den Vorab-Roundtrip und ist zugleich die Basis für den späteren Multitool-Ausbau (weitere Werkzeuge in dieselbe Schleife, Vorrang-Regel wird zur Tool-Rangordnung).

8. Phasenplan

  • v1: SonarTool + ToolCallingLLM (Loop, Persona+Vorrang-Prompt, eigener Tool-Client) + Koreferenz-Vorstufe + Registry-Eintrag + web_search_enabled (global an, Opt-out) + Filler für Server-TTS + Tool/Sonar-Metriken. schedule_hours-honest-punt akzeptiert.
  • Fast-follow: Filler für Gerät-TTS (Event-Protokoll) → Citations in die UI-Bubble → ggf. deterministischer Logistik-Nudge.
  • Später (Multitool): weitere Tools in dieselbe ToolCallingLLM-Schleife (Kalender, Erinnerungen, Medizin-Safety) mit Tool-Rangordnung.

9. Berührte Dateien (Implementierungs-Landkarte)

  • neu: app/providers/llm/tool_calling.py (ToolCallingLLM), app/tools/web_search.py (SonarTool), app/pipeline/decontextualizer.py (Koreferenz-Vorstufe)
  • app/dependencies.pyLLM_REGISTRY-Eintrag; build_orchestrator (tool vs. plain je web_search_enabled)
  • app/core/orchestrator.pyon_tool_start-Callback in chat_stream, Filler-Emission (ephemer)
  • app/config.py / app/runtime_config.pyweb_search_enabled (Default true, RUNTIME_SETTABLE + Nutzer-Pref-Opt-out)
  • app/metrics.py — Zähler tool_calls_total, sonar_calls_total
  • Filler-Texte je Sprache (Pool)