「活动报告取数三件套」此前在报告/markdown/分析/对比等 7 处手写重复, 唯一深化产物 build_campaign_report_dict 被锁在周期对比私有角落。 升位为 report.py 的 load_campaign_report(session, campaign) 单一出口 (探索线 summarize_campaign_exploration 同口径),并把 8 处 scenario_names 推导式收敛为 ScenarioRepository.name_map() 窄方法。 纯结构重排、零行为变更,572 项测试全绿。
124 lines
4.3 KiB
Python
124 lines
4.3 KiB
Python
"""API routes for statistics and dashboard data."""
|
|
|
|
from collections import defaultdict
|
|
from datetime import datetime, timezone
|
|
|
|
from fastapi import APIRouter, Depends
|
|
from sqlmodel import Session
|
|
|
|
from agenteval.evaluation.metrics import aggregate_runs
|
|
from agenteval.models import EvalRun, RunStatus
|
|
from agenteval.storage.model_config_repository import ModelConfigRepository
|
|
from agenteval.storage.repository import RunRepository, ScenarioRepository, TargetRepository
|
|
from agenteval.web.deps import get_db
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _ts(dt: datetime | None) -> float:
|
|
"""Sortable timestamp tolerant of naive/aware mixes in legacy rows."""
|
|
if dt is None:
|
|
return 0.0
|
|
if dt.tzinfo is None:
|
|
dt = dt.replace(tzinfo=timezone.utc)
|
|
return dt.timestamp()
|
|
|
|
|
|
def _settled(runs: list[EvalRun]) -> list[EvalRun]:
|
|
"""Runs with an outcome — in-flight runs are not results yet.
|
|
|
|
Aggregation itself (fault=0.0, cancelled excluded) is ADR-0004's concern
|
|
and lives in ``aggregate_runs``; callers only choose *which* runs count.
|
|
"""
|
|
return [r for r in runs if r.status in (RunStatus.COMPLETED, RunStatus.FAILED)]
|
|
|
|
|
|
@router.get("/dashboard")
|
|
def dashboard(session: Session = Depends(get_db)) -> dict:
|
|
targets = TargetRepository(session).list_all()
|
|
scenario_names = ScenarioRepository(session).name_map()
|
|
runs = RunRepository(session).list_all()
|
|
model_configs = ModelConfigRepository(session).list_all()
|
|
|
|
target_names = {t.id: t.name for t in targets}
|
|
|
|
settled_runs = _settled(runs)
|
|
overall_pass_rate = aggregate_runs(settled_runs)["pass_rate"]
|
|
|
|
today = datetime.now(timezone.utc).date()
|
|
today_runs = 0
|
|
running_count = 0
|
|
trigger_breakdown: dict[str, int] = defaultdict(int)
|
|
for r in runs:
|
|
if r.started_at:
|
|
started = r.started_at
|
|
if started.tzinfo is None:
|
|
started = started.replace(tzinfo=timezone.utc)
|
|
if started.date() == today:
|
|
today_runs += 1
|
|
if r.status in ("running", "pending"):
|
|
running_count += 1
|
|
trigger_breakdown[r.triggered_by.value] += 1
|
|
|
|
# Per-scenario aggregation over settled runs (ADR-0004 via aggregate_runs).
|
|
by_scenario: dict[str, list] = defaultdict(list)
|
|
for r in settled_runs:
|
|
by_scenario[r.scenario_id].append(r)
|
|
scenario_stats = []
|
|
for sid, sruns in by_scenario.items():
|
|
agg = aggregate_runs(sruns)
|
|
last_run = max(sruns, key=lambda r: _ts(r.started_at))
|
|
scenario_stats.append({
|
|
"scenario_id": sid,
|
|
"scenario_name": scenario_names.get(sid, sid[:8]),
|
|
"run_count": agg["run_count"],
|
|
"avg_pass_rate": agg["pass_rate"],
|
|
"last_run_at": last_run.started_at.isoformat() if last_run.started_at else None,
|
|
})
|
|
scenario_stats.sort(key=lambda s: s["run_count"], reverse=True)
|
|
|
|
recent_runs = sorted(runs, key=lambda r: _ts(r.started_at), reverse=True)[:10]
|
|
|
|
return {
|
|
"targets_count": len(targets),
|
|
"scenarios_count": len(scenario_names),
|
|
"runs_count": len(runs),
|
|
"model_configs_count": len(model_configs),
|
|
"today_runs": today_runs,
|
|
"running_count": running_count,
|
|
"overall_pass_rate": overall_pass_rate,
|
|
"trigger_breakdown": dict(trigger_breakdown),
|
|
"scenario_stats": scenario_stats,
|
|
"recent_runs": [
|
|
{
|
|
**r.model_dump(),
|
|
"scenario_name": scenario_names.get(r.scenario_id),
|
|
"target_name": target_names.get(r.target_id),
|
|
}
|
|
for r in recent_runs
|
|
],
|
|
}
|
|
|
|
|
|
@router.get("/trend")
|
|
def trend(days: int = 30, session: Session = Depends(get_db)) -> list[dict]:
|
|
runs = RunRepository(session).list_all()
|
|
|
|
daily: dict[str, list[EvalRun]] = defaultdict(list)
|
|
for run in _settled(runs):
|
|
if run.started_at:
|
|
daily[run.started_at.strftime("%Y-%m-%d")].append(run)
|
|
|
|
sorted_dates = sorted(daily.keys())[-days:]
|
|
points = []
|
|
for d in sorted_dates:
|
|
agg = aggregate_runs(daily[d])
|
|
if agg["pass_rate"] is None: # e.g. only cancelled runs that day
|
|
continue
|
|
points.append({
|
|
"date": d,
|
|
"pass_rate": round(agg["pass_rate"] * 100, 1),
|
|
"run_count": agg["run_count"],
|
|
})
|
|
return points
|