diff --git a/aria-brain/agent.py b/aria-brain/agent.py index 59c6273..17d339d 100644 --- a/aria-brain/agent.py +++ b/aria-brain/agent.py @@ -1131,6 +1131,39 @@ def _looks_like_skill_action(text: str) -> bool: return False +def _claims_live_media_state(text: str) -> bool: + """Erkennt eine BEHAUPTETE Live-Zustands-Auskunft, die NUR ein Werkzeug + wissen kann: aktueller Song/Interpret, Restzeit, was gerade laeuft, welches + Geraet spielt. local erfindet das gern (mal 'Midnight City von M83', mal + 'The House Of House') ohne run_spotify zu rufen. Ohne echten Skill-Aufruf ist + das reine Halluzination → eskalieren (Claude ruft das Tool zuverlaessig). + + Bewusst auf Medien-/Wiedergabe-Zustand begrenzt, damit normale Musik- + Konversation ('ich mag M83') NICHT getroffen wird — es muss nach einer + konkreten Ist-Zustands-Auskunft klingen.""" + t = (text or "").strip().lower() + if not t: + return False + # Restzeit/Abspielposition ('noch 52 Sekunden', 'Restzeit 2:34'). + if "restzeit" in t or "restlaufzeit" in t: + return True + if re.search(r"\bnoch\s+\d+\s*(sekunde|minute|sek|min)", t): + return True + # 'aktueller Song/Titel/Lied heisst/ist …' / 'es laeuft gerade …' / + # 'spielt gerade …' / 'jetzt laeuft …' — Ist-Zustands-Auskunft. + if re.search(r"(aktuell\w*\s+(song|titel|lied|stueck|track))", t): + return True + if re.search(r"(l(ae|ä)uft|spielt)\s+(gerade|jetzt|aktuell|zurzeit|grade)", t): + return True + if re.search(r"(gerade|jetzt|zurzeit|grade)\s+(l(ae|ä)uft|spielt)", t): + return True + # Skip-Quittung, die einen konkreten Folgetitel behauptet ('… ist jetzt „X"'). + if ("ueberspring" in t or "übersprung" in t or "uebersprung" in t) and ( + "song" in t or "titel" in t or "lied" in t): + return True + return False + + def _normalize_for_fast_match(text: str) -> str: norm = _strip_leading_hint_blocks(text).lower() norm = _fold_umlauts(norm) @@ -1486,8 +1519,9 @@ class Agent: # ('Spotify: …', 'Playlist … abspielen'), OHNE run_spotify gerufen zu # haben → es ist real nichts passiert. Dann eskalieren: Claude ruft das # Tool zuverlaessig. (Echte Skill-Ausfuehrung → _local_ran_skill=True.) - if not self._local_ran_skill and _looks_like_skill_action(final): - logger.info("[router] lokal: Skill-Aktion behauptet ohne Tool-Call → Claude") + if not self._local_ran_skill and ( + _looks_like_skill_action(final) or _claims_live_media_state(final)): + logger.info("[router] lokal: Aktion/Live-Zustand behauptet ohne Tool-Call → Claude") return None logger.info("[router] lokal beantwortet (%d Zeichen)", len(final)) diff --git a/aria-brain/router.py b/aria-brain/router.py index d7a3046..f626ffd 100644 --- a/aria-brain/router.py +++ b/aria-brain/router.py @@ -86,6 +86,18 @@ _HARD_HINTS = re.compile( _MAX_LEN_FOR_LOCAL = 220 # laengere Nachrichten = eher komplexe Aufgaben → Claude +# Expliziter Nutzer-Wunsch „nimm das grosse Modell". Stefan sagt oft „frag Clodi" +# / „benutze direkt Claude" — dann soll der Turn NICHT lokal versucht werden, +# sondern direkt an Claude gehen. „Clodi/Clody" ist sein Kosename fuer Claude und +# meint praktisch immer Routing; „claude"/„grosses modell" nur in Imperativ-Kontext +# (nimm/nutze/frag/…), damit reine Trivia „was ist Claude" nicht faelschlich matcht. +_FORCE_CLAUDE = re.compile( + r"\b(clodi|clody)\b" + r"|\b(nimm|nutze|benutze|verwende|frag(e|st)?|nimms?t?|mit|via|direkt|per)\b" + r"[^.?!]*\b(claude|gro(ss|ß)es?\s+modell)\b", + re.IGNORECASE, +) + def _strip_leading_hint_blocks(text: str) -> str: """Fuehrende `[ ... ]`-Hint-Bloecke (GPS, Barge-In von der Bridge) weg — @@ -108,6 +120,10 @@ def should_try_local(user_message: str, cfg: dict) -> bool: msg = _strip_leading_hint_blocks(user_message) if not msg or len(msg) > _MAX_LEN_FOR_LOCAL: return False + # Expliziter „nimm Claude/Clodi"-Wunsch → nie lokal (User hat entschieden). + if _FORCE_CLAUDE.search(msg): + logger.info("[router] expliziter Claude-Wunsch erkannt → nicht lokal") + return False if _TOOL_HINTS.search(msg): return False if _HARD_HINTS.search(msg): @@ -166,6 +182,13 @@ def build_local_system_prompt(identity_anchor: str, has_tools: bool = False, "'Spotify: …' / 'Playlist abspielen'-Quittung o.ae. ohne echten " "Skill-Aufruf. Kannst/willst du es nicht per Werkzeug tun, sag es " "ehrlich oder eskaliere — erfinde keine Bestaetigung.", + "Das gilt GENAUSO fuer Live-Auskuenfte: Nenne NIEMALS einen aktuellen " + "Songtitel, Interpreten, die Restzeit oder was gerade laeuft/welches " + "Geraet spielt, ohne den passenden Skill (z.B. run_spotify) WIRKLICH " + "aufgerufen und sein Ergebnis gelesen zu haben. Kein Tool-Ergebnis = du " + "weisst es NICHT — dann eskaliere, statt einen Titel/eine Zeit zu raten. " + "Gib nur weiter, was im Skill-Ergebnis wirklich steht; hat der Skill " + "keinen Titel geliefert (z.B. nur 'OK: next'), erfinde auch keinen.", ] parts += [ "",