fix: execute multica template agents
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -62,6 +65,144 @@ class TemplateNode(BaseNode):
|
||||
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]:
|
||||
"""获取配置 Schema"""
|
||||
@@ -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'],
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 });
|
||||
<div v-if="log.error" class="text-destructive break-words">
|
||||
{{ log.error }}
|
||||
</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 class="text-muted-foreground mb-1 font-medium">
|
||||
{{ $t('ai-platform.workflowRuns.detail.inputsOutputs') }} - 输入
|
||||
|
||||
Reference in New Issue
Block a user