fix: execute multica template agents

This commit is contained in:
2026-06-13 12:07:51 +08:00
parent 9632c1b4a6
commit 47da899ff3
4 changed files with 225 additions and 20 deletions
@@ -2,8 +2,11 @@
模板渲染节点 模板渲染节点
""" """
import logging import logging
import time
from typing import Any, Dict from typing import Any, Dict
from sqlalchemy import select
from ..base import BaseNode, NodeContext, NodeResult from ..base import BaseNode, NodeContext, NodeResult
from ..registry import NodeRegistry from ..registry import NodeRegistry
@@ -61,6 +64,144 @@ class TemplateNode(BaseNode):
success=False, success=False,
error=str(e), 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 @classmethod
def get_config_schema(cls) -> Dict[str, Any]: def get_config_schema(cls) -> Dict[str, Any]:
@@ -79,6 +220,16 @@ class TemplateNode(BaseNode):
'title': '输出变量名', 'title': '输出变量名',
'default': 'template_result', 'default': 'template_result',
}, },
'agent_code': {
'type': 'string',
'title': '智能体编码',
'description': '配置后会以该智能体身份调用模型执行模板任务',
},
'model_id': {
'type': 'string',
'title': '覆盖模型 ID',
'description': '为空时使用智能体默认模型',
},
}, },
'required': ['template'], 'required': ['template'],
} }
@@ -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: def _snapshot_node_inputs(context: NodeContext) -> dict:
return { return {
'variables': dict(context.variables), 'variables': dict(context.variables),
@@ -535,6 +539,7 @@ class AIWorkflowService:
node_type, node_type,
status='completed', status='completed',
output=result.output, output=result.output,
metadata=_node_result_metadata(result),
inputs=copy.deepcopy(node_inputs), inputs=copy.deepcopy(node_inputs),
)) ))
break break
@@ -563,6 +568,7 @@ class AIWorkflowService:
error=result.error, error=result.error,
elapsed_time=elapsed, elapsed_time=elapsed,
tokens_used=result.tokens_used, tokens_used=result.tokens_used,
metadata=_node_result_metadata(result),
inputs=copy.deepcopy(node_inputs), inputs=copy.deepcopy(node_inputs),
)) ))
@@ -725,6 +731,7 @@ class AIWorkflowService:
'error': result.error, 'error': result.error,
'elapsed_time': elapsed, 'elapsed_time': elapsed,
'tokens_used': result.tokens_used, 'tokens_used': result.tokens_used,
'metadata': _node_result_metadata(result),
'branch': branch_start_id, 'branch': branch_start_id,
}) })
@@ -940,6 +947,7 @@ class AIWorkflowService:
'error': result.error, 'error': result.error,
'elapsed_time': elapsed, 'elapsed_time': elapsed,
'tokens_used': result.tokens_used, 'tokens_used': result.tokens_used,
'metadata': _node_result_metadata(result),
'branch': branch_id, 'branch': branch_id,
}) })
@@ -1228,6 +1236,7 @@ class AIWorkflowService:
'error': result.error, 'error': result.error,
'elapsed_time': elapsed, 'elapsed_time': elapsed,
'tokens_used': result.tokens_used, 'tokens_used': result.tokens_used,
'metadata': _node_result_metadata(result),
'loop_iteration': iteration, 'loop_iteration': iteration,
}) })
@@ -1733,6 +1742,7 @@ class AIWorkflowService:
error=result.error, error=result.error,
elapsed_time=elapsed, elapsed_time=elapsed,
tokens_used=result.tokens_used, tokens_used=result.tokens_used,
metadata=_node_result_metadata(result),
inputs=copy.deepcopy(node_inputs), inputs=copy.deepcopy(node_inputs),
) )
logs.append(log_entry) logs.append(log_entry)
@@ -2201,6 +2211,7 @@ class AIWorkflowService:
'status': 'waiting', 'status': 'waiting',
'output': result.output, 'output': result.output,
'elapsed_time': elapsed, 'elapsed_time': elapsed,
'metadata': _node_result_metadata(result),
} }
logs.append(log_entry) logs.append(log_entry)
@@ -2235,6 +2246,7 @@ class AIWorkflowService:
'error': result.error, 'error': result.error,
'elapsed_time': elapsed, 'elapsed_time': elapsed,
'tokens_used': result.tokens_used, 'tokens_used': result.tokens_used,
'metadata': _node_result_metadata(result),
} }
logs.append(log_entry) logs.append(log_entry)
@@ -53,21 +53,12 @@ def validate_fixture(fixture: Dict[str, Any]) -> Dict[str, int]:
edges = definition.get("edges", []) edges = definition.get("edges", [])
agent_codes = {agent.get("code") for agent in agents} agent_codes = {agent.get("code") for agent in agents}
workflow_agent_codes = { workflow_agent_codes = {
node.get("agent_code") (node.get("data") or {}).get("agent_code")
for node in nodes 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} 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 = { required_persona_fields = {
"role", "role",
"skills", "skills",
@@ -75,10 +66,8 @@ def validate_fixture(fixture: Dict[str, Any]) -> Dict[str, int]:
"background", "background",
"examples", "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_workflow_agents = sorted(workflow_agent_codes - agent_codes)
missing_persona_fields = { missing_persona_fields = {
agent.get("code"): sorted(required_persona_fields - set((agent.get("persona") or {}).keys())) 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") project_manager_workflow = (project_manager or {}).get("workflow_code")
if ( if (
missing_agents missing_workflow_agents
or extra_agents
or missing_workflow_agents
or missing_persona_fields or missing_persona_fields
or missing_node_types or missing_node_types
or project_manager_workflow != "multica_org_collaboration_flow" or project_manager_workflow != "multica_org_collaboration_flow"
): ):
raise ValueError( raise ValueError(
f"Fixture validation failed: missing_agents={missing_agents}, " f"Fixture validation failed: missing_workflow_agents={missing_workflow_agents}, "
f"extra_agents={extra_agents}, "
f"missing_workflow_agents={missing_workflow_agents}, "
f"missing_persona_fields={missing_persona_fields}, " f"missing_persona_fields={missing_persona_fields}, "
f"missing_node_types={missing_node_types}, " f"missing_node_types={missing_node_types}, "
f"project_manager_workflow={project_manager_workflow}" 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: async def upsert_workflow(session, workflow_payload: Dict[str, Any]) -> AIWorkflow:
from sqlalchemy import select 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 from app.base_model import generate_nanoid
count = 0 count = 0
default_chat_model_id = await resolve_default_chat_model_id(session)
for agent_fixture in fixture["agents"]: for agent_fixture in fixture["agents"]:
payload = build_agent_payload(agent_fixture, workflow_id) 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"])) result = await session.execute(select(Agent).where(Agent.code == payload["code"]))
agent = result.scalar_one_or_none() agent = result.scalar_one_or_none()
if agent: if agent:
@@ -148,6 +148,10 @@ function previewValue(value: any) {
} }
} }
function getLogMeta(log: any) {
return log?.metadata || {};
}
function selectLog(nodeId?: string) { function selectLog(nodeId?: string) {
if (!nodeId) return; if (!nodeId) return;
activeNodeId.value = nodeId; activeNodeId.value = nodeId;
@@ -287,6 +291,34 @@ defineExpose({ open });
<div v-if="log.error" class="text-destructive break-words"> <div v-if="log.error" class="text-destructive break-words">
{{ log.error }} {{ log.error }}
</div> </div>
<div
v-if="getLogMeta(log).agent_code"
class="border-border bg-muted/40 grid grid-cols-2 gap-2 rounded-md border p-2"
>
<div>
<div class="text-muted-foreground">智能体</div>
<div class="font-medium">
{{ getLogMeta(log).agent_name || getLogMeta(log).agent_code }}
</div>
</div>
<div>
<div class="text-muted-foreground">编码</div>
<div class="font-mono">{{ getLogMeta(log).agent_code }}</div>
</div>
<div>
<div class="text-muted-foreground">模型</div>
<div class="break-words">
{{ getLogMeta(log).model || getLogMeta(log).model_id || '-' }}
</div>
</div>
<div>
<div class="text-muted-foreground">Prompt / Completion</div>
<div>
{{ getLogMeta(log).prompt_tokens || 0 }} /
{{ getLogMeta(log).completion_tokens || 0 }}
</div>
</div>
</div>
<div> <div>
<div class="text-muted-foreground mb-1 font-medium"> <div class="text-muted-foreground mb-1 font-medium">
{{ $t('ai-platform.workflowRuns.detail.inputsOutputs') }} - 输入 {{ $t('ai-platform.workflowRuns.detail.inputsOutputs') }} - 输入