feat(report): add load_campaign_view as unified campaign read model
活动级读模型单一出口:一次取齐报告 / 探索 / 分析 / 对比四大数据源。
markdown handler 从 30 行拼装逻辑缩到 3 行;分析执行器同步迁移。
comparison.py 内部的 8 次 load_campaign_report 调用暂不动(跨请求冗余,
缓存收益有限,改动风险高)。
- 新增 load_campaign_view(session, campaign) -> dict[str, Any]
- 返回 {report, exploration, analysis, comparison} 四键
- 迁移 markdown handler 和分析执行器两个调用点
- 4 个新测试覆盖 view 的组装逻辑
This commit is contained in:
parent
2fddce8c92
commit
42be31dd1f
@ -13,8 +13,7 @@ from typing import Any, Awaitable, Callable, Optional
|
|||||||
|
|
||||||
from sqlmodel import Session
|
from sqlmodel import Session
|
||||||
|
|
||||||
from agenteval.evaluation.report import load_campaign_report
|
from agenteval.evaluation.report import load_campaign_view
|
||||||
from agenteval.exploration.summary import summarize_campaign_exploration
|
|
||||||
from agenteval.model_gateway import ModelGateway
|
from agenteval.model_gateway import ModelGateway
|
||||||
from agenteval.models import Campaign, ModelCapability, RunStatus
|
from agenteval.models import Campaign, ModelCapability, RunStatus
|
||||||
from agenteval.services.model_configs import (
|
from agenteval.services.model_configs import (
|
||||||
@ -306,14 +305,14 @@ async def execute_campaign_analysis(
|
|||||||
try:
|
try:
|
||||||
client = chat_client or gateway_chat_client(runtime)
|
client = chat_client or gateway_chat_client(runtime)
|
||||||
runs = RunRepository(session).list_by_campaign(campaign_id)
|
runs = RunRepository(session).list_by_campaign(campaign_id)
|
||||||
report = load_campaign_report(session, campaign)
|
view = load_campaign_view(session, campaign)
|
||||||
result = await analyze_campaign(
|
result = await analyze_campaign(
|
||||||
campaign=campaign,
|
campaign=campaign,
|
||||||
report=report,
|
report=view["report"],
|
||||||
failure_samples=collect_failure_samples(campaign_id, session),
|
failure_samples=collect_failure_samples(campaign_id, session),
|
||||||
valid_run_ids={r.id for r in runs if r.id},
|
valid_run_ids={r.id for r in runs if r.id},
|
||||||
chat_client=client,
|
chat_client=client,
|
||||||
exploration_summary=summarize_campaign_exploration(session, campaign_id),
|
exploration_summary=view["exploration"],
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
_logger.warning("活动 %s 智能分析失败: %s", campaign_id, exc)
|
_logger.warning("活动 %s 智能分析失败: %s", campaign_id, exc)
|
||||||
|
|||||||
@ -360,6 +360,33 @@ def load_campaign_report(session: Session, campaign: Campaign) -> dict[str, Any]
|
|||||||
return generate_campaign_report(campaign, runs, scenario_names=ScenarioRepository(session).name_map())
|
return generate_campaign_report(campaign, runs, scenario_names=ScenarioRepository(session).name_map())
|
||||||
|
|
||||||
|
|
||||||
|
def load_campaign_view(session: Session, campaign: Campaign) -> dict[str, Any]:
|
||||||
|
"""活动级读模型单一出口:一次取齐报告 / 探索 / 分析 / 对比。
|
||||||
|
|
||||||
|
四个数据源总是被一起取(markdown 导出、分析执行器、前端报告抽屉),
|
||||||
|
统一入口消除调用方的拼装逻辑。各子模块的取法保持不变,此处只做组合。
|
||||||
|
"""
|
||||||
|
from agenteval.evaluation.comparison import load_comparison_view
|
||||||
|
from agenteval.exploration.summary import summarize_campaign_exploration
|
||||||
|
from agenteval.storage.repository import CampaignAnalysisRepository
|
||||||
|
|
||||||
|
report = load_campaign_report(session, campaign)
|
||||||
|
exploration = summarize_campaign_exploration(session, campaign.id)
|
||||||
|
|
||||||
|
analysis_row = CampaignAnalysisRepository(session).get_by_campaign(campaign.id)
|
||||||
|
analysis = analysis_row.get_result() if analysis_row and analysis_row.status == "completed" else None
|
||||||
|
|
||||||
|
comparison_view = load_comparison_view(session, campaign)
|
||||||
|
comparison = comparison_view if comparison_view.get("status") != "none" else None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"report": report,
|
||||||
|
"exploration": exploration,
|
||||||
|
"analysis": analysis,
|
||||||
|
"comparison": comparison,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def save_report(run_id: str, fmt: str = "html", output_dir: Optional[Path] = None) -> Path:
|
def save_report(run_id: str, fmt: str = "html", output_dir: Optional[Path] = None) -> Path:
|
||||||
"""Generate a run report and save it to disk in the requested format."""
|
"""Generate a run report and save it to disk in the requested format."""
|
||||||
output_dir = output_dir or DATA_DIR / "reports"
|
output_dir = output_dir or DATA_DIR / "reports"
|
||||||
|
|||||||
@ -22,6 +22,7 @@ from agenteval.evaluation.comparison import (
|
|||||||
from agenteval.evaluation.report import (
|
from agenteval.evaluation.report import (
|
||||||
build_campaign_timeline,
|
build_campaign_timeline,
|
||||||
load_campaign_report,
|
load_campaign_report,
|
||||||
|
load_campaign_view,
|
||||||
summarize_campaign_progress,
|
summarize_campaign_progress,
|
||||||
)
|
)
|
||||||
from agenteval.evaluation.report_render import render_campaign_markdown
|
from agenteval.evaluation.report_render import render_campaign_markdown
|
||||||
@ -139,32 +140,30 @@ async def get_campaign_report_markdown(campaign_id: str, session: Session = Depe
|
|||||||
campaign = CampaignRepository(session).get(campaign_id)
|
campaign = CampaignRepository(session).get(campaign_id)
|
||||||
if not campaign:
|
if not campaign:
|
||||||
raise HTTPException(status_code=404, detail="campaign not found")
|
raise HTTPException(status_code=404, detail="campaign not found")
|
||||||
report = load_campaign_report(session, campaign)
|
view = load_campaign_view(session, campaign)
|
||||||
scenario_names = ScenarioRepository(session).name_map()
|
scenario_names = ScenarioRepository(session).name_map()
|
||||||
analysis_row = CampaignAnalysisRepository(session).get_by_campaign(campaign_id)
|
|
||||||
analysis = analysis_row.get_result() if analysis_row and analysis_row.status == "completed" else None
|
|
||||||
target = TargetRepository(session).get(campaign.target_id)
|
target = TargetRepository(session).get(campaign.target_id)
|
||||||
target_name = target.name if target else None
|
target_name = target.name if target else None
|
||||||
|
|
||||||
comparison = None
|
comparison = None
|
||||||
view = load_comparison_view(session, campaign)
|
if view["comparison"] and view["comparison"].get("status") == "completed":
|
||||||
if view["status"] == "completed" and view["comparison"] and view["comparison"].get("result"):
|
cmp_view = view["comparison"]
|
||||||
cmp = view["comparison"]
|
cmp = cmp_view.get("comparison") or {}
|
||||||
baseline = cmp.get("baseline") or {}
|
baseline = cmp.get("baseline") or {}
|
||||||
comparison = {
|
comparison = {
|
||||||
"result": cmp["result"],
|
"result": cmp.get("result"),
|
||||||
"baseline_name": baseline.get("name"),
|
"baseline_name": baseline.get("name"),
|
||||||
"baseline_completed_at": baseline.get("completed_at"),
|
"baseline_completed_at": baseline.get("completed_at"),
|
||||||
"model_name": cmp.get("model_name"),
|
"model_name": cmp.get("model_name"),
|
||||||
"updated_at": cmp.get("updated_at"),
|
"updated_at": cmp.get("updated_at"),
|
||||||
"metric_diff": view["metric_diff"],
|
"metric_diff": cmp_view.get("metric_diff"),
|
||||||
}
|
}
|
||||||
|
|
||||||
md = render_campaign_markdown(
|
md = render_campaign_markdown(
|
||||||
report,
|
view["report"],
|
||||||
analysis=analysis,
|
analysis=view["analysis"],
|
||||||
comparison=comparison,
|
comparison=comparison,
|
||||||
exploration=summarize_campaign_exploration(session, campaign_id),
|
exploration=view["exploration"],
|
||||||
target_name=target_name,
|
target_name=target_name,
|
||||||
scenario_names=scenario_names,
|
scenario_names=scenario_names,
|
||||||
)
|
)
|
||||||
|
|||||||
100
tests/unit/test_campaign_view.py
Normal file
100
tests/unit/test_campaign_view.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
"""Tests for load_campaign_view — the unified campaign read model."""
|
||||||
|
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from agenteval.evaluation.report import load_campaign_view
|
||||||
|
from agenteval.models import (
|
||||||
|
Campaign,
|
||||||
|
CampaignPlanEntry,
|
||||||
|
CampaignStatus,
|
||||||
|
Case,
|
||||||
|
CaseType,
|
||||||
|
EvalTarget,
|
||||||
|
Scenario,
|
||||||
|
)
|
||||||
|
from agenteval.storage.repository import (
|
||||||
|
CampaignAnalysisRepository,
|
||||||
|
CampaignRepository,
|
||||||
|
ScenarioRepository,
|
||||||
|
TargetRepository,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _seed_campaign(db_session, *, status=CampaignStatus.COMPLETED):
|
||||||
|
target = TargetRepository(db_session).create(
|
||||||
|
EvalTarget(id=f"t-{uuid4().hex[:8]}", name="数字员工")
|
||||||
|
)
|
||||||
|
scenario = ScenarioRepository(db_session).create(
|
||||||
|
Scenario(
|
||||||
|
id=f"s-{uuid4().hex[:8]}",
|
||||||
|
name="查账单",
|
||||||
|
target_id=target.id,
|
||||||
|
cases=[Case(id="c-1", type=CaseType.SINGLE, messages=["你好"])],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
campaign = CampaignRepository(db_session).create(
|
||||||
|
Campaign(
|
||||||
|
name="cycle",
|
||||||
|
target_id=target.id,
|
||||||
|
window_seconds=3600,
|
||||||
|
time_scale=1.0,
|
||||||
|
plan=[CampaignPlanEntry(scenario_id=scenario.id, offset_seconds=0, count=1)],
|
||||||
|
status=status,
|
||||||
|
started_at=None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return campaign
|
||||||
|
|
||||||
|
|
||||||
|
def test_view_returns_all_four_keys(db_session):
|
||||||
|
"""View always returns report/exploration/analysis/comparison keys."""
|
||||||
|
campaign = _seed_campaign(db_session)
|
||||||
|
view = load_campaign_view(db_session, campaign)
|
||||||
|
assert "report" in view
|
||||||
|
assert "exploration" in view
|
||||||
|
assert "analysis" in view
|
||||||
|
assert "comparison" in view
|
||||||
|
|
||||||
|
|
||||||
|
def test_view_with_no_analysis_or_comparison(db_session):
|
||||||
|
"""No analysis, no comparison, no exploration → those keys are None."""
|
||||||
|
campaign = _seed_campaign(db_session)
|
||||||
|
view = load_campaign_view(db_session, campaign)
|
||||||
|
assert view["report"] is not None
|
||||||
|
assert view["analysis"] is None
|
||||||
|
assert view["comparison"] is None
|
||||||
|
assert view["exploration"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_view_with_completed_analysis(db_session):
|
||||||
|
"""Completed analysis row → analysis key is non-None."""
|
||||||
|
campaign = _seed_campaign(db_session)
|
||||||
|
analyses = CampaignAnalysisRepository(db_session)
|
||||||
|
analyses.upsert(campaign.id, status="completed", result={"summary": "ok"})
|
||||||
|
view = load_campaign_view(db_session, campaign)
|
||||||
|
assert view["analysis"] is not None
|
||||||
|
assert view["analysis"]["summary"] == "ok"
|
||||||
|
|
||||||
|
|
||||||
|
def test_view_with_comparison(db_session):
|
||||||
|
"""Completed comparison row → comparison key is non-None and contains metric_diff."""
|
||||||
|
campaign = _seed_campaign(db_session)
|
||||||
|
# Create a baseline campaign for comparison
|
||||||
|
baseline = _seed_campaign(db_session)
|
||||||
|
comparisons = CampaignAnalysisRepository(db_session)
|
||||||
|
# Mark both as having completed analysis (required for comparison)
|
||||||
|
comparisons.upsert(baseline.id, status="completed", result={"summary": "baseline"})
|
||||||
|
comparisons.upsert(campaign.id, status="completed", result={"summary": "current"})
|
||||||
|
# Create a comparison row
|
||||||
|
from agenteval.storage.repository import CampaignPeriodComparisonRepository
|
||||||
|
|
||||||
|
cmp_repo = CampaignPeriodComparisonRepository(db_session)
|
||||||
|
cmp_repo.upsert(
|
||||||
|
campaign.id,
|
||||||
|
status="completed",
|
||||||
|
baseline_campaign_id=baseline.id,
|
||||||
|
result={"evolution": []},
|
||||||
|
)
|
||||||
|
view = load_campaign_view(db_session, campaign)
|
||||||
|
assert view["comparison"] is not None
|
||||||
|
assert view["comparison"]["status"] == "completed"
|
||||||
Loading…
Reference in New Issue
Block a user