49 lines
2.1 KiB
Python
49 lines
2.1 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import sqlite3
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
from threading import Lock
|
|
from typing import Any
|
|
|
|
|
|
class ConversationLogStore:
|
|
"""SQLite-backed local audit log for chat and tool activity."""
|
|
|
|
def __init__(self, path: str | Path) -> None:
|
|
self.path = Path(path)
|
|
self._lock = Lock()
|
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
with self._connect() as connection:
|
|
connection.execute("CREATE TABLE IF NOT EXISTS conversation_events (id INTEGER PRIMARY KEY AUTOINCREMENT, occurred_at TEXT NOT NULL, event_type TEXT NOT NULL, payload_json TEXT NOT NULL)")
|
|
|
|
def append(self, event: dict[str, Any]) -> None:
|
|
with self._lock, self._connect() as connection:
|
|
connection.execute("INSERT INTO conversation_events (occurred_at, event_type, payload_json) VALUES (?, ?, ?)", (datetime.now(UTC).isoformat(), str(event.get("type") or "event"), json.dumps(_safe(event), ensure_ascii=True, default=str)))
|
|
|
|
def list_recent(self, *, limit: int = 200) -> list[dict[str, Any]]:
|
|
with self._connect() as connection:
|
|
rows = connection.execute("SELECT id, occurred_at, event_type, payload_json FROM conversation_events ORDER BY id DESC LIMIT ?", (max(1, min(limit, 1000)),)).fetchall()
|
|
result = []
|
|
for row in rows:
|
|
try:
|
|
payload = json.loads(row["payload_json"])
|
|
except (TypeError, ValueError):
|
|
payload = {}
|
|
result.append({"id": row["id"], "occurred_at": row["occurred_at"], "event_type": row["event_type"], **payload})
|
|
return result
|
|
|
|
def _connect(self) -> sqlite3.Connection:
|
|
connection = sqlite3.connect(self.path)
|
|
connection.row_factory = sqlite3.Row
|
|
return connection
|
|
|
|
|
|
def _safe(value: Any) -> Any:
|
|
if isinstance(value, dict):
|
|
return {str(k): _safe(v) for k, v in value.items()}
|
|
if isinstance(value, list):
|
|
return [_safe(v) for v in value]
|
|
return value if isinstance(value, (str, int, float, bool)) or value is None else str(value)
|