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
64
app/store.py
64
app/store.py
|
|
@ -97,6 +97,18 @@ class Store(ABC):
|
|||
def delete_memory(self, user_id: str, memory_id: int) -> bool:
|
||||
"""Loescht eine Erinnerung des Nutzers. True, wenn etwas geloescht wurde."""
|
||||
|
||||
@abstractmethod
|
||||
def get_request_count(self, user_id: str, day: str | None = None) -> int:
|
||||
"""Anzahl der Anfragen des Nutzers am angegebenen Tag (Default: heute, UTC)."""
|
||||
|
||||
@abstractmethod
|
||||
def add_usage(self, user_id: str, units: int = 0, day: str | None = None) -> int:
|
||||
"""Zaehlt eine Anfrage (+units) und liefert die neue Tages-Anfragezahl."""
|
||||
|
||||
@abstractmethod
|
||||
def log_emergency(self, user_id: str, category: str, snippet: str) -> None:
|
||||
"""Protokolliert ein erkanntes Notfall-Signal (sensibel!)."""
|
||||
|
||||
|
||||
class SQLiteStore(Store):
|
||||
def __init__(self, db_path: str):
|
||||
|
|
@ -146,6 +158,20 @@ class SQLiteStore(Store):
|
|||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_user
|
||||
ON memories(user_id, id);
|
||||
CREATE TABLE IF NOT EXISTS usage (
|
||||
user_id TEXT NOT NULL,
|
||||
day TEXT NOT NULL,
|
||||
requests INTEGER NOT NULL DEFAULT 0,
|
||||
units INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (user_id, day)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS emergency_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
snippet TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
|
@ -301,3 +327,41 @@ class SQLiteStore(Store):
|
|||
(memory_id, user_id),
|
||||
)
|
||||
return cur.rowcount > 0
|
||||
|
||||
# ----- Nutzung / Quota --------------------------------------------------
|
||||
@staticmethod
|
||||
def _today() -> str:
|
||||
return datetime.now(timezone.utc).date().isoformat()
|
||||
|
||||
def get_request_count(self, user_id: str, day: str | None = None) -> int:
|
||||
day = day or self._today()
|
||||
with self._connect() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT requests FROM usage WHERE user_id = ? AND day = ?",
|
||||
(user_id, day),
|
||||
).fetchone()
|
||||
return int(row["requests"]) if row else 0
|
||||
|
||||
def add_usage(self, user_id: str, units: int = 0, day: str | None = None) -> int:
|
||||
day = day or self._today()
|
||||
with self._connect() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO usage (user_id, day, requests, units) VALUES (?, ?, 1, ?)"
|
||||
" ON CONFLICT(user_id, day) DO UPDATE SET"
|
||||
" requests = requests + 1, units = units + excluded.units",
|
||||
(user_id, day, units),
|
||||
)
|
||||
row = conn.execute(
|
||||
"SELECT requests FROM usage WHERE user_id = ? AND day = ?",
|
||||
(user_id, day),
|
||||
).fetchone()
|
||||
return int(row["requests"])
|
||||
|
||||
# ----- Notfall-Protokoll ------------------------------------------------
|
||||
def log_emergency(self, user_id: str, category: str, snippet: str) -> None:
|
||||
with self._connect() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO emergency_events (user_id, category, snippet, created_at)"
|
||||
" VALUES (?, ?, ?, ?)",
|
||||
(user_id, category, snippet, _now()),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue