feat:接入sse

This commit is contained in:
2026-09-29 16:41:52 +08:00
parent 2b029b4bc7
commit e332259b07
4 changed files with 152 additions and 57 deletions
+38 -34
View File
@@ -1,12 +1,13 @@
"""异步执行与恢复大屏任务,当前使用单进程内存检查点"""
from contextlib import aclosing
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.observability.tracing import astream_screen
from app.schemas.agent import AgentResult, ResumeRequest
from app.schemas.request import DesignRequest
@@ -16,7 +17,7 @@ class AgentService:
self.graph = graph
self._active: set[str] = set()
async def run(self, request: DesignRequest) -> AgentResult:
def _prepare_run(self, request: DesignRequest):
# 新请求始终独立建任务,不用 conversation_id 充当检查点 ID
thread_id = str(uuid4())
# 暂无会话创建接口,未传会话 ID 时先分配标识,不创建数据库记录
@@ -24,7 +25,7 @@ class AgentService:
payload = request.model_dump(mode="json")
payload["context"]["conversation_id"] = conversation_id
return await self._invoke(
return (
thread_id,
{
"request": payload,
@@ -35,7 +36,13 @@ class AgentService:
conversation_id,
)
async def resume(self, request: ResumeRequest) -> AgentResult:
def stream_run(self, request: DesignRequest):
execution = self._prepare_run(request)
self._claim(execution[0])
return execution
async def prepare_resume(self, request: ResumeRequest):
# 在发送流响应头之前完成校验并占用任务
thread_id = str(request.thread_id)
# 检查和占用之间没有 await,同一事件循环内不会重复进入
self._claim(thread_id)
@@ -71,7 +78,7 @@ class AgentService:
)
# 每次有效补充或取消生成新的输入 ID,任务和会话保持不变
return await self._execute(
return (
thread_id,
Command(
resume=reply.model_dump(mode="json"),
@@ -79,41 +86,20 @@ class AgentService:
),
conversation_id,
)
finally:
self._active.discard(thread_id)
except BaseException:
self.release(thread_id)
raise
def release(self, thread_id):
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"),
)
@staticmethod
def _result(state, thread_id):
waiting = bool(state.get("__interrupt__"))
# 只交付校验通过的完整动作批次,失败草稿留在后端
@@ -129,3 +115,21 @@ class AgentService:
clarification=state.get("clarification") if waiting else None,
failure_reason=state.get("failure_reason"),
)
async def stream(self, thread_id, payload, session_id):
"""消费图事件,完整状态只用于整理最终结果,不直接暴露给客户端"""
state = None
with logger.contextualize(thread_id=thread_id):
async with aclosing(
astream_screen(
self.graph, payload, thread_id=thread_id, session_id=session_id
)
) as events:
async for mode, data in events:
if mode == "values":
state = data
elif data.get("type") == "node_progress" and data.get("label"):
yield "progress", data
if state is None:
raise RuntimeError("图未返回状态")
yield "final", self._result(state, thread_id).model_dump(mode="json")