""" AI 工作流服务 数据权限: - 使用 list_workflows_with_data_scope() 自动应用数据权限 - 支持本人、本部门、本部门及下级、全部等数据范围 """ import copy import logging import time from concurrent.futures import ThreadPoolExecutor, as_completed from queue import Queue from threading import Lock from datetime import datetime from typing import Any, Dict, Generator, List, Optional from sqlalchemy import select, func, and_ from sqlalchemy.ext.asyncio import AsyncSession from ai_platform.models import AIWorkflow, AIWorkflowVersion, AIWorkflowRun from ai_platform.nodes import NodeContext, NodeRegistry, NodeResult from app.data_scope_utils import get_data_scope_filter, apply_data_scope_to_conditions logger = logging.getLogger(__name__) def _apply_node_result_warnings(event: Dict[str, Any], result: NodeResult) -> Dict[str, Any]: """将节点 metadata.warnings 透传到 SSE node_complete 事件""" warnings = (result.metadata or {}).get('warnings') if warnings: event['warnings'] = warnings return event def _resolve_workflow_definition(workflow: AIWorkflow, use_draft: bool) -> dict: if use_draft: return workflow.definition or {} return workflow.published_definition or workflow.definition or {} def _resolve_trigger_type(use_draft: bool, trigger_type: Optional[str] = None) -> str: if trigger_type: return trigger_type return 'editor_draft' if use_draft else 'editor_published' def _node_label_from_map(node_map: Dict[str, Any], node_id: str) -> str: node = node_map.get(node_id) or {} data = node.get('data') or {} return data.get('label') or node.get('label') or node_id def _make_execution_log_entry( node_map: Dict[str, Any], node_id: str, node_type: str, **extra: Any, ) -> dict: return { 'node_id': node_id, 'node_type': node_type, 'node_label': _node_label_from_map(node_map, node_id), **extra, } def _node_result_metadata(result: NodeResult) -> dict: return dict(result.metadata or {}) def _snapshot_node_inputs(context: NodeContext) -> dict: return { 'variables': dict(context.variables), 'previous_output': context.previous_output, } # 资源类型(用于数据权限配置) RESOURCE_TYPE = "ai_workflow" RESOURCE_DISPLAY_NAME = "AI工作流" class AIWorkflowService: """ AI 工作流服务 管理工作流的执行 """ def __init__(self, db: Optional[AsyncSession] = None): self._db = db def _create_workflow_run_record( self, workflow: AIWorkflow, inputs: Dict[str, Any], *, app_id: Optional[str] = None, conversation_id: Optional[str] = None, use_draft: bool = False, trigger_type: Optional[str] = None, ) -> AIWorkflowRun: from utils.context import get_current_user_id_from_context definition = _resolve_workflow_definition(workflow, use_draft) return AIWorkflowRun( workflow_id=workflow.id, app_id=app_id, conversation_id=conversation_id, user_id=get_current_user_id_from_context(), status='running', inputs=inputs, trigger_type=_resolve_trigger_type(use_draft, trigger_type), use_draft=use_draft, workflow_version=workflow.published_version if not use_draft else None, definition_snapshot=copy.deepcopy(definition), started_at=datetime.now(), ) async def get_workflow(self, workflow_id: str) -> Optional[AIWorkflow]: """获取工作流""" if not self._db: return None result = await self._db.execute( select(AIWorkflow).where(AIWorkflow.id == workflow_id, AIWorkflow.is_deleted == False) ) return result.scalar_one_or_none() async def get_workflow_by_code(self, code: str) -> Optional[AIWorkflow]: """根据编码获取工作流""" if not self._db: return None result = await self._db.execute( select(AIWorkflow).where(AIWorkflow.code == code, AIWorkflow.is_deleted == False) ) return result.scalar_one_or_none() async def list_workflows( self, page: int = 1, page_size: int = 20, status: str = None, name: str = None, ) -> tuple: """ 获取工作流列表 Returns: (workflows, total) """ if not self._db: return [], 0 query = select(AIWorkflow).where(AIWorkflow.is_deleted == False) if status: query = query.where(AIWorkflow.status == status) if name: query = query.where(AIWorkflow.name.ilike(f"%{name}%")) # 获取总数 count_result = await self._db.execute( select(func.count()).select_from(query.subquery()) ) total = count_result.scalar() or 0 # 分页 offset = (page - 1) * page_size query = query.offset(offset).limit(page_size) result = await self._db.execute(query) workflows = list(result.scalars().all()) return workflows, total async def list_workflows_with_data_scope( self, page: int = 1, page_size: int = 20, status: str = None, name: str = None, workflow_type: str = None, application_id: str = None, ) -> tuple: """ 获取工作流列表(带数据权限过滤) 自动从上下文获取当前用户信息,应用数据权限过滤 Returns: (workflows, total) """ if not self._db: return [], 0 conditions = [AIWorkflow.is_deleted == False] if status: conditions.append(AIWorkflow.status == status) if name: conditions.append(AIWorkflow.name.ilike(f"%{name}%")) if workflow_type: conditions.append(AIWorkflow.workflow_type == workflow_type) if application_id: conditions.append(AIWorkflow.application_id == application_id) else: conditions.append(AIWorkflow.application_id.is_(None)) # 获取数据权限过滤条件并应用 data_scope_filter = await get_data_scope_filter(self._db, RESOURCE_TYPE) scope_conditions = apply_data_scope_to_conditions(AIWorkflow, data_scope_filter) conditions.extend(scope_conditions) # 获取总数 query = select(AIWorkflow).where(and_(*conditions)) count_result = await self._db.execute( select(func.count()).select_from(query.subquery()) ) total = count_result.scalar() or 0 # 分页 offset = (page - 1) * page_size query = query.order_by(AIWorkflow.sort.desc(), AIWorkflow.sys_create_datetime.desc()) query = query.offset(offset).limit(page_size) result = await self._db.execute(query) workflows = list(result.scalars().all()) return workflows, total async def create_workflow(self, data: Dict) -> AIWorkflow: """创建工作流""" if not self._db: raise ValueError("数据库会话未初始化") workflow = AIWorkflow( name=data.get('name', ''), code=data.get('code', ''), description=data.get('description', ''), definition=data.get('definition', {}), input_variables=data.get('input_variables', []), output_variables=data.get('output_variables', []), ) # 自动填充创建人和部门 from utils.context import get_current_user_info_from_context user_info = get_current_user_info_from_context() if user_info: if not workflow.sys_creator_id: workflow.sys_creator_id = user_info.get('user_id') if not workflow.sys_dept_id and user_info.get('dept_id'): workflow.sys_dept_id = user_info.get('dept_id') self._db.add(workflow) await self._db.commit() await self._db.refresh(workflow) return workflow async def update_workflow(self, workflow_id: str, data: Dict) -> Optional[AIWorkflow]: """更新工作流""" if not self._db: return None result = await self._db.execute( select(AIWorkflow).where(AIWorkflow.id == workflow_id, AIWorkflow.is_deleted == False) ) workflow = result.scalar_one_or_none() if not workflow: return None for key, value in data.items(): if hasattr(workflow, key) and key not in ('id', 'code'): setattr(workflow, key, value) await self._db.commit() await self._db.refresh(workflow) return workflow async def delete_workflow(self, workflow_id: str) -> bool: """删除工作流""" if not self._db: return False result = await self._db.execute( select(AIWorkflow).where(AIWorkflow.id == workflow_id, AIWorkflow.is_deleted == False) ) workflow = result.scalar_one_or_none() if not workflow: return False workflow.is_deleted = True await self._db.commit() return True async def publish_workflow(self, workflow_id: str, description: str = '') -> Optional[AIWorkflow]: """ 发布工作流 1. 创建版本记录 2. 更新工作流的发布状态 """ if not self._db: return None result = await self._db.execute( select(AIWorkflow).where(AIWorkflow.id == workflow_id, AIWorkflow.is_deleted == False) ) workflow = result.scalar_one_or_none() if not workflow: return None # 计算新版本号 new_version = workflow.version or 1 # 创建版本记录 version = AIWorkflowVersion( workflow_id=workflow_id, version=new_version, definition=workflow.definition, description=description, ) self._db.add(version) # 更新工作流状态 workflow.status = 'published' workflow.published_version = new_version workflow.published_at = datetime.now() workflow.published_definition = workflow.definition workflow.version = new_version + 1 # 草稿版本号 +1 await self._db.commit() await self._db.refresh(workflow) return workflow async def rollback_workflow(self, workflow_id: str, version: int) -> Optional[AIWorkflow]: """ 回滚到指定版本 将指定版本的定义复制到当前草稿 """ if not self._db: return None result = await self._db.execute( select(AIWorkflow).where(AIWorkflow.id == workflow_id, AIWorkflow.is_deleted == False) ) workflow = result.scalar_one_or_none() if not workflow: return None version_result = await self._db.execute( select(AIWorkflowVersion).where( AIWorkflowVersion.workflow_id == workflow_id, AIWorkflowVersion.version == version, AIWorkflowVersion.is_deleted == False ) ) version_obj = version_result.scalar_one_or_none() if not version_obj: return None # 将版本定义复制到当前草稿 workflow.definition = version_obj.definition await self._db.commit() await self._db.refresh(workflow) return workflow async def run_workflow( self, workflow_id: str, inputs: Dict[str, Any], app_id: str = None, conversation_id: str = None, use_draft: bool = False, trigger_type: Optional[str] = None, ) -> AIWorkflowRun: """ 运行工作流 Args: workflow_id: 工作流 ID inputs: 输入数据 app_id: 关联应用 ID conversation_id: 关联对话 ID use_draft: 是否使用草稿版本(编辑器调试用) Returns: AIWorkflowRun """ if not self._db: raise ValueError("数据库会话未初始化") workflow = await self.get_workflow(workflow_id) if not workflow: raise ValueError(f'工作流不存在: {workflow_id}') if not use_draft and workflow.status != 'published': raise ValueError('工作流未发布') # 创建运行记录 run = self._create_workflow_run_record( workflow, inputs, app_id=app_id, conversation_id=conversation_id, use_draft=use_draft, trigger_type=trigger_type or 'api', ) self._db.add(run) await self._db.flush() try: # 执行工作流(异步) result = await self._execute_workflow( workflow, inputs, run, use_draft=use_draft ) # 更新运行记录 run.status = 'completed' run.outputs = dict(result.get('outputs', {})) run.execution_log = list(result.get('logs', [])) run.total_tokens = result.get('total_tokens', 0) run.total_steps = result.get('total_steps', 0) run.completed_at = datetime.now() run.elapsed_time = int((run.completed_at - run.started_at).total_seconds() * 1000) # 更新工作流统计 workflow.run_count = (workflow.run_count or 0) + 1 workflow.success_count = (workflow.success_count or 0) + 1 await self._db.commit() except Exception as e: logger.exception(f'工作流执行失败: {e}') run.status = 'failed' run.error_message = str(e) run.completed_at = datetime.now() run.elapsed_time = int((run.completed_at - run.started_at).total_seconds() * 1000) workflow.run_count = (workflow.run_count or 0) + 1 await self._db.commit() raise return run async def _execute_workflow( self, workflow: AIWorkflow, inputs: Dict[str, Any], run: AIWorkflowRun, use_draft: bool = False, ) -> Dict[str, Any]: """ 执行工作流(异步版本) Args: workflow: 工作流定义 inputs: 输入数据 run: 运行记录 use_draft: 是否使用草稿版本(编辑器调试用) Returns: 执行结果 """ # 编辑器调试模式使用草稿版本,否则使用已发布版本 if use_draft: definition = workflow.definition else: definition = workflow.published_definition or workflow.definition nodes = definition.get('nodes', []) edges = definition.get('edges', []) if not nodes: raise ValueError('工作流没有节点') # 构建节点映射和边映射 node_map = {node['id']: node for node in nodes} edge_map = {} # source_id -> [target_ids] # 并行节点的分支映射:source_id -> {sourceHandle -> target_id} parallel_edge_map = {} for edge in edges: source = edge.get('source', '') target = edge.get('target', '') source_handle = edge.get('sourceHandle', '') if source not in edge_map: edge_map[source] = [] edge_map[source].append(target) # 记录 sourceHandle 映射(用于并行节点) if source_handle: if source not in parallel_edge_map: parallel_edge_map[source] = {} parallel_edge_map[source][source_handle] = target # 找到开始节点 start_node = None for node in nodes: if node.get('type') == 'start': start_node = node break if not start_node: raise ValueError('找不到开始节点') # 初始化上下文 context = NodeContext( workflow_run_id=str(run.id), variables=inputs.copy(), user_input=inputs.get('user_input', ''), user_id=str(run.user_id) if run.user_id else '', db_session=self._db, ) # 执行节点 logs = [] total_tokens = 0 total_steps = 0 current_node_id = start_node['id'] while current_node_id: node_config = node_map.get(current_node_id) if not node_config: break node_type = node_config.get('type', '') node_inputs = _snapshot_node_inputs(context) # 结束节点 if node_type == 'end': node_instance = NodeRegistry.create_instance(node_type, node_config.get('data', {})) if node_instance: result = node_instance.execute(context) logs.append(_make_execution_log_entry( node_map, current_node_id, node_type, status='completed', output=result.output, metadata=_node_result_metadata(result), inputs=copy.deepcopy(node_inputs), )) break # 创建节点实例 node_instance = NodeRegistry.create_instance(node_type, node_config.get('data', {})) if not node_instance: logger.warning(f'未知节点类型: {node_type}') # 跳到下一个节点 next_nodes = edge_map.get(current_node_id, []) current_node_id = next_nodes[0] if next_nodes else None continue # 执行节点 start_time = time.time() result = await node_instance.execute_async(context) elapsed = int((time.time() - start_time) * 1000) # 记录日志 logs.append(_make_execution_log_entry( node_map, current_node_id, node_type, status='completed' if result.success else 'failed', output=result.output, error=result.error, elapsed_time=elapsed, tokens_used=result.tokens_used, metadata=_node_result_metadata(result), inputs=copy.deepcopy(node_inputs), )) total_tokens += result.tokens_used total_steps += 1 # 更新运行记录的当前节点并提交 run.current_node_id = current_node_id run.execution_log = logs.copy() if self._db: await self._db.commit() if not result.success: raise ValueError(f'节点执行失败: {result.error}') # 合并输出变量到上下文 context.variables.update(result.output_variables) context.previous_output = result.output # 保存节点输出到命名空间(支持 {{NodeID.key}} 格式引用) context.variables[f'_node_{current_node_id}'] = { 'output': result.output, **result.output_variables, } # 确定下一个节点 if node_type == 'parallel': # 并行节点:执行所有分支 parallel_results = await self._execute_parallel_branches( node_map, edge_map, parallel_edge_map, current_node_id, context, run, logs ) # 将并行结果存入上下文 context.set_variable('_parallel_results', parallel_results['results']) total_tokens += parallel_results['total_tokens'] total_steps += parallel_results['total_steps'] logs.extend(parallel_results['logs']) # 找到合并节点继续执行 current_node_id = parallel_results.get('merge_node_id') elif result.next_node_id: # 检查是否是分支 ID(条件/意图分支节点返回的是分支 ID,需要通过边映射找到实际目标节点) logger.info(f'节点 {current_node_id} 返回 next_node_id={result.next_node_id}') logger.info(f'parallel_edge_map[{current_node_id}]={parallel_edge_map.get(current_node_id, {})}') if result.next_node_id in parallel_edge_map.get(current_node_id, {}): current_node_id = parallel_edge_map[current_node_id][result.next_node_id] logger.info(f'通过分支映射找到目标节点: {current_node_id}') elif result.next_node_id in node_map: # 是实际的节点 ID current_node_id = result.next_node_id logger.info(f'直接使用节点 ID: {current_node_id}') else: # 分支 ID 未找到对应边,尝试按普通边查找 logger.warning(f'分支 ID {result.next_node_id} 未找到对应边,回退到普通边查找') next_nodes = edge_map.get(current_node_id, []) current_node_id = next_nodes[0] if next_nodes else None else: # 按边查找下一个节点 next_nodes = edge_map.get(current_node_id, []) current_node_id = next_nodes[0] if next_nodes else None return { 'outputs': context.variables, 'logs': logs, 'total_tokens': total_tokens, 'total_steps': total_steps, } async def _execute_parallel_branches( self, node_map: Dict[str, Any], edge_map: Dict[str, List[str]], parallel_edge_map: Dict[str, Dict[str, str]], parallel_node_id: str, context: NodeContext, run: AIWorkflowRun, parent_logs: List[Dict], ) -> Dict[str, Any]: """ 执行并行分支(异步版本) Args: node_map: 节点映射 edge_map: 边映射 parallel_edge_map: 并行节点的分支映射 {source_id: {sourceHandle: target_id}} parallel_node_id: 并行节点 ID context: 执行上下文 run: 运行记录 parent_logs: 父级日志列表 Returns: 并行执行结果 """ import asyncio # 获取并行节点的分支映射(sourceHandle -> target_id) branch_mapping = parallel_edge_map.get(parallel_node_id, {}) # 如果没有分支映射,回退到普通边映射 if not branch_mapping: branch_start_nodes = edge_map.get(parallel_node_id, []) # 创建默认的分支映射 branch_mapping = {f'branch_{i}': node_id for i, node_id in enumerate(branch_start_nodes)} branch_start_nodes = list(branch_mapping.values()) if not branch_start_nodes: return { 'results': {}, 'logs': [], 'total_tokens': 0, 'total_steps': 0, 'merge_node_id': None, } # 找到合并节点(所有分支最终汇聚的节点) merge_node_id = self._find_merge_node(node_map, edge_map, branch_start_nodes) # 并行执行各分支 branch_results = {} all_logs = [] total_tokens = 0 total_steps = 0 async def execute_branch(branch_id: str, branch_start_id: str, branch_context: NodeContext) -> Dict: """执行单个分支(异步)""" branch_logs = [] branch_tokens = 0 branch_steps = 0 current_id = branch_start_id branch_output = None while current_id and current_id != merge_node_id: node_config = node_map.get(current_id) if not node_config: break node_type = node_config.get('type', '') # 跳过结束节点和合并节点 if node_type in ('end', 'merge'): break # 创建节点实例 node_instance = NodeRegistry.create_instance(node_type, node_config.get('data', {})) if not node_instance: next_nodes = edge_map.get(current_id, []) current_id = next_nodes[0] if next_nodes else None continue # 执行节点(异步) start_time = time.time() result = await node_instance.execute_async(branch_context) elapsed = int((time.time() - start_time) * 1000) branch_logs.append({ 'node_id': current_id, 'node_type': node_type, 'status': 'completed' if result.success else 'failed', 'output': result.output, 'error': result.error, 'elapsed_time': elapsed, 'tokens_used': result.tokens_used, 'metadata': _node_result_metadata(result), 'branch': branch_start_id, }) branch_tokens += result.tokens_used branch_steps += 1 if not result.success: return { 'branch_id': branch_id, 'success': False, 'output': None, 'error': result.error, 'logs': branch_logs, 'tokens': branch_tokens, 'steps': branch_steps, } # 更新分支上下文 branch_context.variables.update(result.output_variables) branch_context.previous_output = result.output branch_output = result.output # 下一个节点 if result.next_node_id: current_id = result.next_node_id else: next_nodes = edge_map.get(current_id, []) # 过滤掉合并节点 next_nodes = [n for n in next_nodes if n != merge_node_id] current_id = next_nodes[0] if next_nodes else None return { 'branch_id': branch_id, 'success': True, 'output': branch_output, 'logs': branch_logs, 'tokens': branch_tokens, 'steps': branch_steps, 'variables': branch_context.variables, } # 创建反向映射:target_node_id -> branch_id (sourceHandle) target_to_branch = {v: k for k, v in branch_mapping.items()} # 创建所有分支的任务 tasks = [] for target_node_id in branch_start_nodes: # 为每个分支创建独立的上下文副本 branch_context = NodeContext( workflow_run_id=context.workflow_run_id, variables=copy.deepcopy(context.variables), user_input=context.user_input, user_id=context.user_id, conversation_history=context.conversation_history.copy(), previous_output=context.previous_output, db_session=context.db_session, ) # 获取分支 ID(sourceHandle) branch_id = target_to_branch.get(target_node_id, target_node_id) tasks.append(execute_branch(branch_id, target_node_id, branch_context)) # 并行执行所有分支 results = await asyncio.gather(*tasks, return_exceptions=True) for result in results: if isinstance(result, Exception): logger.exception(f'分支执行失败: {result}') all_logs.append({ 'node_id': 'unknown', 'node_type': 'branch', 'status': 'failed', 'error': str(result), }) else: branch_id = result.get('branch_id') branch_results[branch_id] = result.get('output') all_logs.extend(result.get('logs', [])) total_tokens += result.get('tokens', 0) total_steps += result.get('steps', 0) # 合并分支变量到主上下文 if result.get('variables'): for key, value in result['variables'].items(): if not key.startswith('_'): # 不合并内部变量 context.variables[f'{branch_id}_{key}'] = value return { 'results': branch_results, 'logs': all_logs, 'total_tokens': total_tokens, 'total_steps': total_steps, 'merge_node_id': merge_node_id, } async def _execute_parallel_branches_stream( self, node_map: Dict[str, Any], edge_map: Dict[str, List[str]], parallel_edge_map: Dict[str, Dict[str, str]], parallel_node_id: str, context: NodeContext, run: AIWorkflowRun, parent_logs: List[Dict], ): """ 流式执行并行分支,yield 每个节点的执行事件(异步版本) Args: node_map: 节点映射 edge_map: 边映射 parallel_edge_map: 并行节点的分支映射 parallel_node_id: 并行节点 ID context: 执行上下文 run: 运行记录 parent_logs: 父级日志列表 Yields: dict: 节点执行事件 """ import asyncio # 获取并行节点的分支映射 branch_mapping = parallel_edge_map.get(parallel_node_id, {}) if not branch_mapping: branch_start_nodes = edge_map.get(parallel_node_id, []) branch_mapping = {f'branch_{i}': node_id for i, node_id in enumerate(branch_start_nodes)} branch_start_nodes = list(branch_mapping.values()) if not branch_start_nodes: yield { 'type': '_parallel_result', 'data': { 'results': {}, 'logs': [], 'total_tokens': 0, 'total_steps': 0, 'merge_node_id': None, } } return merge_node_id = self._find_merge_node(node_map, edge_map, branch_start_nodes) branch_results = {} all_logs = [] total_tokens = 0 total_steps = 0 target_to_branch = {v: k for k, v in branch_mapping.items()} # 顺序执行各分支(为了能够yield事件) for target_node_id in branch_start_nodes: branch_id = target_to_branch.get(target_node_id, target_node_id) branch_context = NodeContext( workflow_run_id=context.workflow_run_id, variables=copy.deepcopy(context.variables), user_input=context.user_input, user_id=context.user_id, conversation_history=context.conversation_history.copy(), previous_output=context.previous_output, db_session=context.db_session, ) branch_logs = [] branch_tokens = 0 branch_steps = 0 current_id = target_node_id branch_output = None while current_id and current_id != merge_node_id: node_config = node_map.get(current_id) if not node_config: break node_type = node_config.get('type', '') node_label = node_config.get('data', {}).get('label', current_id) if node_type in ('end', 'merge'): break node_instance = NodeRegistry.create_instance(node_type, node_config.get('data', {})) if not node_instance: next_nodes = edge_map.get(current_id, []) current_id = next_nodes[0] if next_nodes else None continue # 发送节点开始事件 yield { 'type': 'node_start', 'node_id': current_id, 'node_type': node_type, 'node_label': node_label, 'branch_id': branch_id, 'timestamp': datetime.now().isoformat(), 'inputs': { 'variables': dict(branch_context.variables), 'previous_output': branch_context.previous_output, }, } # 执行节点(异步) start_time = time.time() result = await node_instance.execute_async(branch_context) elapsed = int((time.time() - start_time) * 1000) branch_logs.append({ 'node_id': current_id, 'node_type': node_type, 'status': 'completed' if result.success else 'failed', 'output': result.output, 'error': result.error, 'elapsed_time': elapsed, 'tokens_used': result.tokens_used, 'metadata': _node_result_metadata(result), 'branch': branch_id, }) branch_tokens += result.tokens_used branch_steps += 1 # 发送节点完成事件 yield _apply_node_result_warnings({ 'type': 'node_complete', 'node_id': current_id, 'node_type': node_type, 'branch_id': branch_id, 'status': 'success' if result.success else 'failed', 'elapsed_time': elapsed, 'tokens_used': result.tokens_used, 'error_message': result.error if not result.success else None, 'outputs': { 'output': result.output, 'output_variables': result.metadata.get('frontend_output_variables', result.output_variables), }, }, result) if not result.success: break branch_context.variables.update(result.output_variables) branch_context.previous_output = result.output branch_output = result.output # 保存节点输出到命名空间 branch_context.variables[f'_node_{current_id}'] = { 'output': result.output, **result.output_variables, } if result.next_node_id: current_id = result.next_node_id else: next_nodes = edge_map.get(current_id, []) next_nodes = [n for n in next_nodes if n != merge_node_id] current_id = next_nodes[0] if next_nodes else None # 收集分支结果 branch_results[branch_id] = branch_output all_logs.extend(branch_logs) total_tokens += branch_tokens total_steps += branch_steps # 合并分支变量到主上下文 for key, value in branch_context.variables.items(): if not key.startswith('_'): context.variables[f'{branch_id}_{key}'] = value # 返回最终结果 yield { 'type': '_parallel_result', 'data': { 'results': branch_results, 'logs': all_logs, 'total_tokens': total_tokens, 'total_steps': total_steps, 'merge_node_id': merge_node_id, } } def _find_merge_node( self, node_map: Dict[str, Any], edge_map: Dict[str, List[str]], branch_start_nodes: List[str], ) -> Optional[str]: """ 查找合并节点 遍历所有分支,找到它们共同汇聚的合并节点 """ # 简单实现:查找类型为 merge 的节点 for node_id, node_config in node_map.items(): if node_config.get('type') == 'merge': return node_id # 如果没有显式的合并节点,尝试找到所有分支汇聚的节点 # 这里使用简单的方法:找到第一个被多个节点指向的节点 incoming_count = {} for source, targets in edge_map.items(): for target in targets: incoming_count[target] = incoming_count.get(target, 0) + 1 for node_id, count in incoming_count.items(): if count >= len(branch_start_nodes): return node_id return None def _get_child_nodes( self, node_map: Dict[str, Any], parent_node_id: str, ) -> List[str]: """ 获取循环节点的子节点列表 子节点通过 parentNode 属性关联到循环节点 """ child_nodes = [] for node_id, node_config in node_map.items(): # 检查节点配置中的 parentNode 属性 parent_node = node_config.get('parentNode') if parent_node == parent_node_id: child_nodes.append(node_id) logger.info(f'[Loop] Found child node: {node_id} with parentNode={parent_node}') logger.info(f'[Loop] Parent node {parent_node_id} has {len(child_nodes)} child nodes: {child_nodes}') return child_nodes async def _execute_loop( self, node_map: Dict[str, Any], edge_map: Dict[str, List[str]], parallel_edge_map: Dict[str, Dict[str, str]], loop_node_id: str, loop_config: Dict[str, Any], context: NodeContext, run: AIWorkflowRun, parent_logs: List[Dict], ): """ 执行循环体(单节点设计,异步版本) 循环体的子节点通过 parentNode 属性关联到循环节点, 或者通过 childNodes 配置指定 Args: node_map: 节点映射 edge_map: 边映射 parallel_edge_map: 并行边映射 loop_node_id: 循环节点 ID loop_config: 循环配置 context: 执行上下文 run: 运行记录 parent_logs: 父级日志列表 Yields: 循环执行事件 """ import asyncio loop_mode = context.get_variable('_loop_mode', 'for_each') max_iterations = loop_config.get('max_iterations', 100) item_var_name = loop_config.get('item_variable_name', 'item') index_var_name = loop_config.get('index_variable_name', 'index') # 获取循环体子节点 # 方式1: 通过 parentNode 属性关联 child_node_ids = self._get_child_nodes(node_map, loop_node_id) # 方式2: 通过 childNodes 配置指定 if not child_node_ids: child_node_ids = loop_config.get('childNodes', []) if not child_node_ids: logger.warning(f'循环节点 {loop_node_id} 没有子节点') return # 构建子节点的边映射(只包含子节点之间的边) child_edge_map = {} for source_id in child_node_ids: targets = edge_map.get(source_id, []) # 只保留指向其他子节点的边 child_targets = [t for t in targets if t in child_node_ids] if child_targets: child_edge_map[source_id] = child_targets # 找到循环体的起始节点(没有入边的子节点) nodes_with_incoming = set() for targets in child_edge_map.values(): nodes_with_incoming.update(targets) start_nodes = [n for n in child_node_ids if n not in nodes_with_incoming] if not start_nodes: # 如果所有节点都有入边,取第一个子节点 start_nodes = [child_node_ids[0]] loop_body_start_id = start_nodes[0] # 从当前循环索引开始(支持恢复执行) iteration = context.get_variable('_loop_index', 0) logger.info(f'[Loop] Starting loop execution, mode={loop_mode}, max_iterations={max_iterations}, start_iteration={iteration}') logger.info(f'[Loop] _loop_items={context.get_variable("_loop_items", [])}') logger.info(f'[Loop] _loop_index={context.get_variable("_loop_index", 0)}') while iteration < max_iterations: # 检查是否继续循环 if loop_mode == 'for_each': items = context.get_variable('_loop_items', []) current_index = context.get_variable('_loop_index', 0) logger.info(f'[Loop] Iteration {iteration}: items={items}, current_index={current_index}') if current_index >= len(items): logger.info(f'[Loop] Breaking loop: current_index({current_index}) >= len(items)({len(items)})') break # 设置当前循环项和索引 current_item = items[current_index] context.set_variable(item_var_name, current_item) context.set_variable(index_var_name, current_index) context.set_variable('_current_item', current_item) context.set_variable('_current_index', current_index) # 同时存储到循环节点的命名空间,支持 {{loop_node_id.item.xxx}} 格式 context.variables[f'_node_{loop_node_id}'] = { item_var_name: current_item, index_var_name: current_index, 'total': len(items), } elif loop_mode == 'while': # while 模式:需要重新评估条件 from ai_platform.nodes.builtin.loop_node import LoopNode loop_node_instance = LoopNode(loop_config) if not loop_node_instance._evaluate_condition(context): break context.set_variable(index_var_name, iteration) # 发送迭代开始事件 total_iterations = len(items) if loop_mode == 'for_each' else None yield { 'type': 'loop_iteration_start', 'node_id': loop_node_id, 'iteration': iteration, 'total': total_iterations, 'item': context.get_variable(item_var_name) if loop_mode == 'for_each' else None, } # 执行循环体 iteration_logs = [] iteration_tokens = 0 iteration_steps = 0 current_id = loop_body_start_id iteration_output = None executed_nodes = set() while current_id and current_id in child_node_ids: # 防止无限循环 if current_id in executed_nodes: break executed_nodes.add(current_id) node_config = node_map.get(current_id) if not node_config: break node_type = node_config.get('type', '') # 创建节点实例 node_instance = NodeRegistry.create_instance(node_type, node_config.get('data', {})) if not node_instance: next_nodes = child_edge_map.get(current_id, []) current_id = next_nodes[0] if next_nodes else None continue # 发送节点开始事件 yield { 'type': 'node_start', 'node_id': current_id, 'node_type': node_type, 'node_label': node_config.get('data', {}).get('label', ''), 'loop_iteration': iteration, 'inputs': { 'previous_output': context.previous_output, 'variables': { '_current_item': context.get_variable('_current_item'), '_current_index': context.get_variable('_current_index'), '_loop_total': context.get_variable('_loop_total'), } }, } # 执行节点(异步) start_time = time.time() logger.info(f'[Loop] Executing child node: {current_id}, type={node_type}') result = await node_instance.execute_async(context) elapsed = int((time.time() - start_time) * 1000) logger.info(f'[Loop] Child node result: success={result.success}, output={result.output}, error={result.error}') iteration_logs.append({ 'node_id': current_id, 'node_type': node_type, 'status': 'completed' if result.success else 'failed', 'output': result.output, 'error': result.error, 'elapsed_time': elapsed, 'tokens_used': result.tokens_used, 'metadata': _node_result_metadata(result), 'loop_iteration': iteration, }) iteration_tokens += result.tokens_used iteration_steps += 1 # 发送节点执行完成事件 yield _apply_node_result_warnings({ 'type': 'node_complete', 'node_id': current_id, 'node_type': node_type, 'status': 'success' if result.success else 'failed', 'elapsed_time': elapsed, 'tokens_used': result.tokens_used, 'error_message': result.error, 'loop_iteration': iteration, 'outputs': { 'output': result.output, 'output_variables': result.metadata.get('frontend_output_variables', result.output_variables), }, 'inputs': { 'previous_output': context.previous_output, 'variables': { '_current_item': context.get_variable('_current_item'), '_current_index': context.get_variable('_current_index'), '_loop_total': context.get_variable('_loop_total'), } }, }, result) # 发送节点产生的事件(如消息事件) if result.events: for event in result.events: yield { 'type': 'node_event', 'node_id': current_id, 'event': event, 'loop_iteration': iteration, } # 检查是否需要等待用户输入(设计预览节点等) if result.waiting_for_input: # 记录日志 iteration_logs.append({ 'node_id': current_id, 'node_type': node_type, 'status': 'waiting', 'elapsed_time': elapsed, 'loop_iteration': iteration, }) # 更新运行记录为等待状态 run.status = 'waiting' run.current_node_id = current_id run.execution_log = parent_logs + iteration_logs run.outputs = dict(context.variables) run.waiting_config = dict(result.waiting_config) if result.waiting_config else {} # 保存循环状态以便恢复 run.waiting_config['_loop_state'] = { 'loop_node_id': loop_node_id, 'iteration': iteration, 'current_index': context.get_variable('_loop_index', 0), } if self._db: await self._db.commit() # 发送等待事件 yield { 'type': 'waiting_input', 'node_id': current_id, 'node_type': node_type, 'config': result.waiting_config, 'loop_iteration': iteration, } # 暂停循环执行,等待用户输入 return if not result.success: logger.error(f'[Loop] 子节点执行失败,发送 loop_iteration_error 事件: {result.error}') yield { 'type': 'loop_iteration_error', 'node_id': loop_node_id, 'iteration': iteration, 'error': result.error, } # 循环中断 return # 更新上下文 context.variables.update(result.output_variables) context.previous_output = result.output iteration_output = result.output # 保存节点输出到命名空间 context.variables[f'_node_{current_id}'] = { 'output': result.output, **result.output_variables, } # 下一个节点(只在子节点范围内) if result.next_node_id and result.next_node_id in child_node_ids: current_id = result.next_node_id else: next_nodes = child_edge_map.get(current_id, []) current_id = next_nodes[0] if next_nodes else None # 收集本次迭代结果 loop_results = context.get_variable('_loop_results', []) loop_results.append(iteration_output) context.set_variable('_loop_results', loop_results) # 更新循环索引 context.set_variable('_loop_index', context.get_variable('_loop_index', 0) + 1) # 添加日志 parent_logs.extend(iteration_logs) # 发送迭代完成事件 yield { 'type': 'loop_iteration_complete', 'node_id': loop_node_id, 'iteration': iteration, 'total': total_iterations, 'output': iteration_output, 'tokens_used': iteration_tokens, 'steps': iteration_steps, } iteration += 1 # 清理循环变量 context.variables.pop('_loop_items', None) context.variables.pop('_loop_index', None) context.variables.pop('_loop_total', None) context.variables.pop('_loop_mode', None) context.variables.pop('_current_item', None) context.variables.pop('_current_index', None) async def run_workflow_stream_async( self, workflow_id: str, inputs: Dict[str, Any], app_id: str = None, conversation_id: str = None, conversation_history: List[Dict[str, str]] = None, use_draft: bool = False, trigger_type: Optional[str] = None, ): """ 异步流式运行工作流 Args: workflow_id: 工作流 ID inputs: 输入变量 app_id: 应用 ID conversation_id: 对话 ID conversation_history: 对话历史 use_draft: 是否使用草稿版本 Yields: dict: 节点执行状态事件 """ if not self._db: yield {'type': 'error', 'message': '数据库会话未初始化'} return workflow = await self.get_workflow(workflow_id) if not workflow: yield {'type': 'error', 'message': f'工作流不存在: {workflow_id}'} return # 编辑器调试模式不检查发布状态 if not use_draft and workflow.status != 'published': yield {'type': 'error', 'message': '工作流未发布'} return # 创建运行记录 run = self._create_workflow_run_record( workflow, inputs, app_id=app_id, conversation_id=conversation_id, use_draft=use_draft, trigger_type=trigger_type, ) self._db.add(run) await self._db.flush() try: # 使用异步生成器执行工作流 async for event in self._execute_workflow_stream(workflow, inputs, run, conversation_history, use_draft): yield event # 在关键事件后提交数据库状态 event_type = event.get('type') if event_type in ('node_complete', 'waiting_input', 'complete', 'error'): await self._db.commit() # 更新运行记录 if run.status == 'waiting': # 等待状态,已在循环中提交 pass elif run.status != 'completed': run.status = 'completed' run.completed_at = datetime.now() run.elapsed_time = int((run.completed_at - run.started_at).total_seconds() * 1000) # 更新工作流统计 workflow.run_count = (workflow.run_count or 0) + 1 workflow.success_count = (workflow.success_count or 0) + 1 await self._db.commit() except Exception as e: logger.exception(f'工作流执行失败: {e}') run.status = 'failed' run.error_message = str(e) run.completed_at = datetime.now() run.elapsed_time = int((run.completed_at - run.started_at).total_seconds() * 1000) workflow.run_count = (workflow.run_count or 0) + 1 await self._db.commit() # 发送错误事件给前端 yield { 'type': 'error', 'message': str(e), 'error_message': str(e), 'run_id': run.id, 'workflow_id': workflow.id, } async def _execute_workflow_stream( self, workflow: AIWorkflow, inputs: Dict[str, Any], run: AIWorkflowRun, conversation_history: List[Dict[str, str]] = None, use_draft: bool = False, ): """ 流式执行工作流,逐节点 yield 状态(异步版本) Args: workflow: 工作流对象 inputs: 输入变量 run: 工作流运行记录 conversation_history: 对话历史(用于 LLM 节点的上下文记忆) use_draft: 是否使用草稿版本(编辑器调试用) """ import asyncio # 编辑器调试模式使用草稿版本,否则使用已发布版本 if use_draft: definition = workflow.definition else: definition = workflow.published_definition or workflow.definition nodes = definition.get('nodes', []) edges = definition.get('edges', []) if not nodes: raise ValueError('工作流没有节点') # 构建节点映射和边映射 node_map = {node['id']: node for node in nodes} edge_map = {} # 并行节点的分支映射:source_id -> {sourceHandle -> target_id} parallel_edge_map = {} # 调试:打印所有节点的 parentNode 属性 for node in nodes: if node.get('parentNode'): logger.info(f'[流式] 节点 {node["id"]} 的 parentNode={node.get("parentNode")}') logger.info(f'[流式] 边列表: {edges}') for edge in edges: source = edge.get('source', '') target = edge.get('target', '') source_handle = edge.get('sourceHandle', '') logger.info(f'[流式] 边: source={source}, target={target}, sourceHandle={source_handle}') if source not in edge_map: edge_map[source] = [] edge_map[source].append(target) # 记录 sourceHandle 映射(用于并行节点) if source_handle: if source not in parallel_edge_map: parallel_edge_map[source] = {} parallel_edge_map[source][source_handle] = target # 找到开始节点 start_node = None for node in nodes: if node.get('type') == 'start': start_node = node break if not start_node: raise ValueError('找不到开始节点') # 初始化上下文 context = NodeContext( workflow_run_id=str(run.id), variables=inputs.copy(), user_input=inputs.get('user_input', ''), user_id=str(run.user_id) if run.user_id else '', conversation_history=conversation_history or [], db_session=self._db, ) logs = [] total_tokens = 0 total_steps = 0 current_node_id = start_node['id'] # 发送工作流开始事件(包含 run_id,用于对话流恢复) yield { 'type': 'start', 'run_id': str(run.id), 'workflow_id': str(workflow.id), 'workflow_name': workflow.name, } while current_node_id: node_config = node_map.get(current_node_id) if not node_config: break node_type = node_config.get('type', '') node_label = node_config.get('data', {}).get('label', current_node_id) # 获取节点输入(当前上下文变量的快照) node_inputs = { 'variables': dict(context.variables), 'previous_output': context.previous_output, } # 发送节点开始事件(包含时间戳和输入数据) yield { 'type': 'node_start', 'node_id': current_node_id, 'node_type': node_type, 'node_label': node_label, 'timestamp': datetime.now().isoformat(), 'inputs': node_inputs, } # 结束节点 if node_type == 'end': node_instance = NodeRegistry.create_instance(node_type, node_config.get('data', {})) if node_instance: result = node_instance.execute(context) logs.append(_make_execution_log_entry( node_map, current_node_id, node_type, status='completed', output=result.output, inputs=copy.deepcopy(node_inputs), )) yield { 'type': 'node_complete', 'node_id': current_node_id, 'node_type': node_type, 'status': 'success', 'elapsed_time': 0, 'tokens_used': 0, } # 保存结束节点输出和上下文变量到运行记录 run.execution_log = logs.copy() # 只有当结束节点有输出时才包含 output 字段 if result.output is not None: run.outputs = { **context.variables, 'output': result.output, # 结束节点的输出内容 } else: run.outputs = dict(context.variables) # 更新运行状态为完成 run.status = 'completed' run.completed_at = datetime.now() run.elapsed_time = int((run.completed_at - run.started_at).total_seconds() * 1000) # 发送 complete 事件 yield { 'type': 'complete', 'run_id': str(run.id), 'status': 'completed', 'outputs': run.outputs, 'elapsed_time': run.elapsed_time, 'total_tokens': total_tokens, } break # 创建节点实例 node_instance = NodeRegistry.create_instance(node_type, node_config.get('data', {})) if not node_instance: logger.warning(f'未知节点类型: {node_type}') next_nodes = edge_map.get(current_node_id, []) current_node_id = next_nodes[0] if next_nodes else None continue # 执行节点(在线程池中运行同步代码) start_time = time.time() # LLM 节点:根据配置决定是否使用流式执行 node_data = node_config.get('data', {}) use_streaming = node_data.get('use_streaming', False) if node_type == 'llm' else False if node_type == 'llm' and use_streaming and hasattr(node_instance, 'execute_stream'): result = None stream_gen = node_instance.execute_stream(context) try: while True: # 在线程池中获取下一个chunk chunk_event = await asyncio.get_event_loop().run_in_executor( None, next, stream_gen ) # 发送 LLM 流式内容事件 if chunk_event.content: yield { 'type': 'llm_chunk', 'node_id': current_node_id, 'content': chunk_event.content, 'accumulated_content': chunk_event.accumulated_content, } except StopIteration as e: # 生成器结束,获取返回值(NodeResult) result = e.value if result is None: result = NodeResult(success=False, error='LLM 节点执行异常') else: # 其他节点使用异步执行 result = await node_instance.execute_async(context) elapsed = int((time.time() - start_time) * 1000) # 处理节点事件(如消息节点发送消息) if result.events: for event in result.events: if event.get('type') == 'message': # 消息事件直接发送为 answer 类型 yield { 'type': 'answer', 'node_id': current_node_id, 'content': event.get('content', ''), } else: # 其他事件保持 node_event 格式 yield { 'type': 'node_event', 'node_id': current_node_id, 'event': event, } # 检查是否需要等待用户输入(对话流节点) if result.waiting_for_input: # 记录日志 log_entry = _make_execution_log_entry( node_map, current_node_id, node_type, status='waiting', output=result.output, elapsed_time=elapsed, inputs=copy.deepcopy(node_inputs), ) logs.append(log_entry) # 调试:打印等待时保存的变量 logger.info(f'等待用户输入,保存变量到 run.outputs,变量键: {list(context.variables.keys())}') node_keys = [k for k in context.variables.keys() if k.startswith('_node_')] logger.info(f'等待用户输入,_node_ 前缀的变量: {node_keys}') # 更新运行记录为等待状态 run.status = 'waiting' run.current_node_id = current_node_id run.execution_log = logs.copy() # 使用 copy() 确保 SQLAlchemy 检测到变化 run.outputs = dict(context.variables) # 使用 dict() 创建新对象,确保 SQLAlchemy 检测到变化 run.waiting_config = dict(result.waiting_config) if result.waiting_config else {} # 注意:save由调用方处理 # 发送等待输入事件 yield { 'type': 'waiting_input', 'node_id': current_node_id, 'node_type': node_type, 'config': result.waiting_config, } # 暂停工作流执行,等待用户输入后续流 return # 记录日志 log_entry = _make_execution_log_entry( node_map, current_node_id, node_type, status='completed' if result.success else 'failed', output=result.output, error=result.error, elapsed_time=elapsed, tokens_used=result.tokens_used, metadata=_node_result_metadata(result), inputs=copy.deepcopy(node_inputs), ) logs.append(log_entry) total_tokens += result.tokens_used total_steps += 1 # 更新运行记录 run.current_node_id = current_node_id run.execution_log = logs.copy() # 使用 copy() 确保 SQLAlchemy 检测到变化 run.total_tokens = total_tokens run.total_steps = total_steps run.outputs = dict(context.variables) # 使用 dict() 创建新对象,确保 SQLAlchemy 检测到变化 # 注意:save由调用方处理 # 发送节点完成事件(包含输出数据) yield _apply_node_result_warnings({ 'type': 'node_complete', 'node_id': current_node_id, 'node_type': node_type, 'status': 'success' if result.success else 'failed', 'elapsed_time': elapsed, 'tokens_used': result.tokens_used, 'error_message': result.error if not result.success else None, 'outputs': { 'output': result.output, 'output_variables': result.metadata.get('frontend_output_variables', result.output_variables), }, }, result) if not result.success: raise ValueError(f'节点执行失败: {result.error}') # 合并输出变量到上下文 context.variables.update(result.output_variables) context.previous_output = result.output # 保存节点输出到命名空间(支持 {{NodeID.key}} 格式引用) context.variables[f'_node_{current_node_id}'] = { 'output': result.output, **result.output_variables, } # 确定下一个节点 if node_type == 'parallel': # 并行节点:执行所有分支(流式) branch_ids = list(parallel_edge_map.get(current_node_id, {}).keys()) or edge_map.get(current_node_id, []) yield { 'type': 'parallel_start', 'node_id': current_node_id, 'branches': branch_ids, } # 使用流式版本的并行分支执行(异步) async for event in self._execute_parallel_branches_stream( node_map, edge_map, parallel_edge_map, current_node_id, context, run, logs ): if event.get('type') == '_parallel_result': # 内部结果事件,不 yield 给前端 parallel_results = event['data'] context.set_variable('_parallel_results', parallel_results['results']) total_tokens += parallel_results['total_tokens'] total_steps += parallel_results['total_steps'] logs.extend(parallel_results['logs']) yield { 'type': 'parallel_complete', 'node_id': current_node_id, 'branch_results': parallel_results['results'], 'total_tokens': parallel_results['total_tokens'], } # 找到合并节点继续执行 current_node_id = parallel_results.get('merge_node_id') else: # 其他事件(node_start, node_complete 等)直接 yield yield event elif node_type == 'loop': # 循环节点:执行循环体 loop_node_id = current_node_id loop_config = node_config.get('data', {}) yield { 'type': 'loop_start', 'node_id': loop_node_id, 'loop_mode': context.get_variable('_loop_mode'), 'total_iterations': context.get_variable('_loop_total', 0), } # 执行循环(异步) loop_waiting = False loop_error = None async for loop_event in self._execute_loop( node_map, edge_map, parallel_edge_map, loop_node_id, loop_config, context, run, logs ): yield loop_event # 累加循环中的 token 和步骤 if loop_event.get('type') == 'loop_iteration_complete': total_tokens += loop_event.get('tokens_used', 0) total_steps += loop_event.get('steps', 0) # 检查是否因等待用户输入而暂停 if loop_event.get('type') == 'waiting_input': loop_waiting = True # 检查是否有循环迭代错误 if loop_event.get('type') == 'loop_iteration_error': loop_error = loop_event.get('error', '循环执行失败') logger.error(f'[流式] 检测到循环迭代错误: {loop_error}') # 如果循环因等待用户输入而暂停,停止主工作流执行 if loop_waiting: return # 如果循环中有错误,抛出异常 if loop_error: raise ValueError(f'循环节点执行失败: {loop_error}') # 获取循环结果 loop_results = context.get_variable('_loop_results', []) output_var = loop_config.get('output_variable', 'loop_results') context.set_variable(output_var, loop_results) yield { 'type': 'loop_complete', 'node_id': loop_node_id, 'total_iterations': context.get_variable('_loop_index', 0), 'results_count': len(loop_results), 'loop_results': loop_results, 'output_variable': output_var, } # 单节点循环设计:循环完成后从循环节点的输出边继续 next_nodes = edge_map.get(loop_node_id, []) # 过滤掉子节点(parentNode 为循环节点的节点) child_node_ids = self._get_child_nodes(node_map, loop_node_id) next_nodes = [n for n in next_nodes if n not in child_node_ids] current_node_id = next_nodes[0] if next_nodes else None elif result.next_node_id: # 检查是否是分支 ID(条件分支节点返回的是分支 ID,需要通过边映射找到实际目标节点) logger.info(f'[流式] 节点 {current_node_id} 返回 next_node_id={result.next_node_id}') logger.info(f'[流式] parallel_edge_map[{current_node_id}]={parallel_edge_map.get(current_node_id, {})}') if result.next_node_id in parallel_edge_map.get(current_node_id, {}): current_node_id = parallel_edge_map[current_node_id][result.next_node_id] logger.info(f'[流式] 通过分支映射找到目标节点: {current_node_id}') elif result.next_node_id in node_map: # 是实际的节点 ID current_node_id = result.next_node_id logger.info(f'[流式] 直接使用节点 ID: {current_node_id}') else: # 尝试从边映射中查找 logger.warning(f'[流式] 分支 ID {result.next_node_id} 未找到对应边,回退到普通边查找') next_nodes = edge_map.get(current_node_id, []) current_node_id = next_nodes[0] if next_nodes else None else: next_nodes = edge_map.get(current_node_id, []) current_node_id = next_nodes[0] if next_nodes else None async def resume_workflow_stream( self, workflow: AIWorkflow, run: AIWorkflowRun, user_input: Any, conversation_history: List[Dict[str, str]] = None, ): """ 恢复等待中的工作流执行(异步生成器) 注意:此方法需要预先加载好workflow和run对象 Args: workflow: 工作流对象 run: 工作流运行记录 user_input: 用户输入的值 conversation_history: 对话历史(用于 LLM 节点的上下文记忆) Yields: dict: 节点执行状态事件 """ # 发送恢复事件 yield { 'type': 'resume', 'run_id': str(run.id), 'node_id': run.current_node_id, } try: # 恢复执行(异步) async for event in self._resume_workflow_stream(workflow, run, user_input, conversation_history): yield event # 发送完成事件(如果已完成) if run.status == 'completed': yield { 'type': 'complete', 'run_id': str(run.id), 'status': 'completed', 'outputs': run.outputs, 'elapsed_time': run.elapsed_time, 'total_tokens': run.total_tokens, } except Exception as e: logger.exception(f'工作流恢复执行失败: {e}') yield { 'type': 'error', 'run_id': str(run.id), 'node_id': run.current_node_id, 'message': str(e), } async def resume_workflow_stream_async( self, run_id: str, user_input: Any, conversation_history: List[Dict[str, str]] = None, ): """ 异步恢复等待中的工作流执行 Args: run_id: 运行记录 ID user_input: 用户输入的值 conversation_history: 对话历史 Yields: dict: 节点执行状态事件 """ if not self._db: yield {'type': 'error', 'message': '数据库会话未初始化'} return # 查询运行记录 result = await self._db.execute( select(AIWorkflowRun).where(AIWorkflowRun.id == run_id) ) run = result.scalar_one_or_none() if not run: yield {'type': 'error', 'message': f'运行记录不存在: {run_id}'} return if run.status != 'waiting': yield {'type': 'error', 'message': f'工作流不在等待状态: {run.status}'} return # 查询工作流 workflow = await self.get_workflow(run.workflow_id) if not workflow: yield {'type': 'error', 'message': '工作流不存在'} return try: # 使用异步生成器恢复执行 async for event in self.resume_workflow_stream(workflow, run, user_input, conversation_history): yield event # 更新运行记录 if run.status == 'completed': # 更新工作流统计 workflow.run_count = (workflow.run_count or 0) + 1 workflow.success_count = (workflow.success_count or 0) + 1 await self._db.commit() except Exception as e: logger.exception(f'工作流恢复执行失败: {e}') run.status = 'failed' run.error_message = str(e) run.completed_at = datetime.now() run.elapsed_time = int((run.completed_at - run.started_at).total_seconds() * 1000) await self._db.commit() # 发送错误事件给前端 yield { 'type': 'error', 'message': str(e), 'error_message': str(e), 'run_id': run.id, 'workflow_id': workflow.id, } async def _resume_workflow_stream( self, workflow: AIWorkflow, run: AIWorkflowRun, user_input: Any, conversation_history: List[Dict[str, str]] = None, ): """ 恢复工作流执行的内部实现(异步版本) Args: workflow: 工作流对象 run: 工作流运行记录 user_input: 用户输入 conversation_history: 对话历史(用于 LLM 节点的上下文记忆) """ import asyncio definition = workflow.definition nodes = definition.get('nodes', []) edges = definition.get('edges', []) # 构建节点映射和边映射 node_map = {node['id']: node for node in nodes} edge_map = {} parallel_edge_map = {} for edge in edges: source = edge.get('source', '') target = edge.get('target', '') source_handle = edge.get('sourceHandle', '') if source not in edge_map: edge_map[source] = [] edge_map[source].append(target) if source_handle: if source not in parallel_edge_map: parallel_edge_map[source] = {} parallel_edge_map[source][source_handle] = target # 恢复上下文 logger.info(f'[WorkflowService] 恢复工作流,接收到的 user_input 类型: {type(user_input)}, 值: {user_input}') context = NodeContext( workflow_run_id=str(run.id), variables=run.outputs.copy() if run.outputs else {}, user_input=str(user_input) if user_input is not None else '', user_id=str(run.user_id) if run.user_id else '', conversation_history=conversation_history or [], db_session=self._db, ) logger.info(f'[WorkflowService] context.user_input 设置为: {context.user_input}') # 调试:打印恢复时加载的变量 logger.info(f'恢复工作流,从 run.outputs 加载变量,变量键: {list(context.variables.keys())}') node_keys = [k for k in context.variables.keys() if k.startswith('_node_')] logger.info(f'恢复工作流,_node_ 前缀的变量: {node_keys}') # 检查循环相关变量 loop_items = context.variables.get('_loop_items') logger.info(f'恢复工作流,_loop_items 长度={len(loop_items) if loop_items else 0}, _loop_index={context.variables.get("_loop_index")}, _loop_mode={context.variables.get("_loop_mode")}') # 检查是否有旧的 __user_input__ old_user_input = context.variables.get('__user_input__') logger.info(f'[WorkflowService] variables 中的旧 __user_input__: {old_user_input}') # 设置用户输入到上下文(对话流节点会读取这个值) context.variables['__user_input__'] = user_input logger.info(f'[WorkflowService] 更新 variables["__user_input__"] 为: {user_input}') logs = run.execution_log or [] total_tokens = run.total_tokens total_steps = run.total_steps current_node_id = run.current_node_id # 检查是否有循环状态需要恢复 loop_state = run.waiting_config.get('_loop_state') if run.waiting_config else None # 检查是否有子流程状态需要恢复 subflow_state = run.waiting_config.get('_subflow_state') if run.waiting_config else None logger.info(f'恢复工作流,loop_state={loop_state}, subflow_state={bool(subflow_state)}, waiting_config={run.waiting_config}') # 如果有子流程状态,将其添加到上下文的 metadata 中 if subflow_state: context.metadata['_subflow_state'] = subflow_state # 更新状态为运行中 run.status = 'running' run.waiting_config = {} while current_node_id: node_config = node_map.get(current_node_id) if not node_config: break node_type = node_config.get('type', '') node_label = node_config.get('data', {}).get('label', current_node_id) # 获取节点输入 node_inputs = { 'variables': dict(context.variables), 'previous_output': context.previous_output, } # 发送节点开始事件 node_start_event = { 'type': 'node_start', 'node_id': current_node_id, 'node_type': node_type, 'node_label': node_label, 'timestamp': datetime.now().isoformat(), 'inputs': node_inputs, } # 如果在循环中,添加迭代信息 if loop_state: node_start_event['loop_iteration'] = loop_state.get('iteration', 0) yield node_start_event # 结束节点 if node_type == 'end': node_instance = NodeRegistry.create_instance(node_type, node_config.get('data', {})) if node_instance: result = node_instance.execute(context) logs.append(_make_execution_log_entry( node_map, current_node_id, node_type, status='completed', output=result.output, inputs=copy.deepcopy(node_inputs), )) yield { 'type': 'node_complete', 'node_id': current_node_id, 'node_type': node_type, 'status': 'success', 'elapsed_time': 0, 'outputs': {'output': result.output}, } # 更新运行记录,保存结束节点的输出 run.status = 'completed' run.completed_at = datetime.now() run.elapsed_time = int((run.completed_at - run.started_at).total_seconds() * 1000) run.execution_log = logs.copy() # 只有当结束节点有输出时才包含 output 字段 if result.output is not None: run.outputs = { **context.variables, 'output': result.output, # 结束节点的输出内容 } else: run.outputs = dict(context.variables) # 注意:save由调用方处理 break # 创建节点实例并执行(在线程池中运行同步代码) node_instance = NodeRegistry.create_instance(node_type, node_config.get('data', {})) if not node_instance: raise ValueError(f'未知节点类型: {node_type}') start_time = time.time() result = await node_instance.execute_async(context) elapsed = int((time.time() - start_time) * 1000) # 处理节点事件 if result.events: for event in result.events: if event.get('type') == 'message': # 消息事件直接发送为 answer 类型 yield { 'type': 'answer', 'node_id': current_node_id, 'content': event.get('content', ''), } else: # 其他事件保持 node_event 格式 yield { 'type': 'node_event', 'node_id': current_node_id, 'event': event, } # 检查是否需要等待用户输入 if result.waiting_for_input: log_entry = { 'node_id': current_node_id, 'node_type': node_type, 'status': 'waiting', 'output': result.output, 'elapsed_time': elapsed, 'metadata': _node_result_metadata(result), } logs.append(log_entry) run.status = 'waiting' run.current_node_id = current_node_id run.execution_log = logs.copy() # 使用 copy() 确保 SQLAlchemy 检测到变化 run.outputs = dict(context.variables) # 使用 dict() 创建新对象,确保 SQLAlchemy 检测到变化 run.waiting_config = dict(result.waiting_config) if result.waiting_config else {} # 如果在循环中,保存循环状态以便恢复 if loop_state: run.waiting_config['_loop_state'] = loop_state # 注意:save由调用方处理 waiting_event = { 'type': 'waiting_input', 'node_id': current_node_id, 'node_type': node_type, 'config': result.waiting_config, } # 如果在循环中,添加迭代信息 if loop_state: waiting_event['loop_iteration'] = loop_state.get('iteration', 0) yield waiting_event return # 记录日志 log_entry = { 'node_id': current_node_id, 'node_type': node_type, 'status': 'completed' if result.success else 'failed', 'output': result.output, 'error': result.error, 'elapsed_time': elapsed, 'tokens_used': result.tokens_used, 'metadata': _node_result_metadata(result), } logs.append(log_entry) total_tokens += result.tokens_used total_steps += 1 # 清除用户输入(只在第一个节点使用) context.variables.pop('__user_input__', None) # 更新运行记录 run.current_node_id = current_node_id run.execution_log = logs.copy() # 使用 copy() 确保 SQLAlchemy 检测到变化 run.total_tokens = total_tokens run.total_steps = total_steps run.outputs = dict(context.variables) # 使用 dict() 创建新对象,确保 SQLAlchemy 检测到变化 # 注意:save由调用方处理 # 发送节点完成事件 node_complete_event = _apply_node_result_warnings({ 'type': 'node_complete', 'node_id': current_node_id, 'node_type': node_type, 'status': 'success' if result.success else 'failed', 'elapsed_time': elapsed, 'tokens_used': result.tokens_used, 'error_message': result.error if not result.success else None, 'outputs': { 'output': result.output, 'output_variables': result.metadata.get('frontend_output_variables', result.output_variables), }, }, result) # 如果在循环中,添加迭代信息 if loop_state: node_complete_event['loop_iteration'] = loop_state.get('iteration', 0) yield node_complete_event if not result.success: raise ValueError(f'节点执行失败: {result.error}') # 合并输出变量到上下文 context.variables.update(result.output_variables) context.previous_output = result.output # 保存节点输出到命名空间 context.variables[f'_node_{current_node_id}'] = { 'output': result.output, **result.output_variables, } # 确定下一个节点 # 检查当前节点是否是循环节点 if node_type == 'loop': # 循环节点:执行循环体 loop_node_id = current_node_id loop_config = node_config.get('data', {}) yield { 'type': 'loop_start', 'node_id': loop_node_id, 'loop_mode': context.get_variable('_loop_mode'), 'total_iterations': context.get_variable('_loop_total', 0), } # 执行循环(异步) loop_waiting = False async for loop_event in self._execute_loop( node_map, edge_map, parallel_edge_map, loop_node_id, loop_config, context, run, logs ): yield loop_event # 累加循环中的 token 和步骤 if loop_event.get('type') == 'loop_iteration_complete': total_tokens += loop_event.get('tokens_used', 0) total_steps += loop_event.get('steps', 0) # 检查是否因等待用户输入而暂停 if loop_event.get('type') == 'waiting_input': loop_waiting = True # 如果循环因等待用户输入而暂停,停止主工作流执行 if loop_waiting: return # 获取循环结果 loop_results = context.get_variable('_loop_results', []) output_var = loop_config.get('output_variable', 'loop_results') context.set_variable(output_var, loop_results) yield { 'type': 'loop_complete', 'node_id': loop_node_id, 'total_iterations': context.get_variable('_loop_index', 0), 'results_count': len(loop_results), 'loop_results': loop_results, 'output_variable': output_var, } # 单节点循环设计:循环完成后从循环节点的输出边继续 next_nodes = edge_map.get(loop_node_id, []) # 过滤掉子节点(parentNode 为循环节点的节点) child_node_ids = self._get_child_nodes(node_map, loop_node_id) next_nodes = [n for n in next_nodes if n not in child_node_ids] current_node_id = next_nodes[0] if next_nodes else None # 检查是否在循环中恢复执行 elif loop_state: loop_node_id = loop_state.get('loop_node_id') loop_node_config = node_map.get(loop_node_id, {}) loop_config = loop_node_config.get('data', {}) # 获取循环体子节点 child_node_ids = self._get_child_nodes(node_map, loop_node_id) if not child_node_ids: child_node_ids = loop_config.get('childNodes', []) # 构建子节点的边映射 child_edge_map = {} for source_id in child_node_ids: targets = edge_map.get(source_id, []) child_targets = [t for t in targets if t in child_node_ids] if child_targets: child_edge_map[source_id] = child_targets # 找循环体内的下一个节点 if result.next_node_id and result.next_node_id in child_node_ids: current_node_id = result.next_node_id else: next_nodes = child_edge_map.get(current_node_id, []) if next_nodes: current_node_id = next_nodes[0] else: # 循环体内没有更多节点,完成当前迭代并继续循环 logger.info(f'[Resume] 循环体内节点执行完毕,完成当前迭代') # 收集当前迭代结果 loop_results = context.get_variable('_loop_results', []) loop_results.append(result.output) context.set_variable('_loop_results', loop_results) # 更新循环索引(进入下一次迭代) current_loop_index = context.get_variable('_loop_index', 0) context.set_variable('_loop_index', current_loop_index + 1) logger.info(f'[Resume] 更新 _loop_index: {current_loop_index} -> {current_loop_index + 1}') # 发送迭代完成事件 loop_items = context.get_variable('_loop_items', []) yield { 'type': 'loop_iteration_complete', 'node_id': loop_node_id, 'iteration': current_loop_index, 'total': len(loop_items) if loop_items else None, 'output': result.output, } # 继续循环执行 loop_items = context.get_variable('_loop_items', []) loop_index = context.get_variable('_loop_index', 0) logger.info(f'[Resume] 继续循环执行, _loop_items 长度={len(loop_items) if loop_items else 0}, _loop_index={loop_index}') logger.info(f'[Resume] context.variables 中的循环变量: _loop_items={bool(context.variables.get("_loop_items"))}, _loop_mode={context.variables.get("_loop_mode")}') loop_waiting = False async for loop_event in self._execute_loop( node_map, edge_map, parallel_edge_map, loop_node_id, loop_config, context, run, logs ): yield loop_event if loop_event.get('type') == 'waiting_input': loop_waiting = True # 如果循环因等待用户输入而暂停,停止执行 if loop_waiting: return # 循环执行完成,找循环节点之后的节点 next_nodes = edge_map.get(loop_node_id, []) next_nodes = [n for n in next_nodes if n not in child_node_ids] current_node_id = next_nodes[0] if next_nodes else None loop_state = None # 清除循环状态 elif result.next_node_id: # 检查是否是分支 ID(条件分支节点返回的是分支 ID,需要通过边映射找到实际目标节点) if result.next_node_id in parallel_edge_map.get(current_node_id, {}): current_node_id = parallel_edge_map[current_node_id][result.next_node_id] elif result.next_node_id in node_map: # 是实际的节点 ID current_node_id = result.next_node_id else: # 尝试从边映射中查找 next_nodes = edge_map.get(current_node_id, []) current_node_id = next_nodes[0] if next_nodes else None else: next_nodes = edge_map.get(current_node_id, []) current_node_id = next_nodes[0] if next_nodes else None @staticmethod def get_node_schemas() -> List[Dict]: """获取所有节点 Schema""" return NodeRegistry.get_all_schemas() @staticmethod def get_node_schemas_by_category() -> Dict[str, List[Dict]]: """按分类获取节点 Schema""" return NodeRegistry.get_schemas_by_category()