ARIA-AGENT/bridge/modes.py

173 lines
4.5 KiB
Python

"""
ARIA Betriebsmodi — steuern wann und wie ARIA spricht.
Jeder Modus definiert, ob ARIA Audio ausgeben darf,
ob sie proaktiv sprechen darf und ob Unterbrechungen erlaubt sind.
"""
from __future__ import annotations
import logging
from dataclasses import dataclass
from enum import Enum
from typing import Optional
logger = logging.getLogger(__name__)
@dataclass(frozen=True)
class ModeConfig:
"""Konfiguration eines Betriebsmodus."""
name: str
emoji: str
activation_phrase: str
can_speak: bool
can_interrupt: bool
audio_output: bool
proactive: bool
class Mode(Enum):
"""ARIAs Betriebsmodi mit zugehoeriger Konfiguration."""
NORMAL = ModeConfig(
name="Normal",
emoji="\U0001f7e2", # Gruener Kreis
activation_phrase="ARIA, Normal-Modus",
can_speak=True,
can_interrupt=True,
audio_output=True,
proactive=True,
)
DND = ModeConfig(
name="Nicht stoeren",
emoji="\U0001f534", # Roter Kreis
activation_phrase="ARIA, nicht stoeren",
can_speak=False,
can_interrupt=False,
audio_output=False,
proactive=False,
)
WHISPER = ModeConfig(
name="Fluester",
emoji="\U0001f7e1", # Gelber Kreis
activation_phrase="ARIA, leise bitte",
can_speak=False,
can_interrupt=False,
audio_output=False,
proactive=True,
)
HANGAR = ModeConfig(
name="Hangar",
emoji="\u2708\ufe0f", # Flugzeug
activation_phrase="ARIA, ich arbeite",
can_speak=True,
can_interrupt=False,
audio_output=True,
proactive=False,
)
GAMING = ModeConfig(
name="Gaming",
emoji="\U0001f3ae", # Gamepad
activation_phrase="ARIA, Gaming-Modus",
can_speak=True,
can_interrupt=False,
audio_output=True,
proactive=False,
)
@property
def config(self) -> ModeConfig:
return self.value
# Aktivierungsphrasen auf Modi mappen (lowercase fuer Vergleich)
_ACTIVATION_MAP: dict[str, Mode] = {
mode.config.activation_phrase.lower(): mode for mode in Mode
}
# ID-Mapping fuer API-Mode-Wechsel (z.B. App ModeSelector schickt 'normal')
_ID_MAP: dict[str, Mode] = {
"normal": Mode.NORMAL,
"nicht_stoeren": Mode.DND,
"dnd": Mode.DND,
"fluester": Mode.WHISPER,
"whisper": Mode.WHISPER,
"hangar": Mode.HANGAR,
"gaming": Mode.GAMING,
}
def mode_from_id(mode_id: str) -> Optional[Mode]:
"""ID-basiertes Mapping fuer API-Mode-Wechsel (ohne Aktivierungsphrase)."""
if not mode_id:
return None
return _ID_MAP.get(mode_id.strip().lower())
# Kanonische IDs fuer Broadcasts (matchen die App-UI-IDs in ModeSelector)
_CANONICAL_ID: dict[Mode, str] = {
Mode.NORMAL: "normal",
Mode.DND: "nicht_stoeren",
Mode.WHISPER: "fluester",
Mode.HANGAR: "hangar",
Mode.GAMING: "gaming",
}
def canonical_id(mode: Mode) -> str:
"""Kanonische ID die App + Diagnostic + Bridge gleichermassen kennen."""
return _CANONICAL_ID.get(mode, mode.name.lower())
def detect_mode_switch(text: str) -> Optional[Mode]:
"""Erkennt ob ein Text eine Modus-Umschaltung enthaelt.
Vergleicht den Text (case-insensitive) mit den Aktivierungsphrasen
aller Modi. Gibt den neuen Modus zurueck oder None.
Args:
text: Eingabetext vom Benutzer.
Returns:
Der erkannte Modus oder None wenn kein Wechsel erkannt wurde.
"""
text_lower = text.strip().lower()
for phrase, mode in _ACTIVATION_MAP.items():
if phrase in text_lower:
logger.info(
"Modus-Wechsel erkannt: %s %s",
mode.config.emoji,
mode.config.name,
)
return mode
return None
def should_speak(mode: Mode, is_critical: bool = False) -> bool:
"""Prueft ob eine Nachricht im aktuellen Modus gesprochen werden soll.
Im DND-Modus wird nur bei kritischen Alarmen gesprochen.
Alle anderen Modi respektieren ihre can_speak / audio_output Flags.
Args:
mode: Aktueller Betriebsmodus.
is_critical: True wenn es sich um einen kritischen Alarm handelt.
Returns:
True wenn die Nachricht gesprochen werden soll.
"""
# Kritische Alarme durchbrechen den DND-Modus
if is_critical and mode == Mode.DND:
logger.warning("Kritischer Alarm im DND-Modus — Sprachausgabe erzwungen")
return True
return mode.config.can_speak and mode.config.audio_output