feat(local-llm): B1b — lokale Tools (web_search via SearXNG, Timer, Spotify, Memory)

Das lokale Tier kann jetzt Werkzeuge nutzen — Wetter/News/Fakten laufen lokal
statt ueber Claudes langsamen Web-Fetch.

- SearXNG-Container (aria VM, aria-net): self-hosted Meta-Suche, keyless,
  JSON-API aktiviert (aria-data/searxng/settings.yml), Rate-Limiter aus.
  Brain-Env SEARXNG_URL.
- web_search-Tool in META_TOOLS + _dispatch_tool (_web_search fragt SearXNG,
  gibt Titel/Snippet/URL der Top-Treffer). Steht auch Claude zur Verfuegung.
- Lokaler Tool-Loop (_try_local_fast_lane): kuratiertes Set web_search /
  memory_search / trigger_timer / run_spotify; max 3 Runden, sonst → Claude.
  Break-/Escalate-Guard bleibt.
- Router: should_try_local schliesst nur noch CLAUDE-ONLY-Themen aus (Bild,
  Skill, Projekt, OAuth, Licht, Kalender/Mail). Wetter/Timer/News/Musik/Memory
  gehen jetzt lokal. Verifiziert (alle Testfaelle korrekt).
- Schlanker Local-Prompt mit Tool-Guidance ("nur nutzen wenn noetig, sonst
  Smalltalk direkt beantworten"). Skill-BAUEN bleibt Claude-only.

Deploy: ARIA-VM brain+bridge+searxng, Gamebox llm-adapter (tool-passthrough).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 12:50:54 +02:00
co-authored by Claude Opus 4.8
parent a58aa5594d
commit 3cd7350160
4 changed files with 223 additions and 54 deletions
+40 -22
View File
@@ -57,15 +57,17 @@ def load_config() -> dict:
# lokale Tier soll nur die klaren Plauder-Turns abgreifen; Fehlklassifikation
# faengt zusaetzlich das <<ESCALATE>> im Modell selbst ab.
# Wortstämme, die auf Tool-/Aktionsbedarf hindeuten → nicht lokal (in B1a).
# CLAUDE-ONLY-Themen → nicht lokal. Seit B1b hat das lokale Tier Werkzeuge
# (web_search, memory_search, trigger_timer, Spotify), daher gehen Wetter, News,
# Fakten, Timer, Musik, Gedaechtnis-Suche jetzt LOKAL. Nur was das lokale Tier
# nicht kann bleibt hier: Bilder, Skills, Projekte, OAuth, Smart-Home (keine
# Anbindung), Kalender/Mail (kein Tool).
_TOOL_HINTS = re.compile(
r"\b(wetter|temperatur|regne|timer|erinner|weck|licht|lampe|steckdose|"
r"spotify|musik|lied|song|playlist|lauter|leiser|"
r"bild|generier|male?\b|zeichne|foto|"
r"merk dir|memory|gedächtnis|erinnere dich|"
r"skill|trigger|projekt|oauth|spotify|kalender|termin|"
r"such(e|st)?\b|google|internet|news|nachrichten|aktie|kurs|"
r"maild?|email|nachricht schreiben|sende)\b",
r"\b(bild|generier|male?\b|malen|zeichne|foto|"
r"skill|projekt|oauth|"
r"licht|lampe|steckdose|rollade|heizung|"
r"kalender|termin|"
r"maild?|e-?mail|nachricht schreiben)\b",
re.IGNORECASE,
)
@@ -104,31 +106,47 @@ def should_try_local(user_message: str, cfg: dict) -> bool:
# (WAS ARIA kann, ohne volle Schemas) + Escalation-Regel. KEINE Tool-Schemas,
# kein volles Memory (B1a).
# Was das lokale Tier NICHT selbst kann → dafuer eskaliert es an Claude.
_AWARENESS = (
"ARIA kann ausserdem (ueber ihr grosses Backend, nicht in diesem schnellen "
"Modus): Wetter/Zeit/Websuche, Timer & Erinnerungen, Musik (Spotify) & Licht "
"steuern, Bilder generieren, ins Gedaechtnis schreiben/suchen, Skills bauen, "
"Projekte & OAuth verwalten."
"Nur das grosse Modell (nicht du im Schnell-Modus) kann: Bilder generieren, "
"Skills bauen/aendern, Projekte & OAuth verwalten, ins Gedaechtnis SCHREIBEN, "
"sowie tiefe/technische Analysen und langen Code."
)
def build_local_system_prompt(identity_anchor: str, pinned_persona: str = "") -> str:
def build_local_system_prompt(identity_anchor: str, has_tools: bool = False,
pinned_persona: str = "") -> str:
"""Schlanker System-Prompt fuers lokale LLM. identity_anchor = derselbe
Anker wie bei Claude (Rolle haelt); pinned_persona optional kurz."""
Anker wie bei Claude (Rolle haelt)."""
parts = [
identity_anchor.strip(),
"",
"## SCHNELL-MODUS",
"Du laeufst gerade als schnelles lokales Modell fuer einfache, kurze "
"Alltags-Konversation. Antworte knapp, freundlich, auf Deutsch, als ARIA.",
"Du laeufst gerade als schnelles lokales Modell fuer Alltags-Konversation "
"und einfache Aufgaben. Antworte knapp, freundlich, auf Deutsch, als ARIA.",
]
if has_tools:
parts += [
"",
"## DEINE WERKZEUGE — nur nutzen wenn die Frage es WIRKLICH braucht",
"- `web_search`: aktuelle Infos aus dem Netz — Wetter, News, Fakten, "
"Preise, Oeffnungszeiten. Bei Wetter: nimm Stefans Ort aus dem "
"GPS-Hinweis in der Nachricht.",
"- `memory_search`: in ARIAs Gedaechtnis nachsehen (lesen).",
"- `trigger_timer`: Timer/Erinnerung setzen ('in 10 Minuten…').",
"- `run_spotify`: Musik steuern (naechster Titel, Pause, weiter).",
"WICHTIG: Bei reinem Smalltalk ('wie gehts', Begruessung, Meinung) "
"KEIN Werkzeug — einfach direkt antworten. Werkzeuge nur bei echtem "
"Bedarf; erfinde keine.",
]
parts += [
"",
"## WAS DU HIER NICHT TUST",
"## WAS DU HIER NICHT KANNST",
_AWARENESS,
"Fuer alles, was ein Werkzeug/eine Aktion braucht, tiefer/technischer wird, "
"aktuelles Wissen oder Nachschlagen verlangt, oder wo du unsicher bist: "
f"antworte AUSSCHLIESSLICH mit exakt `{ESCALATE_MARKER}` (nichts sonst). "
"Dann uebernimmt automatisch das grosse Modell mit vollem Zugriff. "
"Lieber einmal eskalieren als falsch raten.",
"Dafuer — und bei tiefen/technischen Fragen, langem Code, oder wenn du "
f"unsicher bist — antworte AUSSCHLIESSLICH mit exakt `{ESCALATE_MARKER}` "
"(nichts sonst). Dann uebernimmt das grosse Modell mit vollem Zugriff. "
"Lieber einmal eskalieren als falsch raten oder ein Werkzeug erfinden.",
]
if pinned_persona.strip():
parts += ["", "## PERSONA", pinned_persona.strip()]