Files
bxh/app/api/audit.py

101 lines
3.7 KiB
Python

"""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)