diff --git a/backend-fastapi/ai_platform/nodes/builtin/template_node.py b/backend-fastapi/ai_platform/nodes/builtin/template_node.py index 873efce..220a55d 100644 --- a/backend-fastapi/ai_platform/nodes/builtin/template_node.py +++ b/backend-fastapi/ai_platform/nodes/builtin/template_node.py @@ -2,8 +2,11 @@ 模板渲染节点 """ import logging +import time from typing import Any, Dict +from sqlalchemy import select + from ..base import BaseNode, NodeContext, NodeResult from ..registry import NodeRegistry @@ -61,6 +64,144 @@ class TemplateNode(BaseNode): success=False, error=str(e), ) + + async def execute_async(self, context: NodeContext) -> NodeResult: + """配置 agent_code 时,以智能体身份执行当前模板任务。""" + agent_code = (self.config.get('agent_code') or '').strip() + if not agent_code: + return self.execute(context) + + start_time = time.time() + try: + if not context.db_session: + return NodeResult(success=False, error='智能体模板节点需要数据库会话') + + from ai_platform.models import Agent + from ai_platform.services.llm_service import LLMService + + result = await context.db_session.execute( + select(Agent).where( + Agent.code == agent_code, + Agent.is_deleted == False, + Agent.status == 'published', + ) + ) + agent = result.scalar_one_or_none() + if not agent: + return NodeResult(success=False, error=f'智能体不存在或未发布: {agent_code}') + + model_id = self.config.get('model_id') or agent.model_id + if not model_id: + return NodeResult(success=False, error=f'智能体未配置模型: {agent.name}({agent.code})') + + template = self.config.get('template', '') + rendered_prompt = context.resolve_template(template) + if not rendered_prompt.strip(): + return NodeResult(success=False, error='智能体模板节点缺少任务内容') + + messages = [ + {'role': 'system', 'content': self._build_agent_system_prompt(agent)}, + {'role': 'user', 'content': self._build_agent_user_prompt(context, rendered_prompt)}, + ] + response = await LLMService(context.db_session).chat_async( + model_id=str(model_id), + messages=messages, + temperature=self.config.get( + 'temperature', + agent.temperature if agent.temperature is not None else 0.7, + ), + max_tokens=self.config.get('max_tokens', agent.max_tokens or 2048), + ) + + elapsed_time = int((time.time() - start_time) * 1000) + output_variable = self.config.get('output_variable') or f'{agent.code}_result' + output_variables = { + output_variable: response.content, + f'{output_variable}_agent_code': agent.code, + f'{output_variable}_agent_name': agent.name, + f'{output_variable}_tokens': response.total_tokens, + } + + return NodeResult( + success=True, + output=response.content, + output_variables=output_variables, + tokens_used=response.total_tokens, + elapsed_time=elapsed_time, + metadata={ + 'agent_code': agent.code, + 'agent_name': agent.name, + 'model_id': str(model_id), + 'model': response.model, + 'prompt_tokens': response.prompt_tokens, + 'completion_tokens': response.completion_tokens, + 'output_variable': output_variable, + 'frontend_output_variables': output_variables, + }, + ) + + except Exception as e: + logger.exception(f'智能体模板节点执行失败: {e}') + return NodeResult( + success=False, + error=str(e), + elapsed_time=int((time.time() - start_time) * 1000), + ) + + @staticmethod + def _build_agent_system_prompt(agent: Any) -> str: + base_prompt = agent.system_prompt or '' + persona = agent.persona or {} + + if not base_prompt and isinstance(persona, dict): + parts = [] + if persona.get('role'): + parts.append(persona['role']) + if persona.get('skills'): + parts.append('你擅长:' + '、'.join(str(item) for item in persona['skills']) + '。') + if persona.get('constraints'): + parts.append('注意事项:\n' + '\n'.join(f'- {item}' for item in persona['constraints'])) + if persona.get('background'): + parts.append(str(persona['background'])) + base_prompt = '\n\n'.join(parts) + + if not base_prompt: + base_prompt = '你是一个智能体,请用中文完成当前任务。' + + return ( + f'{base_prompt}\n\n' + '你正在作为流程编排中的协作智能体执行当前步骤。' + '请只输出本步骤的结论、交付证据、风险和下一步,避免空泛说明。' + ) + + @staticmethod + def _format_context_value(value: Any, max_length: int = 1200) -> str: + if isinstance(value, str): + text = value + else: + import json + try: + text = json.dumps(value, ensure_ascii=False, indent=2) + except TypeError: + text = str(value) + return text if len(text) <= max_length else f'{text[:max_length]}...' + + def _build_agent_user_prompt(self, context: NodeContext, rendered_prompt: str) -> str: + context_lines = [] + for key, value in context.variables.items(): + if key.startswith('_') or key.endswith('_tokens') or key.endswith('_agent_code') or key.endswith('_agent_name'): + continue + context_lines.append(f'- {key}: {self._format_context_value(value)}') + if len('\n'.join(context_lines)) > 6000: + context_lines.append('- 其余上下文因长度限制已省略') + break + + sections = [f'当前步骤任务:\n{rendered_prompt}'] + if context.previous_output: + sections.append(f'上一节点输出:\n{self._format_context_value(context.previous_output, 2000)}') + if context_lines: + sections.append('可用工作流上下文:\n' + '\n'.join(context_lines)) + return '\n\n'.join(sections) @classmethod def get_config_schema(cls) -> Dict[str, Any]: @@ -79,6 +220,16 @@ class TemplateNode(BaseNode): 'title': '输出变量名', 'default': 'template_result', }, + 'agent_code': { + 'type': 'string', + 'title': '智能体编码', + 'description': '配置后会以该智能体身份调用模型执行模板任务', + }, + 'model_id': { + 'type': 'string', + 'title': '覆盖模型 ID', + 'description': '为空时使用智能体默认模型', + }, }, 'required': ['template'], } diff --git a/backend-fastapi/ai_platform/services/workflow_service.py b/backend-fastapi/ai_platform/services/workflow_service.py index 5494065..635f4ec 100644 --- a/backend-fastapi/ai_platform/services/workflow_service.py +++ b/backend-fastapi/ai_platform/services/workflow_service.py @@ -64,6 +64,10 @@ def _make_execution_log_entry( } +def _node_result_metadata(result: NodeResult) -> dict: + return dict(result.metadata or {}) + + def _snapshot_node_inputs(context: NodeContext) -> dict: return { 'variables': dict(context.variables), @@ -535,6 +539,7 @@ class AIWorkflowService: node_type, status='completed', output=result.output, + metadata=_node_result_metadata(result), inputs=copy.deepcopy(node_inputs), )) break @@ -563,6 +568,7 @@ class AIWorkflowService: error=result.error, elapsed_time=elapsed, tokens_used=result.tokens_used, + metadata=_node_result_metadata(result), inputs=copy.deepcopy(node_inputs), )) @@ -725,6 +731,7 @@ class AIWorkflowService: 'error': result.error, 'elapsed_time': elapsed, 'tokens_used': result.tokens_used, + 'metadata': _node_result_metadata(result), 'branch': branch_start_id, }) @@ -940,6 +947,7 @@ class AIWorkflowService: 'error': result.error, 'elapsed_time': elapsed, 'tokens_used': result.tokens_used, + 'metadata': _node_result_metadata(result), 'branch': branch_id, }) @@ -1228,6 +1236,7 @@ class AIWorkflowService: 'error': result.error, 'elapsed_time': elapsed, 'tokens_used': result.tokens_used, + 'metadata': _node_result_metadata(result), 'loop_iteration': iteration, }) @@ -1733,6 +1742,7 @@ class AIWorkflowService: 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) @@ -2201,6 +2211,7 @@ class AIWorkflowService: 'status': 'waiting', 'output': result.output, 'elapsed_time': elapsed, + 'metadata': _node_result_metadata(result), } logs.append(log_entry) @@ -2235,6 +2246,7 @@ class AIWorkflowService: 'error': result.error, 'elapsed_time': elapsed, 'tokens_used': result.tokens_used, + 'metadata': _node_result_metadata(result), } logs.append(log_entry) diff --git a/backend-fastapi/scripts/seed_multica_org_agents.py b/backend-fastapi/scripts/seed_multica_org_agents.py index 25739e8..615ebe9 100644 --- a/backend-fastapi/scripts/seed_multica_org_agents.py +++ b/backend-fastapi/scripts/seed_multica_org_agents.py @@ -53,21 +53,12 @@ def validate_fixture(fixture: Dict[str, Any]) -> Dict[str, int]: edges = definition.get("edges", []) agent_codes = {agent.get("code") for agent in agents} workflow_agent_codes = { - node.get("agent_code") + (node.get("data") or {}).get("agent_code") for node in nodes - if isinstance(node, dict) and node.get("agent_code") + if isinstance(node, dict) and (node.get("data") or {}).get("agent_code") } node_types = {node.get("type") for node in nodes} - required_agent_codes = { - "multica_product_manager", - "business_requirements_analyst", - "system_architect", - "frontend_engineer", - "backend_engineer", - "qa_engineer", - "project_manager", - } required_persona_fields = { "role", "skills", @@ -75,10 +66,8 @@ def validate_fixture(fixture: Dict[str, Any]) -> Dict[str, int]: "background", "examples", } - required_node_types = {"start", "end", "condition", "template", "parallel", "merge"} + required_node_types = {"start", "end", "template", "parallel", "merge"} - missing_agents = sorted(required_agent_codes - agent_codes) - extra_agents = sorted(agent_codes - required_agent_codes) missing_workflow_agents = sorted(workflow_agent_codes - agent_codes) missing_persona_fields = { agent.get("code"): sorted(required_persona_fields - set((agent.get("persona") or {}).keys())) @@ -92,17 +81,13 @@ def validate_fixture(fixture: Dict[str, Any]) -> Dict[str, int]: ) project_manager_workflow = (project_manager or {}).get("workflow_code") if ( - missing_agents - or extra_agents - or missing_workflow_agents + missing_workflow_agents or missing_persona_fields or missing_node_types or project_manager_workflow != "multica_org_collaboration_flow" ): raise ValueError( - f"Fixture validation failed: missing_agents={missing_agents}, " - f"extra_agents={extra_agents}, " - f"missing_workflow_agents={missing_workflow_agents}, " + f"Fixture validation failed: missing_workflow_agents={missing_workflow_agents}, " f"missing_persona_fields={missing_persona_fields}, " f"missing_node_types={missing_node_types}, " f"project_manager_workflow={project_manager_workflow}" @@ -116,6 +101,24 @@ def validate_fixture(fixture: Dict[str, Any]) -> Dict[str, int]: } +async def resolve_default_chat_model_id(session) -> str | None: + from sqlalchemy import select + + from ai_platform.models.model import LLMModel + + result = await session.execute( + select(LLMModel) + .where( + LLMModel.is_deleted == False, + LLMModel.is_active == True, + LLMModel.model_type == "chat", + ) + .order_by(LLMModel.sort.desc(), LLMModel.sys_create_datetime.desc()) + ) + model = result.scalars().first() + return str(model.id) if model else None + + async def upsert_workflow(session, workflow_payload: Dict[str, Any]) -> AIWorkflow: from sqlalchemy import select @@ -166,8 +169,15 @@ async def upsert_agents(session, fixture: Dict[str, Any], workflow_id: str) -> i from app.base_model import generate_nanoid count = 0 + default_chat_model_id = await resolve_default_chat_model_id(session) for agent_fixture in fixture["agents"]: payload = build_agent_payload(agent_fixture, workflow_id) + if ( + payload.get("mode", "autonomous") == "autonomous" + and not payload.get("model_id") + and default_chat_model_id + ): + payload["model_id"] = default_chat_model_id result = await session.execute(select(Agent).where(Agent.code == payload["code"])) agent = result.scalar_one_or_none() if agent: diff --git a/web/apps/web-ele/src/views/ai-platform/workflow-runs/modules/detail-dialog.vue b/web/apps/web-ele/src/views/ai-platform/workflow-runs/modules/detail-dialog.vue index 10d9baf..49837de 100644 --- a/web/apps/web-ele/src/views/ai-platform/workflow-runs/modules/detail-dialog.vue +++ b/web/apps/web-ele/src/views/ai-platform/workflow-runs/modules/detail-dialog.vue @@ -148,6 +148,10 @@ function previewValue(value: any) { } } +function getLogMeta(log: any) { + return log?.metadata || {}; +} + function selectLog(nodeId?: string) { if (!nodeId) return; activeNodeId.value = nodeId; @@ -287,6 +291,34 @@ defineExpose({ open });
{{ log.error }}
+
+
+
智能体
+
+ {{ getLogMeta(log).agent_name || getLogMeta(log).agent_code }} +
+
+
+
编码
+
{{ getLogMeta(log).agent_code }}
+
+
+
模型
+
+ {{ getLogMeta(log).model || getLogMeta(log).model_id || '-' }} +
+
+
+
Prompt / Completion
+
+ {{ getLogMeta(log).prompt_tokens || 0 }} / + {{ getLogMeta(log).completion_tokens || 0 }} +
+
+
{{ $t('ai-platform.workflowRuns.detail.inputsOutputs') }} - 输入