AgentEvalTool/backend/agenteval/web/routers/exploration.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

123 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""API routes for exploratory evaluation sessions (探索式评测, v0.9).
领域逻辑(账本规则、状态机、巡检)在 exploration/lifecycle.py 与
patrol.py本层只做 HTTP 翻译:解析请求、调用领域操作、把领域异常
映射为状态码NotFound→404、Guardrail→409、Channel→502。预算违规
与状态违规的拒绝文案由领域层给出,拒绝本身就是对常驻智能体的反馈。
"""
from typing import Any
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlmodel import Session
from agenteval.exploration import lifecycle, patrol
from agenteval.exploration.errors import (
ExplorationChannelError,
ExplorationGuardrailError,
ExplorationNotFoundError,
)
from agenteval.exploration.models import ExplorationTrigger
from agenteval.storage.exploration_repository import ExplorationMessageRepository, ExplorationSessionRepository
from agenteval.storage.repository import CampaignRepository
from agenteval.web.deps import get_db
router = APIRouter()
class CreateSessionRequest(BaseModel):
campaign_id: str
persona: dict[str, Any]
goal: str = Field(min_length=1)
seed_ref: dict[str, Any] | None = None
triggered_by: ExplorationTrigger = ExplorationTrigger.AUTO
class SendMessageRequest(BaseModel):
content: str = Field(min_length=1)
class CloseSessionRequest(BaseModel):
experience: dict[str, Any]
def _translate(exc: Exception) -> HTTPException:
if isinstance(exc, ExplorationNotFoundError):
return HTTPException(status_code=404, detail=str(exc))
if isinstance(exc, ExplorationGuardrailError):
return HTTPException(status_code=409, detail=exc.reason)
return HTTPException(status_code=502, detail=exc.reason)
@router.get("/patrol")
async def patrol_endpoint(session: Session = Depends(get_db)) -> dict:
"""Stateless patrol for the resident agent — see exploration/patrol.py."""
return patrol.patrol_report(session)
@router.post("/sessions")
async def create_session(
request: CreateSessionRequest,
session: Session = Depends(get_db),
) -> dict:
try:
session_obj = lifecycle.open_session(
session,
campaign_id=request.campaign_id,
persona=request.persona,
goal=request.goal,
seed_ref=request.seed_ref,
triggered_by=request.triggered_by,
)
except (ExplorationNotFoundError, ExplorationGuardrailError) as exc:
raise _translate(exc) from exc
return session_obj.model_dump(mode="json")
@router.get("/campaigns/{campaign_id}/sessions")
async def list_campaign_sessions(
campaign_id: str,
session: Session = Depends(get_db),
) -> dict:
if not CampaignRepository(session).get(campaign_id):
raise HTTPException(status_code=404, detail="campaign not found")
sessions = ExplorationSessionRepository(session).list_by_campaign(campaign_id)
return {"sessions": [s.model_dump(mode="json") for s in sessions]}
@router.get("/sessions/{session_id}/messages")
async def list_session_messages(
session_id: str,
session: Session = Depends(get_db),
) -> dict:
if not ExplorationSessionRepository(session).get(session_id):
raise HTTPException(status_code=404, detail="exploration session not found")
messages = ExplorationMessageRepository(session).list_by_session(session_id)
return {"messages": [m.model_dump(mode="json") for m in messages]}
@router.post("/sessions/{session_id}/messages")
async def send_session_message(
session_id: str,
request: SendMessageRequest,
session: Session = Depends(get_db),
) -> dict:
try:
return await lifecycle.conduct_turn(session, session_id=session_id, content=request.content)
except (ExplorationNotFoundError, ExplorationGuardrailError, ExplorationChannelError) as exc:
raise _translate(exc) from exc
@router.post("/sessions/{session_id}/close")
async def close_session(
session_id: str,
request: CloseSessionRequest,
session: Session = Depends(get_db),
) -> dict:
try:
updated = lifecycle.close_session(session, session_id=session_id, experience=request.experience)
except (ExplorationNotFoundError, ExplorationGuardrailError) as exc:
raise _translate(exc) from exc
return updated.model_dump(mode="json")