Some checks failed
CI / test (push) Failing after 33s
架构保养候选 3:探索生命周期的账本规则与状态机从 HTTP 层落入 exploration/lifecycle.py(open/conduct/close)与 patrol.py(巡检读模型), 违规改用类型化领域异常(NotFound/Guardrail/Channel),router 瘦回纯 HTTP 翻译(404/409/502 映射),领域层不再依赖 fastapi,可脱离 TestClient 直测(新增 12 个单元测试)。
101 lines
3.8 KiB
Python
101 lines
3.8 KiB
Python
"""Exploration patrol (探索巡检).
|
||
|
||
常驻智能体的无状态巡检:筛出参与探索的正式线活动(running、
|
||
time_scale == 1、有种子集),汇报水位以来的新增结果与剩余探索预算,
|
||
构建完响应后推进水位,保证下次只报增量。
|
||
"""
|
||
|
||
from datetime import datetime
|
||
from typing import Any
|
||
|
||
from sqlmodel import Session
|
||
|
||
from agenteval.evaluation.report import generate_campaign_report
|
||
from agenteval.exploration.models import resolve_budget
|
||
from agenteval.models import Campaign, CampaignStatus, EvalRun
|
||
from agenteval.storage.db import as_utc, iso_utc, utc_now
|
||
from agenteval.storage.repository import (
|
||
CampaignRepository,
|
||
ExplorationSessionRepository,
|
||
RunRepository,
|
||
ScenarioRepository,
|
||
TargetRepository,
|
||
)
|
||
|
||
|
||
def _new_runs_since(runs: list[EvalRun], watermark: datetime | None) -> list[EvalRun]:
|
||
fresh = []
|
||
for run in runs:
|
||
if run.completed_at is None:
|
||
continue
|
||
if watermark is not None and as_utc(run.completed_at) <= watermark:
|
||
continue
|
||
fresh.append(run)
|
||
return fresh
|
||
|
||
|
||
def patrol_report(db_session: Session) -> dict[str, Any]:
|
||
"""一次巡检的完整读模型:patrolled_at + 逐活动条目列表。
|
||
|
||
水位取构建响应之后的时刻:查询与持久化之间完成的结果不会在下次重复上报。
|
||
"""
|
||
campaign_repo = CampaignRepository(db_session)
|
||
run_repo = RunRepository(db_session)
|
||
exploration_repo = ExplorationSessionRepository(db_session)
|
||
scenario_names = ScenarioRepository(db_session).name_map()
|
||
target_names = {t.id: t.name for t in TargetRepository(db_session).list_all()}
|
||
|
||
patrolled_at = utc_now()
|
||
entries: list[dict[str, Any]] = []
|
||
patrolled_campaigns: list[Campaign] = []
|
||
for campaign in campaign_repo.list_all():
|
||
if campaign.status != CampaignStatus.RUNNING:
|
||
continue
|
||
if campaign.time_scale != 1:
|
||
continue
|
||
if campaign.exploration_seeds is None:
|
||
continue
|
||
|
||
watermark = as_utc(campaign.last_patrolled_at) if campaign.last_patrolled_at else None
|
||
fresh = _new_runs_since(run_repo.list_by_campaign(campaign.id), watermark)
|
||
new_results = None
|
||
if fresh:
|
||
report = generate_campaign_report(campaign, fresh, scenario_names=scenario_names)
|
||
new_results = {
|
||
"summary": report["summary"],
|
||
"capability_summary": report["capability_summary"],
|
||
}
|
||
|
||
budget = resolve_budget(campaign)
|
||
sessions = exploration_repo.list_by_campaign(campaign.id)
|
||
seconds_since_last_session = None
|
||
if sessions:
|
||
latest = max(as_utc(s.created_at) for s in sessions if s.created_at)
|
||
seconds_since_last_session = int((patrolled_at - latest).total_seconds())
|
||
|
||
entries.append(
|
||
{
|
||
"campaign_id": campaign.id,
|
||
"campaign_name": campaign.name,
|
||
"target_id": campaign.target_id,
|
||
"target_name": target_names.get(campaign.target_id),
|
||
"last_patrolled_at": iso_utc(campaign.last_patrolled_at),
|
||
"new_results": new_results,
|
||
"budget": {
|
||
"max_sessions": budget.max_sessions,
|
||
"sessions_used": len(sessions),
|
||
"remaining_sessions": max(0, budget.max_sessions - len(sessions)),
|
||
"max_turns": budget.max_turns,
|
||
"min_interval_seconds": budget.min_interval_seconds,
|
||
"seconds_since_last_session": seconds_since_last_session,
|
||
},
|
||
}
|
||
)
|
||
patrolled_campaigns.append(campaign)
|
||
|
||
watermark_at = utc_now()
|
||
for campaign in patrolled_campaigns:
|
||
campaign_repo.touch_patrol_watermark(campaign.id, watermark_at)
|
||
|
||
return {"patrolled_at": iso_utc(watermark_at), "campaigns": entries}
|