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 个单元测试)。
126 lines
4.2 KiB
Python
126 lines
4.2 KiB
Python
"""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.repository import (
|
||
CampaignRepository,
|
||
ExplorationMessageRepository,
|
||
ExplorationSessionRepository,
|
||
)
|
||
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")
|