my_voice_assistant_v3/app/safety/emergency.py

84 lines
3 KiB
Python
Raw Normal View History

"""Heuristische Notfall-Erkennung und Eskalation (Senioren-Kontext).
WICHTIG: Schluesselwort-Heuristik, KEIN Ersatz fuer eine echte Klassifikation.
Sie kann Notlagen verpassen oder Fehlalarme ausloesen. Die erkannten Textauszuege
sind hochsensibel und werden bewusst protokolliert (DSGVO beachten: Einwilligung,
Aufbewahrung, Zugriff).
"""
import asyncio
import httpx
from app.config import settings, Settings
from app.metrics import metrics
# Phrasen je Kategorie (de/en), bewusst eher spezifisch gegen Fehlalarme.
_PATTERNS: dict[str, list[str]] = {
"medical": [
"brustschmerz", "schmerzen in der brust", "kann nicht atmen", "keine luft",
"atemnot", "herzinfarkt", "schlaganfall", "bewusstlos", "gestuerzt", "gestürzt",
"gefallen und komme nicht hoch", "starke blutung",
"chest pain", "can't breathe", "cannot breathe", "heart attack", "stroke",
"i fell and can't", "bleeding badly",
],
"self_harm": [
"nicht mehr leben", "mich umbringen", "selbstmord", "suizid", "will sterben",
"kill myself", "end my life", "suicide", "want to die",
],
"help": [
"notruf", "notarzt", "krankenwagen", "ruf einen arzt", "es brennt",
"call an ambulance", "call 911", "call 112",
],
}
def detect(text: str):
"""Liefert (category, matched_phrase) oder None."""
if not text:
return None
low = text.lower()
for category, phrases in _PATTERNS.items():
for phrase in phrases:
if phrase in low:
return category, phrase
return None
async def _fire_webhook(url: str, user, category: str, snippet: str) -> None:
try:
async with httpx.AsyncClient(timeout=5) as client:
await client.post(
url,
json={
"user_id": user.id,
"display_name": user.display_name,
"category": category,
"text": snippet,
},
)
except Exception: # noqa: BLE001 - best effort, darf den Chat nicht brechen
metrics.inc("emergency_webhook_error_total")
def handle_emergency(user, text: str, store, cfg: Settings = settings):
"""Erkennt, protokolliert und eskaliert ein Notfall-Signal.
Gibt {"category", "matched"} zurueck, wenn etwas erkannt wurde, sonst None.
Der Webhook (falls konfiguriert) wird nicht-blockierend ausgeloest.
"""
match = detect(text)
if not match:
return None
category, phrase = match
snippet = text[:500]
store.log_emergency(user.id, category, snippet)
metrics.inc("emergency_total", {"category": category})
if cfg.emergency_webhook_url:
try:
asyncio.get_running_loop().create_task(
_fire_webhook(cfg.emergency_webhook_url, user, category, snippet)
)
except RuntimeError:
pass # kein laufender Event-Loop (z. B. im Test) -> Webhook ueberspringen
return {"category": category, "matched": phrase}