feat: Tageskontingent und Notfall-Eskalation (#6)
- Quota (app/quota.py): Anfragen pro Nutzer/Tag (usage-Tabelle), DAILY_REQUEST_LIMIT, pro Nutzer via prefs uebersteuerbar; 429 (REST) bzw. error-Event (WS); Metrik - Notfall (app/safety/emergency.py): heuristische Erkennung (de/en); Log im Store (emergency_events) + optionaler Webhook (best-effort) + X-Emergency/emergency-Event; Metrik emergency_total; Notfaelle umgehen das Kontingent - verdrahtet in chat/speak/transcribe + WS-Turns - Tests: 64 gruen (+6); Doku aktualisiert (README, BEDIENUNGSANLEITUNG, Architektur, .env.example) Hinweis: Notfall-Erkennung ist eine Heuristik (kein Lebensretter); erkannte Texte sind sensibel -> DSGVO beachten. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
6422444017
commit
1eb79c1f09
14 changed files with 378 additions and 2 deletions
|
|
@ -13,6 +13,8 @@ from app.dependencies import (
|
|||
resolve_output_endpoint,
|
||||
get_store,
|
||||
)
|
||||
from app.quota import enforce_quota, record_usage, QuotaExceededError
|
||||
from app.safety.emergency import handle_emergency
|
||||
from app.schemas import ChatRequest
|
||||
|
||||
router = APIRouter()
|
||||
|
|
@ -77,6 +79,15 @@ async def chat(
|
|||
)
|
||||
llm_context = [{"role": "system", "content": memory_text}] + llm_context
|
||||
|
||||
# Notfall-Erkennung zuerst (immer eskalieren, auch bei Quota-Limit).
|
||||
emergency = handle_emergency(user, payload.text, store)
|
||||
|
||||
if emergency is None:
|
||||
try:
|
||||
enforce_quota(user, store)
|
||||
except QuotaExceededError as exc:
|
||||
raise HTTPException(status_code=429, detail=str(exc))
|
||||
|
||||
try:
|
||||
trace, audio = await orchestrator.chat_text(
|
||||
payload.text,
|
||||
|
|
@ -88,6 +99,8 @@ async def chat(
|
|||
except Exception as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc))
|
||||
|
||||
record_usage(user, store, len(payload.text) + len(trace.semantic_response or ""))
|
||||
|
||||
# Turn persistieren (User-Eingabe + semantische Antwort) fuer das Gedaechtnis.
|
||||
if session_id:
|
||||
store.append_message(session_id, user.id, "user", payload.text)
|
||||
|
|
@ -101,6 +114,7 @@ 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,
|
||||
|
|
@ -119,4 +133,6 @@ async def chat(
|
|||
"X-Audio-Sample-Width": "16",
|
||||
**_route_headers(route),
|
||||
}
|
||||
if emergency:
|
||||
headers["X-Emergency"] = emergency["category"]
|
||||
return StreamingResponse(BytesIO(audio), media_type="audio/pcm", headers=headers)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue