"""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.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() @router.get("/dashboard") def dashboard(session: Session = Depends(get_db)) -> dict: targets = TargetRepository(session).list_all() scenarios = ScenarioRepository(session).list_all() runs = RunRepository(session).list_all() model_configs = ModelConfigRepository(session).list_all() scenario_names = {s.id: s.name for s in scenarios} target_names = {t.id: t.name for t in targets} completed_runs = [r for r in runs if r.status == "completed" and r.summary] pass_rates = [r.summary.get("pass_rate", 0) for r in completed_runs if isinstance(r.summary, dict)] overall_pass_rate = sum(pass_rates) / len(pass_rates) if pass_rates else None 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 completed runs. by_scenario: dict[str, list] = defaultdict(list) for r in completed_runs: by_scenario[r.scenario_id].append(r) scenario_stats = [] for sid, sruns in by_scenario.items(): rates = [r.summary.get("pass_rate", 0) for r in sruns if isinstance(r.summary, dict)] 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": len(sruns), "avg_pass_rate": round(sum(rates) / len(rates), 4) if rates else None, "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(scenarios), "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() completed = [r for r in runs if r.status == "completed" and r.summary and r.started_at] daily: dict[str, list[float]] = defaultdict(list) for run in completed: date_str = run.started_at.strftime("%Y-%m-%d") if run.started_at else "" if date_str and isinstance(run.summary, dict): daily[date_str].append(run.summary.get("pass_rate", 0)) sorted_dates = sorted(daily.keys())[-days:] return [ { "date": d, "pass_rate": round(sum(daily[d]) / len(daily[d]) * 100, 1), "run_count": len(daily[d]), } for d in sorted_dates ]