[add] workflow support stream mode

This commit is contained in:
Mark
2025-12-18 19:46:36 +08:00
committed by 谢俊男
parent 9e48f2143e
commit 3aff6baccb
6 changed files with 282 additions and 144 deletions

View File

@@ -574,7 +574,7 @@ async def draft_run(
# 3. 流式返回 # 3. 流式返回
if payload.stream: if payload.stream:
logger.debug( logger.debug(
"开始多智能体流式试运行", "开始工作流流式试运行",
extra={ extra={
"app_id": str(app_id), "app_id": str(app_id),
"message_length": len(payload.message), "message_length": len(payload.message),
@@ -583,16 +583,13 @@ async def draft_run(
) )
async def event_generator(): async def event_generator():
"""多智能体流式事件生成器""" """工作流事件生成器"""
multiservice = MultiAgentService(db)
# 调用多智能体服务的流式方法 # 调用多智能体服务的流式方法
async for event in multiservice.run_stream( async for event in workflow_service.run_stream(
app_id=app_id, app_id=app_id,
request=multi_agent_request, payload=payload,
storage_type=storage_type, config=config
user_rag_memory_id=user_rag_memory_id
): ):
yield event yield event

View File

@@ -11,16 +11,14 @@ from typing import Any
from langchain_core.messages import HumanMessage from langchain_core.messages import HumanMessage
from langgraph.graph import StateGraph, START, END from langgraph.graph import StateGraph, START, END
from langgraph.graph.state import CompiledStateGraph
from app.core.workflow.nodes import WorkflowState, NodeFactory from app.core.workflow.nodes import WorkflowState, NodeFactory
from app.core.workflow.expression_evaluator import evaluate_condition from app.core.workflow.expression_evaluator import evaluate_condition
from app.models.workflow_model import WorkflowExecution, WorkflowNodeExecution
from app.core.tools.registry import ToolRegistry from app.core.tools.registry import ToolRegistry
from app.core.tools.executor import ToolExecutor from app.core.tools.executor import ToolExecutor
from app.core.tools.langchain_adapter import LangchainAdapter from app.core.tools.langchain_adapter import LangchainAdapter
TOOL_MANAGEMENT_AVAILABLE = True TOOL_MANAGEMENT_AVAILABLE = True
from app.db import get_db
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -99,7 +97,7 @@ class WorkflowExecutor:
def build_graph(self) -> StateGraph: def build_graph(self) -> CompiledStateGraph:
"""构建 LangGraph """构建 LangGraph
Returns: Returns:
@@ -283,13 +281,24 @@ class WorkflowExecutor:
): ):
"""执行工作流(流式) """执行工作流(流式)
手动执行节点以支持细粒度的流式输出:
- workflow_start: 工作流开始
- node_start: 节点开始执行
- node_chunk: LLM 节点的流式输出片段(逐 token
- node_complete: 节点执行完成
- workflow_complete: 工作流完成
Args: Args:
input_data: 输入数据 input_data: 输入数据
Yields: Yields:
流式事件 流式事件
""" """
logger.info(f"开始执行工作流(流式): execution_id={self.execution_id}") #
logger.info(f"开始执行工作流: execution_id={self.execution_id}")
# 记录开始时间
start_time = datetime.datetime.now()
# 1. 构建图 # 1. 构建图
graph = self.build_graph() graph = self.build_graph()
@@ -297,32 +306,29 @@ class WorkflowExecutor:
# 2. 初始化状态(自动注入系统变量) # 2. 初始化状态(自动注入系统变量)
initial_state = self._prepare_initial_state(input_data) initial_state = self._prepare_initial_state(input_data)
# 3. 流式执行工作流 # 3. 执行工作流
try: try:
# 使用 astream 获取节点级别的更新 async for chunk in graph.astream(
async for event in graph.astream(initial_state, stream_mode="updates"): initial_state,
for node_name, state_update in event.items(): # subgraphs=True,
yield { stream_mode="updates",
"type": "node_complete", ):
"node": node_name, # print(chunk)
"data": state_update, yield chunk
"execution_id": self.execution_id
}
logger.info(f"工作流执行完成(流式): execution_id={self.execution_id}")
# 发送完成事件
yield {
"type": "workflow_complete",
"execution_id": self.execution_id
}
except Exception as e: except Exception as e:
logger.error(f"工作流执行失败(流式): execution_id={self.execution_id}, error={e}", exc_info=True) # 计算耗时(即使失败也记录)
end_time = datetime.datetime.now()
elapsed_time = (end_time - start_time).total_seconds()
logger.error(f"工作流执行失败: execution_id={self.execution_id}, error={e}", exc_info=True)
yield { yield {
"type": "workflow_error", "status": "failed",
"execution_id": self.execution_id, "error": str(e),
"error": str(e) "output": None,
"node_outputs": {},
"elapsed_time": elapsed_time,
"token_usage": None
} }

View File

@@ -50,6 +50,11 @@ class VariableDefinition(BaseModel):
description="变量描述" description="变量描述"
) )
max_length: int = Field(
default=200,
description="只对字符串类型生效"
)
class Config: class Config:
json_schema_extra = { json_schema_extra = {
"examples": [ "examples": [

View File

@@ -5,7 +5,6 @@ End 节点实现
""" """
import logging import logging
from typing import Any
from app.core.workflow.nodes.base_node import BaseNode, WorkflowState from app.core.workflow.nodes.base_node import BaseNode, WorkflowState

View File

@@ -10,10 +10,8 @@ from langchain_core.messages import AIMessage, SystemMessage, HumanMessage
from app.core.workflow.nodes.base_node import BaseNode, WorkflowState from app.core.workflow.nodes.base_node import BaseNode, WorkflowState
from app.core.models import RedBearLLM, RedBearModelConfig from app.core.models import RedBearLLM, RedBearModelConfig
from app.models import ModelConfig from app.db import get_db_context
from app.db import get_db, get_db_context from app.services.model_service import ModelConfigService
from app.models.models_model import ModelApiKey
from app.services.model_service import ModelConfigService, ModelApiKeyService
from app.core.exceptions import BusinessException from app.core.exceptions import BusinessException
from app.core.error_codes import BizCode from app.core.error_codes import BizCode

View File

@@ -1,7 +1,7 @@
""" """
工作流服务层 工作流服务层
""" """
import json
import logging import logging
import uuid import uuid
import datetime import datetime
@@ -530,6 +530,109 @@ class WorkflowService:
message=f"工作流执行失败: {str(e)}" message=f"工作流执行失败: {str(e)}"
) )
async def run_stream(
self,
app_id: uuid.UUID,
payload: DraftRunRequest,
config: WorkflowConfig
):
"""运行工作流(流式)
Args:
app_id: 应用 ID
payload: 请求对象(包含 message, variables, conversation_id 等)
config: 存储类型(可选)
Yields:
SSE 格式的流式事件
Raises:
BusinessException: 配置不存在或执行失败时抛出
"""
# 1. 获取工作流配置
if not config:
config = self.get_workflow_config(app_id)
if not config:
raise BusinessException(
code=BizCode.CONFIG_MISSING,
message=f"工作流配置不存在: app_id={app_id}"
)
input_data = {"message": payload.message, "variables": payload.variables,
"conversation_id": payload.conversation_id}
# 转换 user_id 为 UUID
triggered_by_uuid = None
if payload.user_id:
try:
triggered_by_uuid = uuid.UUID(payload.user_id)
except (ValueError, AttributeError):
logger.warning(f"无效的 user_id 格式: {payload.user_id}")
# 转换 conversation_id 为 UUID
conversation_id_uuid = None
if payload.conversation_id:
try:
conversation_id_uuid = uuid.UUID(payload.conversation_id)
except (ValueError, AttributeError):
logger.warning(f"无效的 conversation_id 格式: {payload.conversation_id}")
# 2. 创建执行记录
execution = self.create_execution(
workflow_config_id=config.id,
app_id=app_id,
trigger_type="manual",
triggered_by=triggered_by_uuid,
conversation_id=conversation_id_uuid,
input_data=input_data
)
# 3. 构建工作流配置字典
workflow_config_dict = {
"nodes": config.nodes,
"edges": config.edges,
"variables": config.variables,
"execution_config": config.execution_config
}
# 4. 获取工作空间 ID从 app 获取)
from app.models import App
# 5. 流式执行工作流
from app.core.workflow.executor import execute_workflow, execute_workflow_stream
try:
# 更新状态为运行中
self.update_execution_status(execution.execution_id, "running")
# 发送开始事件
yield f"data: {json.dumps({'type': 'workflow_start', 'execution_id': execution.execution_id})}\n\n"
# 调用流式执行
async for event in self._run_workflow_stream(
workflow_config=workflow_config_dict,
input_data=input_data,
execution_id=execution.execution_id,
workspace_id="",
user_id=payload.user_id
):
# 清理事件数据,移除不可序列化的对象
cleaned_event = self._clean_event_for_json(event)
# 转换为 SSE 格式
yield f"data: {json.dumps(cleaned_event)}\n\n"
# 发送完成事件
yield f"data: {json.dumps({'type': 'workflow_end', 'execution_id': execution.execution_id})}\n\n"
except Exception as e:
logger.error(f"工作流流式执行失败: execution_id={execution.execution_id}, error={e}", exc_info=True)
self.update_execution_status(
execution.execution_id,
"failed",
error_message=str(e)
)
# 发送错误事件
yield f"data: {json.dumps({'type': 'error', 'execution_id': execution.execution_id, 'error': str(e)})}\n\n"
async def run_workflow( async def run_workflow(
self, self,
app_id: uuid.UUID, app_id: uuid.UUID,
@@ -651,14 +754,44 @@ class WorkflowService:
message=f"工作流执行失败: {str(e)}" message=f"工作流执行失败: {str(e)}"
) )
def _clean_event_for_json(self, event: dict[str, Any]) -> dict[str, Any]:
"""清理事件数据,移除不可序列化的对象
Args:
event: 原始事件数据
Returns:
可序列化的事件数据
"""
from langchain_core.messages import BaseMessage
def clean_value(value):
"""递归清理值"""
if isinstance(value, BaseMessage):
# 将 Message 对象转换为字典
return {
"type": value.__class__.__name__,
"content": value.content,
}
elif isinstance(value, dict):
return {k: clean_value(v) for k, v in value.items()}
elif isinstance(value, list):
return [clean_value(item) for item in value]
elif isinstance(value, (str, int, float, bool, type(None))):
return value
else:
# 其他不可序列化的对象转换为字符串
return str(value)
return clean_value(event)
async def _run_workflow_stream( async def _run_workflow_stream(
self, self,
workflow_config: dict[str, Any], workflow_config: dict[str, Any],
input_data: dict[str, Any], input_data: dict[str, Any],
execution_id: str, execution_id: str,
workspace_id: str, workspace_id: str,
user_id: str user_id: str):
):
"""运行工作流(流式,内部方法) """运行工作流(流式,内部方法)
Args: Args: