feat(voice): ARIA erkennt Gespraechs-Phase selbst (Befehl/Frage, Kette/Ende)
Voice-First: ARIA deklariert die Phase aus dem Text per Inline-Marker — dasselbe Muster wie [[AWAIT]], die Marker werden vor Anzeige/TTS/History entfernt. Loest: - Claude-Pfad las Steuerbefehle vor, obwohl das Skill lief (Manifest-speak-Flag kennt den Unterschied Befehl/Frage nicht — dasselbe Skill ist mal Aktion, mal Auskunft). Jetzt entscheidet ARIA aus dem Kontext. - Befehlsketten (VNC oeffnen, Menue klicken, ...): Mikro soll offen bleiben und stumm gearbeitet werden, bis Stefan die Kette beendet. Marker (ARIA haengt sie ans Antwort-Ende): [[STUMM]] -> Steuerbefehl, nicht vorlesen. Allein = Einzelbefehl -> danach Stop. [[WEITER]] -> Konversation/Kette laeuft weiter -> Mikro offen halten. [[ENDE]] -> Konversation/Kette beenden -> zurueck aufs Wake-Word. Brain: - _extract_flow_markers() zieht [[STUMM]]/[[WEITER]]/[[ENDE]] aus dem Claude-Reply und ueberschreibt speak/converse (Marker ist autoritativ ueber Skill-Flag). [[STUMM]] allein impliziert converse=false; [[ENDE]] schlaegt [[WEITER]]. - prompts.py: neue Sektion build_voice_flow_section() bringt ARIA Marker + Befehl/Frage-Klassifikation + Ketten-/Ende-Erkennung bei. App (ChatScreen): stiller Antwort-Zweig verzweigt jetzt auf converse — converse=true (Kette) -> endConversation(false), Mikro bleibt offen fuer den naechsten Kettenbefehl; converse=false -> ariaStopRecording (Stop wie gehabt). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+60
-5
@@ -1347,6 +1347,53 @@ def _extract_await_marker(text: str) -> tuple:
|
||||
return text, False
|
||||
|
||||
|
||||
# ── Sprach-/Gespraechs-Steuermarker (ARIA deklariert die Phase SELBST) ──
|
||||
#
|
||||
# Voice-First: ARIA erkennt aus dem Text, ob Stefan einen BEFEHL gibt (etwas tun)
|
||||
# oder eine FRAGE stellt (etwas wissen), und ob das Gespraech/eine Befehlskette
|
||||
# weiterlaeuft oder endet. Sie haengt dazu Marker ans Ende ihrer Antwort — genau
|
||||
# wie [[AWAIT]], und sie werden ebenso entfernt (nicht angezeigt/vorgelesen/in
|
||||
# History). Der Marker ist AUTORITATIV — er ueberschreibt das Skill-Manifest-Flag,
|
||||
# denn dasselbe Skill (z.B. VM-/GUI-Steuerung) ist mal Befehl, mal Auskunft; nur
|
||||
# ARIA weiss aus dem Kontext, was gerade gemeint ist.
|
||||
#
|
||||
# [[STUMM]] -> reiner Steuerbefehl: NICHT vorlesen (speak=false). Allein =
|
||||
# Einzelbefehl → danach zurueck aufs Wake-Word (converse=false).
|
||||
# [[WEITER]] -> Konversation/Befehlskette laeuft weiter: Mikro offen halten
|
||||
# (converse=true) — kein erneutes "Computer" noetig.
|
||||
# [[ENDE]] -> Konversation/Kette beenden: zurueck aufs Wake-Word (converse=false).
|
||||
_SILENT_MARKER_RE = re.compile(r"\[\[\s*STUMM\s*\]\]", re.IGNORECASE)
|
||||
_CONT_MARKER_RE = re.compile(r"\[\[\s*WEITER\s*\]\]", re.IGNORECASE)
|
||||
_END_MARKER_RE = re.compile(r"\[\[\s*ENDE\s*\]\]", re.IGNORECASE)
|
||||
|
||||
|
||||
def _extract_flow_markers(text: str) -> tuple:
|
||||
"""Zieht [[STUMM]]/[[WEITER]]/[[ENDE]] aus dem finalen Text.
|
||||
Gibt (clean_text, speak_override, converse_override) zurueck; ein Override ist
|
||||
None, wenn der jeweilige Marker fehlt (dann gilt Default/Skill-Flag).
|
||||
Regeln: [[STUMM]] alleine = Einzelbefehl → auch converse=false (Mikro zu),
|
||||
ausser [[WEITER]] haelt es explizit offen. [[ENDE]] gewinnt gegen [[WEITER]]."""
|
||||
if not text:
|
||||
return text, None, None
|
||||
speak_ov = None
|
||||
conv_ov = None
|
||||
if _SILENT_MARKER_RE.search(text):
|
||||
speak_ov = False
|
||||
text = _SILENT_MARKER_RE.sub("", text)
|
||||
if _END_MARKER_RE.search(text):
|
||||
conv_ov = False
|
||||
text = _END_MARKER_RE.sub("", text)
|
||||
if _CONT_MARKER_RE.search(text):
|
||||
# [[ENDE]] hat Vorrang — widerspruechliche Marker → beenden.
|
||||
if conv_ov is None:
|
||||
conv_ov = True
|
||||
text = _CONT_MARKER_RE.sub("", text)
|
||||
# Stiller Einzelbefehl ohne explizites Weiterlauschen → Mikro zu.
|
||||
if speak_ov is False and conv_ov is None:
|
||||
conv_ov = False
|
||||
return text.strip(), speak_ov, conv_ov
|
||||
|
||||
|
||||
def _normalize_for_fast_match(text: str) -> str:
|
||||
norm = _strip_leading_hint_blocks(text).lower()
|
||||
norm = _fold_umlauts(norm)
|
||||
@@ -2033,16 +2080,24 @@ class Agent:
|
||||
# Rueckfrage-Marker aus dem finalen Text ziehen (vor History/Return, damit
|
||||
# er nicht angezeigt/vorgelesen wird und nicht die Conversation vergiftet).
|
||||
final_reply, awaiting_reply = _extract_await_marker(final_reply)
|
||||
# ARIAs Phasen-Marker ([[STUMM]]/[[WEITER]]/[[ENDE]]) ziehen — VOR History,
|
||||
# damit sie nicht angezeigt/vorgelesen/gespeichert werden.
|
||||
final_reply, _speak_ov, _conv_ov = _extract_flow_markers(final_reply)
|
||||
|
||||
# 7. Assistant-Turn (final reply) in die Conversation
|
||||
self.conversation.add("assistant", final_reply,
|
||||
project_id=active_project_id)
|
||||
# speak/converse folgen dem ausgefuehrten Skill (sonst Default: Gespraech);
|
||||
# speak/converse folgen dem ausgefuehrten Skill (sonst Default: Gespraech).
|
||||
# ARIAs Phasen-Marker sind AUTORITATIV: sie kennt aus dem Text den Unter-
|
||||
# schied Befehl/Frage und Kette/Ende, den das Skill-Manifest nicht kennt.
|
||||
speak = bool(getattr(self, "_claude_turn_speak", True))
|
||||
converse = bool(getattr(self, "_claude_turn_converse", True))
|
||||
if _speak_ov is not None:
|
||||
speak = _speak_ov
|
||||
if _conv_ov is not None:
|
||||
converse = _conv_ov
|
||||
# awaiting_reply = ARIA stellt eine blockierende Rueckfrage (Queue pausiert).
|
||||
return (final_reply, "claude",
|
||||
bool(getattr(self, "_claude_turn_speak", True)),
|
||||
bool(getattr(self, "_claude_turn_converse", True)),
|
||||
awaiting_reply)
|
||||
return (final_reply, "claude", speak, converse, awaiting_reply)
|
||||
|
||||
# ── Tool-Dispatcher ───────────────────────────────────────
|
||||
|
||||
|
||||
+42
-1
@@ -162,6 +162,46 @@ def build_time_section() -> str:
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
def build_voice_flow_section() -> str:
|
||||
"""Sprach-/Gespraechssteuerung: ARIA erkennt AUS DEM TEXT die Phase (Befehl vs.
|
||||
Frage, Kette vs. Ende) und deklariert sie per Marker — wie [[AWAIT]]. Die
|
||||
Marker werden im Brain entfernt (nie angezeigt/vorgelesen)."""
|
||||
return "\n".join([
|
||||
"## Sprach- & Gespraechssteuerung (Voice-First — du entscheidest die Phase)",
|
||||
"Stefan spricht meist mit dir. DU erkennst aus dem Text, was gerade Phase "
|
||||
"ist — niemand raet das fuer dich. Dazu haengst du EINEN Marker (bei Bedarf "
|
||||
"zwei) ganz ans ENDE deiner Antwort. Sie werden entfernt: nicht angezeigt, "
|
||||
"nicht vorgelesen, nicht gespeichert — genau wie `[[AWAIT]]`.",
|
||||
"",
|
||||
"- `[[STUMM]]` → Deine Antwort ist ein reiner **Steuerbefehl** (du hast etwas "
|
||||
"GETAN: Musik, VNC oeffnen, einen Menuepunkt klicken, Licht …). Sie wird "
|
||||
"NICHT vorgelesen; der kurze Bestaetigungstext steht nur in der Bubble. "
|
||||
"Setz das IMMER, wenn Stefan dir einen Befehl gibt statt eine Frage stellt — "
|
||||
"AUCH wenn du den Befehl ueber ein Skill/Tool ausfuehrst (nicht nur beim "
|
||||
"Fast-Path). `[[STUMM]]` ALLEIN = Einzelbefehl → danach direkt zurueck aufs "
|
||||
"Wake-Word.",
|
||||
"- `[[WEITER]]` → Das Gespraech bzw. eine **Befehlskette** laeuft weiter: das "
|
||||
"Mikro bleibt offen, du wartest auf die naechste Eingabe (kein erneutes "
|
||||
"\"Computer\" noetig). Setz das, wenn Stefan eine Kette ankuendigt ('ich geb "
|
||||
"dir gleich mehrere Befehle', 'wir machen das jetzt Schritt fuer Schritt') "
|
||||
"oder das Gespraech klar weitergeht.",
|
||||
"- `[[ENDE]]` → Konversation/Kette ist zu Ende: zurueck aufs Wake-Word. Setz "
|
||||
"das, wenn Stefan schliesst ('das war's', 'Konversation Ende', 'Befehlskette "
|
||||
"Ende', 'danke, fertig'). Stellt er in DERSELBEN Nachricht noch eine Frage, "
|
||||
"beantworte sie normal (OHNE `[[STUMM]]`, wird also vorgelesen) UND haeng "
|
||||
"`[[ENDE]]` an.",
|
||||
"",
|
||||
"Regeln:",
|
||||
"- Befehl (etwas TUN) → `[[STUMM]]`. Frage (etwas WISSEN / plaudern) → normal, "
|
||||
"ohne Marker (wird vorgelesen).",
|
||||
"- Befehlskette: JEDER Schritt `[[STUMM]] [[WEITER]]` (stumm arbeiten, Mikro "
|
||||
"offen), bis Stefan die Kette beendet → letzter Turn `[[ENDE]]`.",
|
||||
"- Ohne Marker = normales Gespraech: du wirst vorgelesen und ich lausche "
|
||||
"danach kurz weiter (Stefan kann einfach antworten, ohne 'Computer').",
|
||||
"- Nie widerspruechlich: `[[ENDE]]` schlaegt `[[WEITER]]`.",
|
||||
])
|
||||
|
||||
|
||||
TYPE_HEADINGS = {
|
||||
"identity": "## Wer du bist",
|
||||
"rule": "## Sicherheitsregeln & Prinzipien",
|
||||
@@ -463,7 +503,8 @@ def build_system_prompt(
|
||||
"""Kompletter System-Prompt: Hot + Cold + Skills + Triggers + FLUX + OAuth."""
|
||||
# Identitaets-Anker IMMER zuerst — vor allen Memories/Sektionen, damit die
|
||||
# ARIA-Rolle auch in Projekten mit injection-artigem Inhalt (Pentest) haelt.
|
||||
parts = [IDENTITY_ANCHOR, "", build_hot_memory_section(pinned), "", build_time_section()]
|
||||
parts = [IDENTITY_ANCHOR, "", build_hot_memory_section(pinned), "", build_time_section(),
|
||||
"", build_voice_flow_section()]
|
||||
if skills:
|
||||
parts.append("")
|
||||
parts.append(build_skills_section(skills))
|
||||
|
||||
Reference in New Issue
Block a user