"""Intelligent eval lifecycle (状态机 + 领域操作). 状态机: draft → planning → pending_approval → executing → completed → cancelled → failed pending_approval 可打回 → planning(附反馈) executing 可取消 → cancelled 非法转换抛 IntelligentEvalTransitionError,路由层映射为 409。 """ import json from typing import Any from sqlmodel import Session from agenteval.channels.factory import ChannelFactory from agenteval.config import get_settings from agenteval.intelligent_eval.models import ( IntelligentEval, IntelligentEvalMessage, IntelligentEvalSession, IntelligentEvalSessionStatus, IntelligentEvalStatus, ) from agenteval.intelligent_eval.repository import ( IntelligentEvalMessageRepository, IntelligentEvalRepository, IntelligentEvalSessionRepository, ) from agenteval.storage.db import utc_now from agenteval.storage.repository import TargetRepository # 合法转换表:当前状态 → 允许的目标状态集合 _TRANSITIONS: dict[IntelligentEvalStatus, set[IntelligentEvalStatus]] = { IntelligentEvalStatus.DRAFT: {IntelligentEvalStatus.PLANNING}, IntelligentEvalStatus.PLANNING: {IntelligentEvalStatus.PENDING_APPROVAL}, IntelligentEvalStatus.PENDING_APPROVAL: {IntelligentEvalStatus.EXECUTING, IntelligentEvalStatus.PLANNING, IntelligentEvalStatus.CANCELLED}, IntelligentEvalStatus.EXECUTING: {IntelligentEvalStatus.COMPLETED, IntelligentEvalStatus.CANCELLED, IntelligentEvalStatus.FAILED}, IntelligentEvalStatus.COMPLETED: set(), IntelligentEvalStatus.CANCELLED: set(), IntelligentEvalStatus.FAILED: set(), } class IntelligentEvalNotFoundError(Exception): pass class IntelligentEvalTransitionError(Exception): def __init__(self, reason: str): self.reason = reason super().__init__(reason) class IntelligentEvalChannelError(Exception): pass def coerce_reply_text(content: Any) -> str: """Flatten a reply payload to text (tutu returns msgBody as a parsed object).""" if isinstance(content, str): return content if isinstance(content, dict): for key in ("content", "text", "message"): value = content.get(key) if isinstance(value, str) and value: return value if content is None: return "" return json.dumps(content, ensure_ascii=False) def _get_or_raise(repo: IntelligentEvalRepository, eval_id: str) -> IntelligentEval: ev = repo.get(eval_id) if ev is None: raise IntelligentEvalNotFoundError(f"intelligent eval {eval_id} not found") return ev def _transition(repo: IntelligentEvalRepository, ev: IntelligentEval, target: IntelligentEvalStatus) -> IntelligentEval: allowed = _TRANSITIONS.get(ev.status, set()) if target not in allowed: raise IntelligentEvalTransitionError( f"cannot transition from {ev.status.value} to {target.value}" ) return repo.transition_status(ev.id, target) def create_eval( session: Session, *, name: str, target_id: str, goal: str, seeds: dict[str, Any], intent: str, role_description: str, time_window_hours: int = 24, ) -> IntelligentEval: """创建智能评估并直接进入 planning 状态(draft → planning 一步完成)。""" repo = IntelligentEvalRepository(session) ev = repo.create(IntelligentEval( name=name, target_id=target_id, status=IntelligentEvalStatus.DRAFT, goal=goal, seeds=seeds, intent=intent, role_description=role_description, time_window_hours=time_window_hours, )) return _transition(repo, ev, IntelligentEvalStatus.PLANNING) def submit_plan(session: Session, eval_id: str, plan: dict[str, Any]) -> IntelligentEval: """OpenClaw 提交粗计划:planning → pending_approval。""" repo = IntelligentEvalRepository(session) ev = _get_or_raise(repo, eval_id) ev.plan = plan ev.plan_feedback = None ev = repo.update(ev) return _transition(repo, ev, IntelligentEvalStatus.PENDING_APPROVAL) def approve(session: Session, eval_id: str) -> IntelligentEval: """用户批准:pending_approval → executing。""" repo = IntelligentEvalRepository(session) ev = _get_or_raise(repo, eval_id) return _transition(repo, ev, IntelligentEvalStatus.EXECUTING) def reject(session: Session, eval_id: str, feedback: str) -> IntelligentEval: """用户打回:pending_approval → planning(附反馈)。""" repo = IntelligentEvalRepository(session) ev = _get_or_raise(repo, eval_id) ev.plan_feedback = feedback ev = repo.update(ev) return _transition(repo, ev, IntelligentEvalStatus.PLANNING) def cancel(session: Session, eval_id: str) -> IntelligentEval: """用户取消:pending_approval / executing → cancelled。""" repo = IntelligentEvalRepository(session) ev = _get_or_raise(repo, eval_id) return _transition(repo, ev, IntelligentEvalStatus.CANCELLED) def submit_report(session: Session, eval_id: str, report: dict[str, Any]) -> IntelligentEval: """OpenClaw 提交结构化报告:executing → completed。 报告结构校验在路由层(pydantic),此处只负责落库与状态迁移。 """ repo = IntelligentEvalRepository(session) ev = _get_or_raise(repo, eval_id) ev.report = report ev = repo.update(ev) return _transition(repo, ev, IntelligentEvalStatus.COMPLETED) def get_eval(session: Session, eval_id: str) -> IntelligentEval: repo = IntelligentEvalRepository(session) return _get_or_raise(repo, eval_id) def list_evals(session: Session) -> list[IntelligentEval]: return IntelligentEvalRepository(session).list_all() def _get_session_or_raise(repo: IntelligentEvalSessionRepository, session_id: str) -> IntelligentEvalSession: obj = repo.get(session_id) if obj is None: raise IntelligentEvalNotFoundError(f"intelligent eval session {session_id} not found") return obj def open_session( session: Session, *, eval_id: str, persona: dict[str, Any], goal: str, dimension: str | None = None, ) -> IntelligentEvalSession: """创建虚拟用户会话:仅 executing 状态的评估可创建。""" eval_repo = IntelligentEvalRepository(session) ev = _get_or_raise(eval_repo, eval_id) if ev.status != IntelligentEvalStatus.EXECUTING: raise IntelligentEvalTransitionError( f"评估不在执行中(当前 {ev.status.value}),无法创建会话" ) repo = IntelligentEvalSessionRepository(session) return repo.create(IntelligentEvalSession( eval_id=ev.id, target_id=ev.target_id, persona=persona, goal=goal, dimension=dimension, )) async def conduct_turn(session: Session, *, session_id: str, content: str) -> dict[str, Any]: """一轮完整问答:状态检查 → 通道往返 → 双条消息落库 → 轮次自增。""" repo = IntelligentEvalSessionRepository(session) obj = _get_session_or_raise(repo, session_id) if obj.status != IntelligentEvalSessionStatus.RUNNING: raise IntelligentEvalTransitionError("会话不在进行中,拒收消息") target = TargetRepository(session).get(obj.target_id) if not target: raise IntelligentEvalNotFoundError("session target not found") channel = ChannelFactory.create(target) sent_at = utc_now() try: send_result = await channel.send(content) except Exception as exc: # channel adapters raise transport-specific errors raise IntelligentEvalChannelError(f"评测对象通道发送失败: {exc}") from exc if not send_result.ok: raise IntelligentEvalChannelError(f"评测对象通道发送失败: {send_result.error}") message_repo = IntelligentEvalMessageRepository(session) message_repo.create(IntelligentEvalMessage( session_id=obj.id, role="user", content=content, created_at=sent_at )) repo.increment_turns(obj.id) try: reply = await channel.poll_reply( send_result.question_msg_id, timeout=get_settings().poll_reply_timeout, ) except Exception as exc: raise IntelligentEvalChannelError(f"等待评测对象回复失败: {exc}") from exc if reply is None: raise IntelligentEvalChannelError("等待评测对象回复超时") received_at = utc_now() latency_ms = int((received_at - sent_at).total_seconds() * 1000) reply_text = coerce_reply_text(reply.content) message_repo.create(IntelligentEvalMessage( session_id=obj.id, role="assistant", content=reply_text, latency_ms=latency_ms, created_at=received_at, )) return {"reply": reply_text, "latency_ms": latency_ms, "turn_count": obj.turn_count + 1} def close_session(session: Session, *, session_id: str, verdict: dict[str, Any]) -> IntelligentEvalSession: """关闭会话并记录结论(verdict);仅 running 会话可关闭。""" repo = IntelligentEvalSessionRepository(session) obj = _get_session_or_raise(repo, session_id) if obj.status != IntelligentEvalSessionStatus.RUNNING: raise IntelligentEvalTransitionError("会话不在进行中,无法关闭") closed = repo.close(obj.id, verdict) if closed is None: raise IntelligentEvalNotFoundError(f"intelligent eval session {session_id} not found") return closed def get_session_by_id(session: Session, session_id: str) -> IntelligentEvalSession: return _get_session_or_raise(IntelligentEvalSessionRepository(session), session_id) def list_sessions(session: Session, eval_id: str) -> list[IntelligentEvalSession]: eval_repo = IntelligentEvalRepository(session) _get_or_raise(eval_repo, eval_id) return IntelligentEvalSessionRepository(session).list_by_eval(eval_id) def list_messages(session: Session, session_id: str) -> list[IntelligentEvalMessage]: _get_session_or_raise(IntelligentEvalSessionRepository(session), session_id) return IntelligentEvalMessageRepository(session).list_by_session(session_id)