254 lines
9.3 KiB
Python
254 lines
9.3 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:
|
|
model_id = await self._resolve_default_chat_model_id(context)
|
|
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
|
|
async def _resolve_default_chat_model_id(context: NodeContext) -> str:
|
|
from ai_platform.models import LLMModel
|
|
|
|
result = await context.db_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 ''
|
|
|
|
@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'],
|
|
}
|