From 9d2c07d8d10977e6be5bc0a140f27ce851b98843 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sun, 16 Aug 2026 01:35:49 +0200 Subject: [PATCH] =?UTF-8?q?feat(voice):=20"Konversation=20fortf=C3=BChren"?= =?UTF-8?q?=20h=C3=A4lt=20offen=20=E2=80=94=20auch=20bei=20Fast-Path-Befeh?= =?UTF-8?q?l?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stefans Repro: "spiel Spotify auf Smartphone ab ABER Konversation fortführen" → die Kette endete trotzdem. Grund: "spiel spotify" trifft den Fast-Path (Regex), der den Satz-Rest nicht versteht → converse=false aus dem Skill-Manifest; ARIAs [[WEITER]]-Marker lebt in Claude, der wurde uebersprungen. Fix: _user_wants_conversation_continue() — Gegenstueck zu _user_wants_conversation_ end(). Erkennt "Konversation/Gespraech fortfuehren/weiterfuehren/offen halten/nicht beenden", "weiter reden/sprechen" etc. und erzwingt converse=true an ALLEN Returns (fast-path/local/claude), auch wenn das Manifest false sagt. [[ENDE]]/_wants_end hat Vorrang bei Widerspruch. Getestet inkl. Negativfaelle (sofort/spotify ab). Deploy: Brain-Neustart. Co-Authored-By: Claude Opus 4.8 --- aria-brain/agent.py | 43 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/aria-brain/agent.py b/aria-brain/agent.py index d4aac59..c72c5e7 100644 --- a/aria-brain/agent.py +++ b/aria-brain/agent.py @@ -1418,6 +1418,30 @@ def _user_wants_conversation_end(text: str) -> bool: return bool(_END_CONVERSATION_RE.search(_strip_leading_hint_blocks(text))) +# Gegenstueck zu _END: expliziter "Konversation OFFEN halten / fortfuehren"-Wunsch. +# Wichtig fuer BEFEHLE die den Fast-Path treffen: "spiel Spotify ab ABER Konversation +# fortfuehren" — der Fast-Path (Regex) versteht den Satz-Rest nicht und wuerde mit +# converse=false schliessen. Dieser Detektor erzwingt converse=true, auch am +# Fast-Path, egal was das Skill-Manifest sagt. Nomen+Verb in einem Satzteil, plus +# "weiter reden/sprechen" ohne Nomen. +_CONT_VERB = (r"(?:fortf[uü]hr\w*|fortsetz\w*|weiterf[uü]hr\w*|weiter\s*mach\w*|" + r"weiter\b|fort\b|offen\s+(?:halten|lassen)|nicht\s+beenden|weiterlauf\w*)") +_CONTINUE_CONVERSATION_RE = re.compile( + rf"\b{_CONV_NOUN}\b[^.!?]{{0,20}}\b{_CONT_VERB}" + rf"|\b{_CONT_VERB}[^.!?]{{0,20}}\b{_CONV_NOUN}\b" + rf"|\bweiter\s*(?:reden|sprechen|quatschen|plaudern|labern)\b", + re.IGNORECASE, +) + + +def _user_wants_conversation_continue(text: str) -> bool: + """True, wenn der User explizit weiter im Gespraech bleiben will (converse=true + erzwingen — auch bei einem Fast-Path-Befehl). [[ENDE]]/_wants_end hat Vorrang.""" + if not text: + return False + return bool(_CONTINUE_CONVERSATION_RE.search(_strip_leading_hint_blocks(text))) + + def _normalize_for_fast_match(text: str) -> str: norm = _strip_leading_hint_blocks(text).lower() norm = _fold_umlauts(norm) @@ -1826,10 +1850,13 @@ class Agent: if not user_message: raise ValueError("Leere Nachricht") - # Expliziter "Konversation/Kette beenden"-Wunsch (deterministisch). Wird an - # JEDEM Return auf converse=false angewendet — unabhaengig davon, ob ARIA - # den [[ENDE]]-Marker setzt. Stefans fester Trigger "Konversation Ende". + # Explizite Gespraechs-Steuerung vom USER (deterministisch, an JEDEM Return + # angewendet — auch am Fast-Path, den die LLM-Marker nicht erreichen): + # _wants_end → converse=false ("Konversation Ende") + # _wants_continue → converse=true ("... aber Konversation fortfuehren") + # End hat Vorrang bei Widerspruch. _wants_end = _user_wants_conversation_end(user_message) + _wants_continue = (not _wants_end) and _user_wants_conversation_continue(user_message) # Events vom letzten Turn weglassen self._pending_events = [] @@ -1860,6 +1887,8 @@ class Agent: converse = bool(getattr(self, "_fast_path_converse", False)) if _wants_end: converse = False + elif _wants_continue: + converse = True # Fast-Path = reiner Steuerbefehl, nie eine Rueckfrage → awaiting=False. return fast_reply, "fast-path", speak, converse, False @@ -1881,6 +1910,8 @@ class Agent: converse = getattr(self, "_local_turn_converse", True) if _wants_end: converse = False + elif _wants_continue: + converse = True # Local ist tool-loses Reden; blockierende Rueckfragen macht Claude. return local_reply, "local", speak, converse, False @@ -2129,10 +2160,12 @@ class Agent: speak = _speak_ov if _conv_ov is not None: converse = _conv_ov - # Expliziter User-Wunsch "Konversation beenden" gewinnt IMMER — Frage wird - # noch beantwortet (speak bleibt), aber danach zurueck aufs Wake-Word. + # Explizite User-Woerter gewinnen ueber Marker/Manifest: "Konversation + # beenden" → zu; "... fortfuehren" → offen halten. End hat Vorrang. if _wants_end: converse = False + elif _wants_continue: + converse = True # awaiting_reply = ARIA stellt eine blockierende Rueckfrage (Queue pausiert). return (final_reply, "claude", speak, converse, awaiting_reply)