amana / src /audit.py
Misbahuddin's picture
Deploy-readiness: Neon persistence, moderator identity, run telemetry, warm redesign
8e98f03
Raw
History Blame Contribute Delete
2.66 kB
"""Append-only audit log of moderator decisions.
Each record is one human decision over a campaign: the AI's recommendation, the human's final call,
who made it, whether it was an override, and the reason. This is the receipt that the **human** —
not the AI — owns the decision.
Two backends behind one interface: when `DATABASE_URL` is set (Neon Postgres on the deployed Space)
the records persist there and survive restarts; otherwise they go to a flat JSONL file (easy to show
locally, trivially greppable). Callers (`api.py`) don't care which is active.
"""
from __future__ import annotations
import json
from datetime import datetime, timezone
from pathlib import Path
from .config import CONFIG
def utc_now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def _use_db() -> bool:
"""Use Postgres when a database is configured; otherwise the local JSONL file."""
return bool(CONFIG.database_url)
def append_decision(record: dict, path: str | Path | None = None) -> dict:
"""Append one decision record to the log (a `timestamp` is added if absent).
Returns the stored record. Writes to Postgres when configured, else the JSONL file (creating its
parent dir if needed).
"""
stored = {"timestamp": utc_now_iso(), **record}
if _use_db():
from . import db # lazy: only pulls in psycopg when a database is configured
db.insert_decision(stored)
return stored
p = Path(path or CONFIG.audit_log_path)
p.parent.mkdir(parents=True, exist_ok=True)
with p.open("a", encoding="utf-8") as f:
f.write(json.dumps(stored, ensure_ascii=False) + "\n")
return stored
def load_decisions(path: str | Path | None = None) -> list[dict]:
"""Load all decision records in write order (oldest first). Tolerates a missing/partial file."""
if _use_db():
from . import db # lazy
return db.fetch_decisions()
p = Path(path or CONFIG.audit_log_path)
if not p.exists():
return []
out: list[dict] = []
for line in p.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line:
continue
try:
out.append(json.loads(line))
except json.JSONDecodeError:
continue # skip a half-written line rather than crash the UI
return out
def decided_campaign_ids(path: str | Path | None = None) -> dict[str, dict]:
"""Map each decided campaign_id to its MOST RECENT decision record."""
latest: dict[str, dict] = {}
for rec in load_decisions(path):
cid = rec.get("campaign_id")
if cid:
latest[cid] = rec
return latest