"""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}