103 lines
4.1 KiB
Python
103 lines
4.1 KiB
Python
"""异步执行与恢复大屏任务,当前使用单进程内存检查点"""
|
|
|
|
from uuid import uuid4
|
|
|
|
from langgraph.types import Command
|
|
from loguru import logger
|
|
|
|
from app.core.exceptions import BizException
|
|
from app.observability.tracing import ainvoke_screen
|
|
from app.schemas.agent import AgentResult, ResumeRequest
|
|
from app.schemas.request import DesignRequest
|
|
|
|
|
|
class AgentService:
|
|
def __init__(self, graph):
|
|
self.graph = graph
|
|
self._active: set[str] = set()
|
|
|
|
async def run(self, request: DesignRequest) -> AgentResult:
|
|
# 新请求始终独立建任务,不用 conversation_id 充当检查点 ID
|
|
thread_id = str(uuid4())
|
|
return await self._invoke(
|
|
thread_id,
|
|
{"request": request.model_dump(mode="json")},
|
|
request.context.conversation_id,
|
|
)
|
|
|
|
async def resume(self, request: ResumeRequest) -> AgentResult:
|
|
thread_id = str(request.thread_id)
|
|
# 检查和占用之间没有 await,同一事件循环内不会重复进入
|
|
self._claim(thread_id)
|
|
try:
|
|
snapshot = await self.graph.aget_state(
|
|
{"configurable": {"thread_id": thread_id}}
|
|
)
|
|
if not snapshot.values:
|
|
raise BizException(404, "任务不存在或已过期")
|
|
if not any(task.interrupts for task in snapshot.tasks):
|
|
raise BizException(409, "任务当前未等待用户补充")
|
|
# 恢复前校验选项,避免无效回答消耗一次 interrupt 恢复
|
|
clarification = snapshot.values["clarification"]
|
|
reply = request.reply
|
|
if (
|
|
not reply.cancel
|
|
and reply.option_id is not None
|
|
and reply.option_id not in {item.id for item in clarification.options}
|
|
):
|
|
raise BizException(422, "请选择当前问题的有效选项")
|
|
session_id = (
|
|
snapshot.values["request"].get("context", {}).get("conversation_id")
|
|
)
|
|
return await self._execute(
|
|
thread_id, Command(resume=reply.model_dump(mode="json")), session_id
|
|
)
|
|
finally:
|
|
self._active.discard(thread_id)
|
|
|
|
def _claim(self, thread_id: str) -> None:
|
|
if thread_id in self._active:
|
|
raise BizException(409, "任务正在执行,请勿重复提交")
|
|
self._active.add(thread_id)
|
|
|
|
async def _invoke(self, thread_id, payload, session_id) -> AgentResult:
|
|
self._claim(thread_id)
|
|
try:
|
|
return await self._execute(thread_id, payload, session_id)
|
|
finally:
|
|
# 异常和取消也必须释放占用,不阻塞后续请求
|
|
self._active.discard(thread_id)
|
|
|
|
async def _execute(self, thread_id, payload, session_id) -> AgentResult:
|
|
# 上下文只作用于当前异步任务,节点自动继承任务标识
|
|
with logger.contextualize(thread_id=thread_id):
|
|
logger.info("任务执行开始")
|
|
try:
|
|
state = await ainvoke_screen(
|
|
self.graph, payload, thread_id=thread_id, session_id=session_id
|
|
)
|
|
except Exception as exc:
|
|
# API 异常处理发生在上下文退出后,保留任务标识用于定位
|
|
exc._agent_thread_id = thread_id
|
|
raise
|
|
logger.info(
|
|
"任务执行结束,状态 {}",
|
|
"waiting_for_input"
|
|
if state.get("__interrupt__")
|
|
else state.get("status"),
|
|
)
|
|
|
|
waiting = bool(state.get("__interrupt__"))
|
|
|
|
# 只交付校验通过的完整动作批次,失败草稿留在后端
|
|
return AgentResult(
|
|
thread_id=thread_id,
|
|
status="waiting_for_input" if waiting else state["status"],
|
|
actions=state.get("actions") or []
|
|
if state.get("validation_passed") is True and state["status"] == "completed"
|
|
else [],
|
|
answer=state.get("answer"),
|
|
clarification=state.get("clarification") if waiting else None,
|
|
failure_reason=state.get("failure_reason"),
|
|
)
|