"""Stable read projections for evaluation Campaigns.""" from typing import Any, Optional from sqlmodel import Session from agenteval.evaluation.campaign_scheduler import clock_offset, elapsed_seconds from agenteval.evaluation.comparison import load_comparison_view from agenteval.evaluation.report import ( build_campaign_timeline, generate_campaign_report, summarize_campaign_progress, ) from agenteval.exploration.summary import summarize_campaign_exploration from agenteval.storage.db import iso_utc, utc_now from agenteval.storage.repository import ( CampaignAnalysisRepository, CampaignRepository, RunRepository, ScenarioRepository, TargetRepository, ) class CampaignReadModel: """One interface for Campaign list, detail, report and export projections.""" def __init__(self, session: Session): self._session = session self._campaigns = CampaignRepository(session) self._runs = RunRepository(session) self._scenarios = ScenarioRepository(session) self._analyses = CampaignAnalysisRepository(session) def list_items(self) -> list[dict[str, Any]]: campaigns = self._campaigns.list_all() campaign_ids = [campaign.id for campaign in campaigns if campaign.id] runs_by_campaign = self._runs.list_by_campaigns(campaign_ids) items = [] for campaign in campaigns: data = campaign.model_dump() data["progress"] = summarize_campaign_progress( campaign, runs_by_campaign.get(campaign.id, []), ) items.append(data) return items def detail(self, campaign_id: str) -> Optional[dict[str, Any]]: campaign = self._campaigns.get(campaign_id) if campaign is None: return None data = campaign.model_dump() runs = self._runs.list_by_campaign(campaign_id) current_offset = 0.0 if campaign.started_at is not None: current_offset = min( clock_offset( elapsed_seconds=elapsed_seconds(now=utc_now(), started_at=campaign.started_at), time_scale=campaign.time_scale, ), float(campaign.window_seconds), ) data["progress"] = { "current_offset_seconds": current_offset, "spawned_runs": len(runs), "completed_runs": sum(1 for run in runs if run.status.value == "completed"), } return data def report(self, campaign_id: str) -> Optional[dict[str, Any]]: campaign = self._campaigns.get(campaign_id) if campaign is None: return None report = self._core_report(campaign) exploration = summarize_campaign_exploration(self._session, campaign_id) if exploration is not None: report["exploration"] = exploration return report def timeline(self, campaign_id: str) -> Optional[dict[str, Any]]: campaign = self._campaigns.get(campaign_id) if campaign is None: return None entries = build_campaign_timeline( campaign, self._runs.list_by_campaign(campaign_id), scenario_names=self._scenarios.name_map(), ) return {"entries": entries} def analysis(self, campaign_id: str) -> Optional[dict[str, Any]]: if self._campaigns.get(campaign_id) is None: return None row = self._analyses.get_by_campaign(campaign_id) if row is None: return {"status": "none"} return { "status": row.status, "result": row.get_result(), "error": row.error, "model_config_id": row.model_config_id, "triggered_by": row.triggered_by, "updated_at": iso_utc(row.updated_at), } def comparison(self, campaign_id: str) -> Optional[dict[str, Any]]: campaign = self._campaigns.get(campaign_id) return load_comparison_view(self._session, campaign) if campaign is not None else None def full_view(self, campaign_id: str) -> Optional[dict[str, Any]]: campaign = self._campaigns.get(campaign_id) if campaign is None: return None report = self._core_report(campaign) exploration = summarize_campaign_exploration(self._session, campaign_id) analysis = self.analysis(campaign_id) comparison = load_comparison_view(self._session, campaign) return { "report": report, "exploration": exploration, "analysis": analysis.get("result") if analysis and analysis.get("status") == "completed" else None, "comparison": comparison if comparison.get("status") != "none" else None, } def markdown_projection(self, campaign_id: str) -> Optional[dict[str, Any]]: campaign = self._campaigns.get(campaign_id) view = self.full_view(campaign_id) if campaign is None or view is None: return None comparison = None comparison_view = view["comparison"] if comparison_view and comparison_view.get("status") == "completed": row = comparison_view.get("comparison") or {} baseline = row.get("baseline") or {} comparison = { "result": row.get("result"), "baseline_name": baseline.get("name"), "baseline_completed_at": baseline.get("completed_at"), "model_name": row.get("model_name"), "updated_at": row.get("updated_at"), "metric_diff": comparison_view.get("metric_diff"), } target = TargetRepository(self._session).get(campaign.target_id) return { **view, "comparison": comparison, "target_name": target.name if target else None, "scenario_names": self._scenarios.name_map(), } def _core_report(self, campaign) -> dict[str, Any]: return generate_campaign_report( campaign, self._runs.list_by_campaign(campaign.id), scenario_names=self._scenarios.name_map(), )