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)