101 lines
3.7 KiB
Python
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)
|