AgentEvalTool/backend/agenteval/storage/exploration_repository.py
sinohqb 7eae6de52d refactor(evaluation/storage): 结算统一与 repository 拆分(Phase 2 + 3)
合并两个不可分割的深化:

Phase 2 — 智能作业结算统一(ADR-0012)
- intelligence_jobs.execute(job_kind, campaign_id, ...) 作为结算的
  唯一实现:建行 → 认领 → 校验 → generating → 落账,一处编排、
  一处截断(500 字符)。两个 executor 退化为 ensure_queued /
  validate / work_fn 三个小 adapter。
- analysis.validate_analysis_request() 共享校验入口(活动终态 →
  模型),路由捕获映射 400、executor 捕获落 failed 行,与
  validate_comparison_request 先例同构。
- campaign_runner._auto_start_analysis 的跳过守卫收敛至
  auto_intelligence_eligible 单一判断点。
- comparison.py 删除零调用的 build_comparison_payload;
  load_comparison_view 投影归位至 campaign_read_model。
- 新增 characterization 测试(认领竞争、重复触发、截断、恢复上限)。

Phase 3 — storage/repository.py 拆分
- AsyncJobRepository 及两个子类迁至
  storage/async_job_repository.py(Phase 2 的 intelligence_jobs
  与 comparison 必须 import 自该路径,故与 Phase 2 同 commit)。
- ExplorationSession / ExplorationMessage 迁至
  storage/exploration_repository.py;repository.py 由 1180 行降至
  约 814 行,grep 确认无残留符号。
- exploration 子模块与路由 import 全部更新;测试 import 跟随。

刻意不做:CAS 共享原语、app.py 五 registry 关停顺序归一
(ADR-0006 精神,等真实需求出现再议)。
2026-08-24 05:50:27 +08:00

136 lines
5.0 KiB
Python

"""Repositories for virtual-user exploration sessions (探索式评测)."""
from typing import Optional
from sqlmodel import Session, select
from agenteval.exploration.models import ExplorationMessage, ExplorationSession, ExplorationSessionStatus
from agenteval.storage.db import ExplorationMessageDB, ExplorationSessionDB, get_session, utc_now
from agenteval.storage.repository import BaseRepository
class ExplorationSessionRepository(BaseRepository[ExplorationSession, ExplorationSessionDB]):
"""Repository for virtual-user exploration sessions (探索会话)."""
_table = ExplorationSessionDB
_order_by = "created_at"
def _copy_mutable(self, db: ExplorationSessionDB, session_obj: ExplorationSession) -> None:
db.goal = session_obj.goal
db.status = session_obj.status.value
db.triggered_by = session_obj.triggered_by.value
db.turn_count = session_obj.turn_count
db.error = session_obj.error
db.closed_at = session_obj.closed_at
db.set_persona(session_obj.persona)
if session_obj.seed_ref is not None:
db.set_seed_ref(session_obj.seed_ref)
if session_obj.experience is not None:
db.set_experience(session_obj.experience)
if session_obj.judge_review is not None:
db.set_judge_review(session_obj.judge_review)
def _to_db(self, session_obj: ExplorationSession) -> ExplorationSessionDB:
db = ExplorationSessionDB(
id=session_obj.id,
campaign_id=session_obj.campaign_id,
target_id=session_obj.target_id,
created_at=session_obj.created_at,
)
self._copy_mutable(db, session_obj)
return db
def _from_db(self, db: ExplorationSessionDB) -> ExplorationSession:
return ExplorationSession(
id=db.id,
campaign_id=db.campaign_id,
target_id=db.target_id,
persona=db.get_persona(),
goal=db.goal,
seed_ref=db.get_seed_ref(),
status=db.status,
triggered_by=db.triggered_by,
experience=db.get_experience(),
judge_review=db.get_judge_review(),
turn_count=db.turn_count,
error=db.error,
created_at=db.created_at,
closed_at=db.closed_at,
)
def update(self, session_obj: ExplorationSession) -> Optional[ExplorationSession]:
existing = self.session.get(ExplorationSessionDB, session_obj.id)
if not existing:
return None
self._copy_mutable(existing, session_obj)
self.session.add(existing)
self.session.commit()
self.session.refresh(existing)
return self._from_db(existing)
def list_by_campaign(self, campaign_id: str) -> list[ExplorationSession]:
statement = (
select(ExplorationSessionDB)
.where(ExplorationSessionDB.campaign_id == campaign_id)
.order_by(ExplorationSessionDB.created_at)
)
return [self._from_db(r) for r in self.session.exec(statement).all()]
def expire_running_sessions(self, campaign_id: str) -> int:
"""Expire every still-running exploration session of a finalized campaign.
Returns the number of sessions expired. Completed/failed sessions keep
their evidence untouched.
"""
expired = 0
for session_obj in self.list_by_campaign(campaign_id):
if session_obj.status != ExplorationSessionStatus.RUNNING:
continue
session_obj.status = ExplorationSessionStatus.EXPIRED
session_obj.closed_at = utc_now()
self.update(session_obj)
expired += 1
return expired
class ExplorationMessageRepository:
"""Append-only repository for exploration session chat rows."""
def __init__(self, session: Optional[Session] = None):
self.session = session or get_session()
def save_message(self, message: ExplorationMessage) -> ExplorationMessage:
db = ExplorationMessageDB(
id=message.id,
session_id=message.session_id,
round_index=message.round_index,
role=message.role,
content=message.content,
latency_ms=message.latency_ms,
created_at=message.created_at,
)
self.session.add(db)
self.session.commit()
self.session.refresh(db)
message.id = db.id
return message
def list_by_session(self, session_id: str) -> list[ExplorationMessage]:
statement = (
select(ExplorationMessageDB)
.where(ExplorationMessageDB.session_id == session_id)
.order_by(ExplorationMessageDB.created_at)
)
return [
ExplorationMessage(
id=r.id,
session_id=r.session_id,
round_index=r.round_index,
role=r.role,
content=r.content,
latency_ms=r.latency_ms,
created_at=r.created_at,
)
for r in self.session.exec(statement).all()
]