feat:统一节点进度事件
This commit is contained in:
@@ -10,6 +10,7 @@ from pydantic import ValidationError
|
||||
if TYPE_CHECKING:
|
||||
from app.config import Settings
|
||||
from app.exceptions import AppError, ImportProcessError
|
||||
from app.processor.progress import node_progress
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
@@ -37,7 +38,8 @@ class BaseNode(ABC):
|
||||
start_time = time.perf_counter()
|
||||
self.logger.info("节点开始")
|
||||
try:
|
||||
result = self.process(state)
|
||||
with node_progress(self.name, state):
|
||||
result = self.process(state)
|
||||
except GraphInterrupt:
|
||||
raise
|
||||
except (AppError, ValidationError):
|
||||
@@ -61,7 +63,8 @@ class BaseNode(ABC):
|
||||
start_time = time.perf_counter()
|
||||
self.logger.info("节点开始")
|
||||
try:
|
||||
result = await self.process(state)
|
||||
with node_progress(self.name, state):
|
||||
result = await self.process(state)
|
||||
except GraphInterrupt:
|
||||
raise
|
||||
except (AppError, ValidationError):
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
"""通过 LangGraph custom 流发送进度,不额外保存任务状态"""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Mapping
|
||||
from contextlib import contextmanager
|
||||
from time import perf_counter
|
||||
from uuid import uuid4
|
||||
|
||||
from langgraph.config import get_config, get_stream_writer
|
||||
from langgraph.errors import GraphInterrupt
|
||||
|
||||
from app.schemas.node_progress import NodeProgress, NodeProgressStatus
|
||||
|
||||
NODE_PROGRESS_MESSAGES = {
|
||||
"requirement_node": "正在理解你的需求",
|
||||
"qa_node": "正在整理回答",
|
||||
"component_resource_node": "正在选择合适的图表和组件",
|
||||
"planner_node": "正在设计布局",
|
||||
"action_node": "正在准备画布修改",
|
||||
"validator_node": "正在检查修改结果",
|
||||
"reflection_node": "正在调整修改方案",
|
||||
}
|
||||
|
||||
|
||||
@contextmanager
|
||||
def node_progress(node: str, state):
|
||||
"""每次调用独立计时和编号,同步与异步入口共用"""
|
||||
try:
|
||||
config = get_config()
|
||||
except RuntimeError:
|
||||
# 独立调用节点没有图上下文,直接执行业务逻辑
|
||||
yield
|
||||
return
|
||||
|
||||
writer = get_stream_writer()
|
||||
values = state if isinstance(state, Mapping) else {}
|
||||
# 以实际检查点配置为准,兼容没有在 state 中填写 ID 的本地示例
|
||||
thread_id = config.get("configurable", {}).get("thread_id") or values.get(
|
||||
"thread_id"
|
||||
)
|
||||
execution_id = str(uuid4())
|
||||
started = perf_counter()
|
||||
|
||||
def emit(status: NodeProgressStatus):
|
||||
event = NodeProgress(
|
||||
thread_id=thread_id,
|
||||
message_id=values.get("message_id"),
|
||||
execution_id=execution_id,
|
||||
node=node,
|
||||
label=NODE_PROGRESS_MESSAGES.get(node),
|
||||
status=status,
|
||||
duration_ms=None
|
||||
if status == "started"
|
||||
else round((perf_counter() - started) * 1000, 3),
|
||||
)
|
||||
writer(event.model_dump(mode="json", exclude_none=True))
|
||||
|
||||
emit("started")
|
||||
try:
|
||||
yield
|
||||
except GraphInterrupt:
|
||||
# interrupt 是正常暂停,恢复后会产生新的执行 ID
|
||||
emit("waiting_for_input")
|
||||
raise
|
||||
except asyncio.CancelledError:
|
||||
# 协程被取消也要结束这次节点执行,继续向上传递取消信号
|
||||
emit("cancelled")
|
||||
raise
|
||||
except Exception:
|
||||
# 只通知失败,异常详情仍由原有日志和 API 异常处理负责
|
||||
emit("failed")
|
||||
raise
|
||||
else:
|
||||
# 校验不通过等业务结果仍属于节点正常执行完成
|
||||
emit("completed")
|
||||
@@ -16,6 +16,9 @@ from app.schemas.screen_plan import ScreenPlan
|
||||
|
||||
|
||||
class QueryState(TypedDict):
|
||||
conversation_id: NotRequired[str] # 当前聊天会话 ID
|
||||
thread_id: NotRequired[str] # 当前任务 ID
|
||||
message_id: NotRequired[str] # 本次触发执行的用户消息 ID,澄清恢复时更新
|
||||
request: JsonObject # 原始请求,保留提交时的内容
|
||||
input_context: NotRequired[InputContext] # 校验后的上下文,包含最新大屏和澄清历史
|
||||
intent_decision: NotRequired[DecisionResponse | None] # Jev 原始意图判断及分数
|
||||
|
||||
Reference in New Issue
Block a user