Files
ai-agent-admin/backend-fastapi/ai_platform/nodes/builtin/template_node.py
T

236 lines
8.6 KiB
Python

"""
模板渲染节点
"""
import logging
import time
from typing import Any, Dict
from sqlalchemy import select
from ..base import BaseNode, NodeContext, NodeResult
from ..registry import NodeRegistry
logger = logging.getLogger(__name__)
@NodeRegistry.register
class TemplateNode(BaseNode):
"""
模板渲染节点
使用变量渲染模板字符串
"""
node_type = 'template'
node_name = '模板'
node_category = 'data'
node_icon = 'file-text'
node_description = '使用变量渲染模板字符串'
inputs = [
{
'name': 'template',
'type': 'string',
'description': '模板字符串',
},
]
outputs = [
{
'name': 'result',
'type': 'string',
'description': '渲染结果',
},
]
def execute(self, context: NodeContext) -> NodeResult:
"""执行模板渲染"""
try:
template = self.config.get('template', '')
output_variable = self.config.get('output_variable', 'template_result')
# 渲染模板
result = context.resolve_template(template)
return NodeResult(
success=True,
output=result,
output_variables={output_variable: result},
)
except Exception as e:
logger.exception(f'模板节点执行失败: {e}')
return NodeResult(
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]:
"""获取配置 Schema"""
return {
'type': 'object',
'properties': {
'template': {
'type': 'string',
'title': '模板',
'description': '支持变量引用,如 {{variable_name}}',
'format': 'textarea',
},
'output_variable': {
'type': 'string',
'title': '输出变量名',
'default': 'template_result',
},
'agent_code': {
'type': 'string',
'title': '智能体编码',
'description': '配置后会以该智能体身份调用模型执行模板任务',
},
'model_id': {
'type': 'string',
'title': '覆盖模型 ID',
'description': '为空时使用智能体默认模型',
},
},
'required': ['template'],
}