"""STEP 02 — AI Audit endpoints.""" from fastapi import APIRouter, HTTPException from app.auth import CurrentUser from app.config import settings from app.db import get_conn, create_audit_run, get_audit_run, get_latest_audit_run from app.agents.auditor import schedule_audit router = APIRouter() @router.post("/audit/run") async def run_audit(_user: CurrentUser = None): """Background-audit all un-evaluated question traces; returns run_id.""" s = settings.db_schema async with get_conn() as conn: async with conn.cursor() as cur: await cur.execute( f"SELECT id FROM {s}.question_traces " "WHERE tenant_id=%s AND project_id=%s AND coverage_score IS NULL " "ORDER BY created_at LIMIT 100", (settings.default_tenant, settings.default_project), ) rows = await cur.fetchall() if not rows: return {"message": "没有待补审的未评估问题(题库稽查请用「运行稽查」)", "run_id": None, "total": 0} trace_ids = [r["id"] for r in rows] run_id = await create_audit_run("audit_run", len(trace_ids)) schedule_audit(trace_ids, run_id) return {"run_id": run_id, "total": len(trace_ids)} @router.get("/audit-runs/latest") async def _latest_run(_user: CurrentUser = None): return await get_latest_audit_run() or {} @router.get("/audit-runs/{run_id}") async def _get_run(run_id: int, _user: CurrentUser = None): r = await get_audit_run(run_id) if not r: raise HTTPException(404, "audit run not found") return r @router.get("/audit/reports") async def list_reports(_user: CurrentUser = None): """Return historical audit snapshots (aggregated from question_traces).""" s = settings.db_schema async with get_conn() as conn: async with conn.cursor() as cur: await cur.execute( f"""SELECT DATE(evaluated_at) AS report_date, COUNT(*) AS total, COUNT(*) FILTER (WHERE suggested_action='hit') AS hits, COUNT(*) FILTER (WHERE suggested_action='gap') AS gaps, COUNT(*) FILTER (WHERE suggested_action='low_quality') AS low_quality, COUNT(*) FILTER (WHERE suggested_action='conflict') AS conflicts, ROUND(AVG(coverage_score)::numeric, 3) AS avg_coverage FROM {s}.question_traces WHERE evaluated_at IS NOT NULL GROUP BY DATE(evaluated_at) ORDER BY report_date DESC LIMIT 30""", ) return await cur.fetchall() @router.get("/audit/reports/latest") async def latest_report(_user: CurrentUser = None): s = settings.db_schema async with get_conn() as conn: async with conn.cursor() as cur: await cur.execute( f"SELECT * FROM {s}.question_traces " "WHERE evaluated_at IS NOT NULL " "ORDER BY evaluated_at DESC LIMIT 50" ) rows = await cur.fetchall() if not rows: return {"message": "No audit data yet"} hits = sum(1 for r in rows if r.get("suggested_action") == "hit") gaps = sum(1 for r in rows if r.get("suggested_action") == "gap") avg_cov = sum(r.get("coverage_score") or 0 for r in rows) / len(rows) if rows else 0 return { "total_evaluated": len(rows), "hits": hits, "gaps": gaps, "avg_coverage": round(avg_cov, 3), "coverage_rate": round(hits / len(rows), 3) if rows else 0, } @router.get("/audit/gaps") async def list_gaps(_user: CurrentUser = None): from app.db import get_audit_gaps return await get_audit_gaps(settings.default_tenant, settings.default_project)