feat(voice): "Konversation Ende" deterministisch — nicht nur ARIAs [[ENDE]]-Marker

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 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 14:25:09 +02:00
co-authored by Claude Opus 4.8
parent 9e78d75149
commit 9bdfb7193e
+37
View File
@@ -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)