feat(llm): Web-Suche per Tool-Calling (Weg 2) — Sonar + ToolCallingLLM
Frische-/Web-Such-Funktion: Das zentrale Modell entscheidet selbst via web_search-Tool, ob es tagesaktuelle Fakten braucht, holt sie über perplexity/sonar und formuliert die Antwort in Persona (Augment). - SonarTool (app/tools/web_search.py): Fakten via perplexity/sonar, Citations als Metadaten, honest-punt-Sentinel bei Fehler/Timeout. - ToolCallingLLM (app/providers/llm/tool_calling.py): agentischer Loop als LLMProvider; complete() + gestreamtes stream() mit SSE-Tool-Assembler; Persona- + Trigger- + Vorrang-Prompt (Tool-Ergebnis schlaegt Gedaechtnis). - Verdrahtung: Registry-Eintrag openrouter-tools; web_search_enabled (global an, pro Nutzer/Profil abschaltbar) via Route-Layering; build_orchestrator waehlt tool-faehig vs. plain, Fallback-Kette erhalten. - Filler: ephemerer Beruhigungssatz beim Tool-Start (sofort angezeigt UND gesprochen als Satz null), nie in semantic_response/History; on_tool_start defensiv durch die stream()-Kette gefaedelt (kein Bruch bestehender Provider). - Modellwechsel: Standard auf mistralai/mistral-small-3.2-24b-instruct (tool-faehig; im Eval einziger Recall-Gate-Passer). 2501 ist tool-unfaehig. - Eval-Harness (eval/tool_calling/): Datensatz + Runner zur Modellauswahl. Doc: Docs/weg2-tool-calling.md. Tests: 290 gruen. Offen (Schritt 5): Koreferenz-Vorstufe (nl-Pronomen) + Metrik-Zaehler. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
19b3998948
commit
703a695bed
19 changed files with 1470 additions and 9 deletions
|
|
@ -17,6 +17,8 @@ from app.providers.stt.openrouter import OpenRouterSTTProvider
|
|||
from app.providers.stt.faster_whisper import FasterWhisperProvider
|
||||
from app.providers.llm.local_openai_compatible import LocalOpenAICompatibleLLM
|
||||
from app.providers.llm.openrouter import OpenRouterLLMProvider
|
||||
from app.providers.llm.tool_calling import ToolCallingLLM
|
||||
from app.tools.web_search import SonarTool
|
||||
from app.providers.tts.openrouter import OpenRouterTTSProvider
|
||||
from app.providers.tts.cartesia import CartesiaTTSProvider
|
||||
from app.providers.tts.chatterbox import ChatterboxTTSProvider
|
||||
|
|
@ -56,6 +58,12 @@ STT_REGISTRY = {
|
|||
|
||||
LLM_REGISTRY = {
|
||||
"openrouter": lambda s: OpenRouterLLMProvider(s.openrouter_api_key, s.openrouter_llm_model),
|
||||
# Tool-fähige Variante desselben OpenRouter-Modells (Weg 2: web_search via Sonar).
|
||||
"openrouter-tools": lambda s: ToolCallingLLM(
|
||||
s.openrouter_api_key,
|
||||
s.openrouter_llm_model,
|
||||
tools=[SonarTool(s.openrouter_api_key)],
|
||||
),
|
||||
"local-openai-compatible": lambda s: LocalOpenAICompatibleLLM(
|
||||
s.local_llm_base_url,
|
||||
s.local_llm_api_key,
|
||||
|
|
@ -156,8 +164,20 @@ ROUTE_KEYS = (
|
|||
"tts_provider",
|
||||
"language",
|
||||
"voice_gender",
|
||||
"web_search_enabled",
|
||||
)
|
||||
|
||||
|
||||
def _as_bool(value, default: bool = True) -> bool:
|
||||
"""Robuste Bool-Auflösung (Prefs/Overrides können Strings sein)."""
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
return value.strip().lower() in ("1", "true", "yes", "on")
|
||||
if value is None:
|
||||
return default
|
||||
return bool(value)
|
||||
|
||||
# Stimm-Auswahl nach Sprache: (effektive) Sprache → beste Piper-Stimme (gender-agnostisch).
|
||||
LANG_TO_PIPER_VOICE: dict[str, str] = {
|
||||
"de": "de_DE-thorsten-high",
|
||||
|
|
@ -236,6 +256,7 @@ class ResolvedRoute:
|
|||
tts_provider: str
|
||||
language: str
|
||||
voice_gender: str = "any"
|
||||
web_search_enabled: bool = True
|
||||
|
||||
def as_dict(self) -> dict:
|
||||
return {
|
||||
|
|
@ -246,6 +267,7 @@ class ResolvedRoute:
|
|||
"tts_provider": self.tts_provider,
|
||||
"language": self.language,
|
||||
"voice_gender": self.voice_gender,
|
||||
"web_search_enabled": self.web_search_enabled,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -286,6 +308,7 @@ def resolve_route(
|
|||
"llm_provider": cfg.default_llm_provider,
|
||||
"tts_provider": cfg.default_tts_provider,
|
||||
"language": cfg.default_language,
|
||||
"web_search_enabled": cfg.web_search_enabled,
|
||||
}
|
||||
|
||||
user_prefs = user.prefs if user is not None else {}
|
||||
|
|
@ -298,6 +321,8 @@ def resolve_route(
|
|||
if value is not None:
|
||||
resolved[key] = value
|
||||
|
||||
# web_search_enabled kann als String aus Prefs/Overrides kommen -> robust nach bool.
|
||||
resolved["web_search_enabled"] = _as_bool(resolved["web_search_enabled"], cfg.web_search_enabled)
|
||||
route = ResolvedRoute(**resolved)
|
||||
# Admin-Vorgabe „erlaubte Sprachen pro Nutzer": auf eine erlaubte Sprache klemmen.
|
||||
allowed = [s.strip() for s in str(user_prefs.get("allowed_languages") or "").split(",") if s.strip()]
|
||||
|
|
@ -337,9 +362,15 @@ def _resolve_normalize_level(tts_provider: str, cfg: Settings) -> str:
|
|||
|
||||
def build_orchestrator(route: ResolvedRoute, cfg=None) -> Orchestrator:
|
||||
cfg = cfg or runtime_settings
|
||||
# web_search (Weg 2): tool-fähige Variante desselben OpenRouter-Modells wählen.
|
||||
# Nur wenn der aufgelöste LLM-Provider "openrouter" ist — lokale Modelle können
|
||||
# (über diesen Pfad) kein Tool-Calling. Die llm_fallback-Kette bleibt erhalten.
|
||||
llm_name = route.llm_provider
|
||||
if route.web_search_enabled and llm_name == "openrouter":
|
||||
llm_name = "openrouter-tools"
|
||||
return Orchestrator(
|
||||
stt=_provider_chain(STT_REGISTRY, route.stt_provider, cfg.stt_fallback, "stt", cfg),
|
||||
llm=_provider_chain(LLM_REGISTRY, route.llm_provider, cfg.llm_fallback, "llm", cfg),
|
||||
llm=_provider_chain(LLM_REGISTRY, llm_name, cfg.llm_fallback, "llm", cfg),
|
||||
tts=_provider_chain(TTS_REGISTRY, route.tts_provider, cfg.tts_fallback, "tts", cfg),
|
||||
input_cleaner=InputCleaner(),
|
||||
spoken_adapter=SpokenResponseAdapter(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue