# 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 (~1–3 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`/`tts` → `on_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 = True` — **global 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.py` — `LLM_REGISTRY`-Eintrag; `build_orchestrator` (tool vs. plain je `web_search_enabled`) - `app/core/orchestrator.py` — `on_tool_start`-Callback in `chat_stream`, Filler-Emission (ephemer) - `app/config.py` / `app/runtime_config.py` — `web_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)