AgentEvalTool/backend/agenteval/web/routers/campaigns.py
sinohqb e4404f1fa2 feat(campaigns): add Campaign persistence and create/query API
Introduce the 评估活动 (Campaign) aggregate above Run: a single-target,
service-cycle window driving a static plan. Adds Campaign/CampaignPlanEntry
models, CampaignDB table, nullable eval_runs.campaign_id, CampaignRepository,
Alembic migration, and POST/GET /api/campaigns with validation.

Ticket 01 of v0.6; no scheduling or child-run spawning yet (ADR-0003 v1).
2026-07-30 11:56:19 +08:00

64 lines
2.0 KiB
Python

"""API routes for evaluation campaigns (评估活动).
This ticket covers persistence and create/query only — no scheduling or child
Run spawning. Those arrive in later tickets.
"""
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlmodel import Session
from agenteval.models import Campaign, CampaignPlanEntry
from agenteval.storage.repository import CampaignRepository, ScenarioRepository, TargetRepository
from agenteval.web.deps import get_db
router = APIRouter()
class CreateCampaignRequest(BaseModel):
name: str
target_id: str
window_seconds: int = Field(gt=0)
time_scale: float = Field(default=1.0, gt=0)
plan: list[CampaignPlanEntry] = Field(min_length=1)
@router.get("")
async def list_campaigns(session: Session = Depends(get_db)) -> list[dict]:
return [c.model_dump() for c in CampaignRepository(session).list_all()]
@router.post("")
async def create_campaign(
request: CreateCampaignRequest,
session: Session = Depends(get_db),
) -> dict:
if not TargetRepository(session).get(request.target_id):
raise HTTPException(status_code=404, detail="target not found")
scenario_repo = ScenarioRepository(session)
for entry in request.plan:
if not scenario_repo.get(entry.scenario_id):
raise HTTPException(
status_code=404,
detail=f"scenario not found: {entry.scenario_id}",
)
campaign = Campaign(
name=request.name,
target_id=request.target_id,
window_seconds=request.window_seconds,
time_scale=request.time_scale,
plan=request.plan,
)
campaign = CampaignRepository(session).create(campaign)
return campaign.model_dump()
@router.get("/{campaign_id}")
async def get_campaign(campaign_id: str, session: Session = Depends(get_db)) -> dict:
campaign = CampaignRepository(session).get(campaign_id)
if not campaign:
raise HTTPException(status_code=404, detail="campaign not found")
return campaign.model_dump()