From 9bdfb7193ea67f81bac94cacd3f9cb87bbc7c478 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 15 Aug 2026 14:25:09 +0200 Subject: [PATCH] =?UTF-8?q?feat(voice):=20"Konversation=20Ende"=20determin?= =?UTF-8?q?istisch=20=E2=80=94=20nicht=20nur=20ARIAs=20[[ENDE]]-Marker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Beobachtung Stefan: ARIA haelt die Konversation offen (Default, korrekt), aber auf "Konversation Ende" hin schloss sie NICHT — sie hat den [[ENDE]]-Marker nicht gesetzt. Das aufs LLM allein zu verlassen ist zu unzuverlaessig fuer einen festen Trigger. Fix: _user_wants_conversation_end() erkennt explizite Beenden-Phrasen im User-Text (konversation/gespraech/befehlskette + ende/beenden/aus/stop/schluss, beide Reihenfolgen, ein Satzteil) und erzwingt converse=false an ALLEN drei Returns (fast-path/local/claude). ARIA beantwortet eine evtl. enthaltene Frage noch normal (speak bleibt), danach zurueck aufs Wake-Word. "befehls?kette" statt bare "kette", damit "Lieferkette" u.ae. nicht faelschlich matchen (per Test abgesichert). Deploy: Brain-Neustart (nicht waehrend ARIA arbeitet). Co-Authored-By: Claude Opus 4.8 --- aria-brain/agent.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/aria-brain/agent.py b/aria-brain/agent.py index 2193fe0..d4aac59 100644 --- a/aria-brain/agent.py +++ b/aria-brain/agent.py @@ -1394,6 +1394,30 @@ def _extract_flow_markers(text: str) -> tuple: return text.strip(), speak_ov, conv_ov +# Explizite "Konversation beenden"-Phrasen vom USER — deterministisch, NICHT auf +# ARIAs [[ENDE]]-Marker angewiesen. Stefan will "Konversation Ende" o.ae. als +# festen Trigger: danach zurueck aufs Wake-Word, egal was ARIA sonst tut. Eine in +# derselben Nachricht enthaltene Frage beantwortet sie normal (wird vorgelesen), +# aber converse wird auf false gezwungen. Nomen + Ende-Wort in EINEM Satzteil +# ([^.!?]{0,15}) in beliebiger Reihenfolge; "befehls?kette" damit "Lieferkette" +# o.ae. nicht faelschlich matcht. +_CONV_NOUN = r"(?:konversation|gespr[aä]ch|befehls?kette)" +_CONV_END_VERB = r"(?:ende|beenden|beende|aus|stop|stopp|schluss)" +_END_CONVERSATION_RE = re.compile( + rf"\b{_CONV_NOUN}\b[^.!?]{{0,15}}\b{_CONV_END_VERB}\b" + rf"|\b(?:beende|schlie(?:ß|ss)e?)\b[^.!?]{{0,15}}\b{_CONV_NOUN}\b", + re.IGNORECASE, +) + + +def _user_wants_conversation_end(text: str) -> bool: + """True, wenn der User in dieser Nachricht explizit die Konversation/Kette + beenden will (deterministisch, unabhaengig vom LLM-Marker).""" + if not text: + return False + return bool(_END_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) @@ -1802,6 +1826,11 @@ 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". + _wants_end = _user_wants_conversation_end(user_message) + # Events vom letzten Turn weglassen self._pending_events = [] @@ -1829,6 +1858,8 @@ class Agent: speak = bool(getattr(self, "_fast_path_speak", False)) # converse folgt dem Skill (Manifest/Output) — nicht mehr generell False. converse = bool(getattr(self, "_fast_path_converse", False)) + if _wants_end: + converse = False # Fast-Path = reiner Steuerbefehl, nie eine Rueckfrage → awaiting=False. return fast_reply, "fast-path", speak, converse, False @@ -1848,6 +1879,8 @@ class Agent: # dem Skill (bzw. Default: Info/Gespraech = vorlesen + 30s). speak = getattr(self, "_local_turn_speak", True) converse = getattr(self, "_local_turn_converse", True) + if _wants_end: + converse = False # Local ist tool-loses Reden; blockierende Rueckfragen macht Claude. return local_reply, "local", speak, converse, False @@ -2096,6 +2129,10 @@ 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. + if _wants_end: + converse = False # awaiting_reply = ARIA stellt eine blockierende Rueckfrage (Queue pausiert). return (final_reply, "claude", speak, converse, awaiting_reply)