safety layer: hard daily budget, user quotas, spend report, forgetme + privacy
This commit is contained in:
@@ -13,7 +13,7 @@ from contextlib import closing
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
SCHEMA_VERSION = 1
|
||||
SCHEMA_VERSION = 2
|
||||
|
||||
|
||||
class PersistentStore:
|
||||
@@ -27,14 +27,21 @@ class PersistentStore:
|
||||
return conn
|
||||
|
||||
def _init_db(self) -> None:
|
||||
# Forward-only migrations keyed on user_version (PER-06)
|
||||
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with closing(self._connect()) as conn, conn:
|
||||
if conn.execute("PRAGMA user_version").fetchone()[0] < SCHEMA_VERSION:
|
||||
version = conn.execute("PRAGMA user_version").fetchone()[0]
|
||||
if version < 1:
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS history (id INTEGER PRIMARY KEY, channel TEXT NOT NULL, role TEXT NOT NULL, content TEXT NOT NULL)"
|
||||
)
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS history_channel ON history (channel)")
|
||||
conn.execute("CREATE TABLE IF NOT EXISTS memory (channel TEXT PRIMARY KEY, content TEXT NOT NULL)")
|
||||
if version < 2:
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS usage (day TEXT NOT NULL, key TEXT NOT NULL, value REAL NOT NULL, PRIMARY KEY (day, key))"
|
||||
)
|
||||
if version < SCHEMA_VERSION:
|
||||
conn.execute(f"PRAGMA user_version = {SCHEMA_VERSION}")
|
||||
os.chmod(self.db_path, 0o600) # conversation data (PER-04)
|
||||
|
||||
@@ -65,6 +72,24 @@ class PersistentStore:
|
||||
(channel, content),
|
||||
)
|
||||
|
||||
def usage_add(self, day: str, key: str, amount: float) -> None:
|
||||
with closing(self._connect()) as conn, conn:
|
||||
conn.execute(
|
||||
"INSERT INTO usage (day, key, value) VALUES (?, ?, ?) ON CONFLICT(day, key) DO UPDATE SET value = value + excluded.value",
|
||||
(day, key, amount),
|
||||
)
|
||||
|
||||
def usage_get(self, day: str, key: str) -> float:
|
||||
with closing(self._connect()) as conn:
|
||||
row = conn.execute("SELECT value FROM usage WHERE day = ? AND key = ?", (day, key)).fetchone()
|
||||
return float(row[0]) if row else 0.0
|
||||
|
||||
def delete_history_of_user(self, user: str) -> int:
|
||||
"""Remove persisted rows carrying this user's messages (SAF-08)."""
|
||||
with closing(self._connect()) as conn, conn:
|
||||
cursor = conn.execute("DELETE FROM history WHERE content LIKE ?", (f'%"user": "{user}"%',))
|
||||
return cursor.rowcount
|
||||
|
||||
def migrate_pickles(self, channel: str, history_file: Path, memory_file: Path) -> None:
|
||||
"""Import legacy pickles once; rename them *.migrated (PER-03)."""
|
||||
if self.load_history(channel) or self.load_memory(channel) is not None:
|
||||
|
||||
Reference in New Issue
Block a user