238 lines
8.4 KiB
Python
238 lines
8.4 KiB
Python
"""API routes for intelligent evaluation (智能评估).
|
||
|
||
领域逻辑(状态机)在 intelligent_eval/lifecycle.py;本层只做 HTTP 翻译:
|
||
NotFound→404、TransitionError→409。
|
||
"""
|
||
|
||
from typing import Any
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException
|
||
from fastapi.responses import PlainTextResponse
|
||
from pydantic import BaseModel, Field
|
||
from sqlmodel import Session
|
||
|
||
from agenteval.intelligent_eval import lifecycle
|
||
from agenteval.intelligent_eval.lifecycle import (
|
||
IntelligentEvalChannelError,
|
||
IntelligentEvalNotFoundError,
|
||
IntelligentEvalTransitionError,
|
||
)
|
||
from agenteval.intelligent_eval.read_model import IntelligentEvalReadModel
|
||
from agenteval.intelligent_eval.report import ReportModel, render_report_markdown
|
||
from agenteval.web.deps import get_db
|
||
|
||
router = APIRouter()
|
||
|
||
|
||
class CreateEvalRequest(BaseModel):
|
||
name: str = Field(min_length=1)
|
||
target_id: str = Field(min_length=1)
|
||
goal: str = Field(min_length=1)
|
||
seeds: dict[str, Any] = Field(default_factory=dict)
|
||
intent: str = ""
|
||
role_description: str = ""
|
||
time_window_hours: int = Field(default=24, ge=1)
|
||
|
||
|
||
class SubmitPlanRequest(BaseModel):
|
||
plan: dict[str, Any]
|
||
|
||
|
||
class RejectRequest(BaseModel):
|
||
feedback: str = Field(min_length=1)
|
||
|
||
|
||
class CreateSessionRequest(BaseModel):
|
||
persona: dict[str, Any] = Field(default_factory=dict)
|
||
goal: str = Field(min_length=1)
|
||
dimension: str | None = None
|
||
|
||
|
||
class SendMessageRequest(BaseModel):
|
||
content: str = Field(min_length=1)
|
||
|
||
|
||
class CloseSessionRequest(BaseModel):
|
||
verdict: dict[str, Any]
|
||
|
||
|
||
class SubmitReportRequest(BaseModel):
|
||
report: ReportModel
|
||
|
||
|
||
def _translate(exc: Exception) -> HTTPException:
|
||
if isinstance(exc, IntelligentEvalNotFoundError):
|
||
return HTTPException(status_code=404, detail=str(exc))
|
||
if isinstance(exc, IntelligentEvalChannelError):
|
||
return HTTPException(status_code=502, detail=str(exc))
|
||
return HTTPException(status_code=409, detail=exc.reason)
|
||
|
||
|
||
def _eval_response(ev, session: Session) -> dict:
|
||
"""Serialize one stable intelligent-evaluation read projection."""
|
||
|
||
projection = IntelligentEvalReadModel(session).list_item(ev)
|
||
return projection.model_dump(mode="json")
|
||
|
||
|
||
@router.post("")
|
||
async def create_eval(request: CreateEvalRequest, session: Session = Depends(get_db)) -> dict:
|
||
try:
|
||
ev = lifecycle.create_eval(
|
||
session,
|
||
name=request.name,
|
||
target_id=request.target_id,
|
||
goal=request.goal,
|
||
seeds=request.seeds,
|
||
intent=request.intent,
|
||
role_description=request.role_description,
|
||
time_window_hours=request.time_window_hours,
|
||
)
|
||
except IntelligentEvalNotFoundError as exc:
|
||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||
return _eval_response(ev, session)
|
||
|
||
|
||
@router.get("")
|
||
async def list_evals(session: Session = Depends(get_db)) -> dict:
|
||
evals = lifecycle.list_evals(session)
|
||
reader = IntelligentEvalReadModel(session)
|
||
return {"intelligent_evals": [item.model_dump(mode="json") for item in reader.list_items(evals)]}
|
||
|
||
|
||
@router.get("/{eval_id}")
|
||
async def get_eval(eval_id: str, session: Session = Depends(get_db)) -> dict:
|
||
projection = IntelligentEvalReadModel(session).detail_by_id(eval_id)
|
||
if projection is None:
|
||
raise HTTPException(status_code=404, detail=f"intelligent eval {eval_id} not found")
|
||
return projection.model_dump(mode="json")
|
||
|
||
|
||
@router.put("/{eval_id}/plan")
|
||
async def submit_plan(eval_id: str, request: SubmitPlanRequest, session: Session = Depends(get_db)) -> dict:
|
||
try:
|
||
ev = lifecycle.submit_plan(session, eval_id, request.plan)
|
||
except (IntelligentEvalNotFoundError, IntelligentEvalTransitionError) as exc:
|
||
raise _translate(exc) from exc
|
||
return _eval_response(ev, session)
|
||
|
||
|
||
@router.post("/{eval_id}/approve")
|
||
async def approve(eval_id: str, session: Session = Depends(get_db)) -> dict:
|
||
try:
|
||
ev = lifecycle.approve(session, eval_id)
|
||
except (IntelligentEvalNotFoundError, IntelligentEvalTransitionError) as exc:
|
||
raise _translate(exc) from exc
|
||
return _eval_response(ev, session)
|
||
|
||
|
||
@router.post("/{eval_id}/reject")
|
||
async def reject(eval_id: str, request: RejectRequest, session: Session = Depends(get_db)) -> dict:
|
||
try:
|
||
ev = lifecycle.reject(session, eval_id, request.feedback)
|
||
except (IntelligentEvalNotFoundError, IntelligentEvalTransitionError) as exc:
|
||
raise _translate(exc) from exc
|
||
return _eval_response(ev, session)
|
||
|
||
|
||
@router.post("/{eval_id}/cancel")
|
||
async def cancel(eval_id: str, session: Session = Depends(get_db)) -> dict:
|
||
try:
|
||
ev = lifecycle.cancel(session, eval_id)
|
||
except (IntelligentEvalNotFoundError, IntelligentEvalTransitionError) as exc:
|
||
raise _translate(exc) from exc
|
||
return _eval_response(ev, session)
|
||
|
||
|
||
@router.put("/{eval_id}/report")
|
||
async def submit_report(eval_id: str, request: SubmitReportRequest, session: Session = Depends(get_db)) -> dict:
|
||
try:
|
||
ev = lifecycle.submit_report(session, eval_id, request.report.model_dump())
|
||
except (IntelligentEvalNotFoundError, IntelligentEvalTransitionError) as exc:
|
||
raise _translate(exc) from exc
|
||
return _eval_response(ev, session)
|
||
|
||
|
||
@router.get("/{eval_id}/report")
|
||
async def get_report(eval_id: str, session: Session = Depends(get_db)) -> dict:
|
||
report = IntelligentEvalReadModel(session).report_by_eval(eval_id)
|
||
if report is None:
|
||
raise HTTPException(status_code=404, detail="report not submitted yet")
|
||
return report[1]
|
||
|
||
|
||
@router.get("/{eval_id}/report/markdown", response_class=PlainTextResponse)
|
||
async def get_report_markdown(eval_id: str, session: Session = Depends(get_db)) -> PlainTextResponse:
|
||
report = IntelligentEvalReadModel(session).report_by_eval(eval_id)
|
||
if report is None:
|
||
raise HTTPException(status_code=404, detail="report not submitted yet")
|
||
name, payload = report
|
||
markdown = render_report_markdown(payload, name=name, eval_id=eval_id)
|
||
return PlainTextResponse(markdown, media_type="text/markdown; charset=utf-8")
|
||
|
||
|
||
@router.post("/{eval_id}/sessions")
|
||
async def create_session(eval_id: str, request: CreateSessionRequest, session: Session = Depends(get_db)) -> dict:
|
||
try:
|
||
obj = lifecycle.open_session(
|
||
session,
|
||
eval_id=eval_id,
|
||
persona=request.persona,
|
||
goal=request.goal,
|
||
dimension=request.dimension,
|
||
)
|
||
except (IntelligentEvalNotFoundError, IntelligentEvalTransitionError) as exc:
|
||
raise _translate(exc) from exc
|
||
return obj.model_dump(mode="json")
|
||
|
||
|
||
@router.get("/{eval_id}/sessions")
|
||
async def list_sessions(eval_id: str, session: Session = Depends(get_db)) -> dict:
|
||
sessions = IntelligentEvalReadModel(session).sessions_by_eval(eval_id)
|
||
if sessions is None:
|
||
raise HTTPException(status_code=404, detail=f"intelligent eval {eval_id} not found")
|
||
return {"sessions": [s.model_dump(mode="json") for s in sessions]}
|
||
|
||
|
||
@router.post("/{eval_id}/sessions/{session_id}/messages")
|
||
async def send_message(
|
||
eval_id: str, session_id: str, request: SendMessageRequest, session: Session = Depends(get_db)
|
||
) -> dict:
|
||
try:
|
||
return await lifecycle.conduct_turn(
|
||
session,
|
||
eval_id=eval_id,
|
||
session_id=session_id,
|
||
content=request.content,
|
||
)
|
||
except (
|
||
IntelligentEvalNotFoundError,
|
||
IntelligentEvalTransitionError,
|
||
IntelligentEvalChannelError,
|
||
) as exc:
|
||
raise _translate(exc) from exc
|
||
|
||
|
||
@router.post("/{eval_id}/sessions/{session_id}/close")
|
||
async def close_session(
|
||
eval_id: str, session_id: str, request: CloseSessionRequest, session: Session = Depends(get_db)
|
||
) -> dict:
|
||
try:
|
||
obj = lifecycle.close_session(
|
||
session,
|
||
eval_id=eval_id,
|
||
session_id=session_id,
|
||
verdict=request.verdict,
|
||
)
|
||
except (IntelligentEvalNotFoundError, IntelligentEvalTransitionError) as exc:
|
||
raise _translate(exc) from exc
|
||
return obj.model_dump(mode="json")
|
||
|
||
|
||
@router.get("/{eval_id}/sessions/{session_id}/messages")
|
||
async def list_messages(eval_id: str, session_id: str, session: Session = Depends(get_db)) -> dict:
|
||
messages = IntelligentEvalReadModel(session).messages_by_session(eval_id, session_id)
|
||
if messages is None:
|
||
raise HTTPException(status_code=404, detail=f"intelligent eval session {session_id} not found")
|
||
return {"messages": [m.model_dump(mode="json") for m in messages]}
|