feat(1b): Automatische Notfallerkennung raus → manueller Notruf-Knopf

Die unzuverlässige/intransparente Auto-Erkennung (Stichwörter + LLM) ist
ersatzlos entfernt: emergency.py neu (nur manueller, vom Nutzer bestätigter
Notruf), llm_classifier.py gelöscht, Aufrufe in chat.py/ws.py raus,
emergency_llm_* aus der Config.

Neu: prominenter "🆘 Hilfe rufen"-Knopf (Bestätigung) → POST /api/emergency
→ protokolliert + liefert Hinweis in Nutzersprache. Eskalation (Benachrichtigung
an Angehörige) folgt später; vorerst Platzhalter "ACHTUNG: Es ist noch keine
Benachrichtigung eingebaut." (10 Sprachen). Admin-Tab "Notfälle" → "Notrufe".

Tests: test_emergency_llm.py entfernt, test_quota_safety.py auf Notruf-Endpunkt
umgeschrieben. 208 passed. Live verifiziert.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-25 03:02:52 +02:00
commit 3389f75af0
10 changed files with 112 additions and 433 deletions

View file

@ -17,8 +17,8 @@ from app.dependencies import (
)
from app.core.memory_extractor import maybe_schedule_extraction
from app.quota import enforce_quota, record_usage, QuotaExceededError
from app.safety.emergency import handle_emergency, schedule_llm_emergency_check
from app.schemas import ChatRequest
from app.safety.emergency import record_manual_emergency
from app.schemas import ChatRequest, EmergencyRequest
router = APIRouter()
@ -84,16 +84,10 @@ async def chat(
if user_context_parts:
llm_context = [{"role": "system", "content": "\n".join(user_context_parts)}] + llm_context
# Notfall-Erkennung zuerst (immer eskalieren, auch bei Quota-Limit).
emergency = handle_emergency(user, payload.text, store)
# Stufe 2: LLM-Klassifikation als Hintergrund-Task (nur wenn Stichwoerter nichts fanden).
schedule_llm_emergency_check(user, payload.text, store, emergency)
if emergency is None:
try:
enforce_quota(user, store)
except QuotaExceededError as exc:
raise HTTPException(status_code=429, detail=str(exc))
try:
enforce_quota(user, store)
except QuotaExceededError as exc:
raise HTTPException(status_code=429, detail=str(exc))
# Explizit angeforderte Stimme gewinnt; sonst folgt sie der Routensprache.
voice = payload.voice or voice_for_route(route.tts_provider, route.language, route.voice_gender)
@ -126,7 +120,6 @@ async def chat(
"route": route.as_dict(),
"history_len": len(conversation),
"memories_len": len(memories),
"emergency": emergency,
"trace": {
"raw_transcript": trace.raw_transcript,
"cleaned_transcript": trace.cleaned_transcript,
@ -148,3 +141,13 @@ async def chat(
if emergency:
headers["X-Emergency"] = emergency["category"]
return StreamingResponse(BytesIO(audio), media_type="audio/pcm", headers=headers)
@router.post("/emergency")
async def trigger_emergency(payload: EmergencyRequest, user: User = Depends(require_user)):
"""Vom Nutzer bestätigter Notruf. Protokolliert + liefert den Hinweis (in Nutzersprache).
Die eigentliche Benachrichtigung von Angehörigen folgt später.
"""
store = get_store()
return record_manual_emergency(user, store, payload.language)

View file

@ -34,7 +34,6 @@ from app.audio.vad import EnergyVAD
from app.core.memory_extractor import maybe_schedule_extraction
from app.quota import enforce_quota, record_usage, QuotaExceededError
from app.auth import authenticate
from app.safety.emergency import handle_emergency, schedule_llm_emergency_check
router = APIRouter()
@ -86,30 +85,11 @@ async def _run_turn(
if user_context_parts:
llm_context = [{"role": "system", "content": "\n".join(user_context_parts)}] + llm_context
# Notfall-Erkennung zuerst (immer eskalieren, auch bei Quota-Limit).
emergency = handle_emergency(user, text, store)
async def _on_llm_emergency(category):
try:
await websocket.send_json(
{"type": "emergency", "category": category, "source": "llm"}
)
except Exception: # Socket evtl. geschlossen -> ignorieren
pass
# Stufe 2: LLM-Klassifikation als Hintergrund-Task (nur wenn Stichwoerter nichts fanden).
schedule_llm_emergency_check(
user, text, store, emergency, on_emergency=_on_llm_emergency
)
if emergency:
await websocket.send_json({"type": "emergency", "category": emergency["category"]})
else:
try:
enforce_quota(user, store)
except QuotaExceededError as exc:
await websocket.send_json({"type": "error", "status": 429, "detail": str(exc)})
return
try:
enforce_quota(user, store)
except QuotaExceededError as exc:
await websocket.send_json({"type": "error", "status": 429, "detail": str(exc)})
return
await websocket.send_json({"type": "ack", "route": route.as_dict()})