AgentEvalTool/backend/agenteval/evaluation/campaign_read_model.py

160 lines
6.1 KiB
Python

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