""" 模板渲染节点 """ 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'], }