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)