feat: Web-Such-Budget pro Nutzer/Tag

Neue Tabelle web_search_usage + Store-Zaehler (web_search_count /
add_web_search_usage, kaskadiert in delete_user). Pro-Nutzer-Limit
web_search_daily_limit (Pref > global; 0 = unbegrenzt) in quota.py
(web_search_within_budget). ws.py: Gate vor _resolve (Budget aufgebraucht ->
Web-Suche fuer den Turn aus) + Zaehlung nach dem Turn (trace.web_searches,
gezaehlt im Orchestrator je on_citations). Admin: Feld + UI-Eingabe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-30 09:22:06 +02:00
commit 88c8c9df7f
8 changed files with 106 additions and 5 deletions

View file

@ -120,6 +120,14 @@ class Store(ABC):
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 web_search_count(self, user_id: str, day: str | None = None) -> int:
"""Anzahl Web-Suchen des Nutzers am Tag (Default: heute, UTC)."""
@abstractmethod
def add_web_search_usage(self, user_id: str, n: int = 1, day: str | None = None) -> int:
"""Zaehlt n Web-Suchen dazu und liefert die neue Tages-Zahl."""
@abstractmethod
def delete_user(self, user_id: str) -> bool:
"""Loescht einen Nutzer und alle seine Daten (Sessions, Nachrichten, Erinnerungen,
@ -229,6 +237,12 @@ class SQLiteStore(Store):
units INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, day)
);
CREATE TABLE IF NOT EXISTS web_search_usage (
user_id TEXT NOT NULL,
day TEXT NOT NULL,
count 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,
@ -480,6 +494,29 @@ class SQLiteStore(Store):
).fetchone()
return int(row["requests"])
def web_search_count(self, user_id: str, day: str | None = None) -> int:
day = day or self._today()
with self._connect() as conn:
row = conn.execute(
"SELECT count FROM web_search_usage WHERE user_id = ? AND day = ?",
(user_id, day),
).fetchone()
return int(row["count"]) if row else 0
def add_web_search_usage(self, user_id: str, n: int = 1, day: str | None = None) -> int:
day = day or self._today()
with self._connect() as conn:
conn.execute(
"INSERT INTO web_search_usage (user_id, day, count) VALUES (?, ?, ?)"
" ON CONFLICT(user_id, day) DO UPDATE SET count = count + excluded.count",
(user_id, day, int(n)),
)
row = conn.execute(
"SELECT count FROM web_search_usage WHERE user_id = ? AND day = ?",
(user_id, day),
).fetchone()
return int(row["count"])
def delete_user(self, user_id: str) -> bool:
if user_id == ANONYMOUS_USER_ID:
raise ValueError("Der anonyme Nutzer kann nicht geloescht werden.")
@ -494,6 +531,7 @@ class SQLiteStore(Store):
conn.execute("DELETE FROM sessions WHERE user_id = ?", (user_id,))
conn.execute("DELETE FROM memories WHERE user_id = ?", (user_id,))
conn.execute("DELETE FROM usage WHERE user_id = ?", (user_id,))
conn.execute("DELETE FROM web_search_usage WHERE user_id = ?", (user_id,))
conn.execute("DELETE FROM users WHERE id = ?", (user_id,))
return True