[add] workflow support stream mode
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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": [
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user