395 lines
13 KiB
Python
395 lines
13 KiB
Python
"""
|
|
节点基类
|
|
"""
|
|
import logging
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass, field
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class NodeContext:
|
|
"""
|
|
节点执行上下文
|
|
|
|
包含节点执行所需的所有信息
|
|
"""
|
|
# 工作流运行实例
|
|
workflow_run_id: str = ''
|
|
|
|
# 变量存储(所有节点共享)
|
|
variables: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
# 用户输入
|
|
user_input: str = ''
|
|
|
|
# 当前用户
|
|
user_id: str = ''
|
|
|
|
# 对话历史(用于 LLM 节点)
|
|
conversation_history: List[Dict[str, str]] = field(default_factory=list)
|
|
|
|
# 节点配置
|
|
node_config: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
# 上一个节点的输出
|
|
previous_output: Any = None
|
|
|
|
# 元数据
|
|
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
# 数据库会话(用于需要数据库访问的节点)
|
|
db_session: Any = None
|
|
|
|
def get_variable(self, name: str, default: Any = None) -> Any:
|
|
"""获取变量"""
|
|
return self.variables.get(name, default)
|
|
|
|
def set_variable(self, name: str, value: Any) -> None:
|
|
"""设置变量"""
|
|
self.variables[name] = value
|
|
|
|
def resolve_template(self, template: str) -> str:
|
|
"""
|
|
解析模板中的变量引用
|
|
|
|
支持格式:
|
|
- {{variable_name}} - 直接变量引用
|
|
- {{variable_name[0]}} - 数组索引访问
|
|
- {{variable_name[-1]}} - 负数索引(最后一个)
|
|
- {{NodeID.key}} - 节点输出引用(如果存在)
|
|
- {{NodeID.key}}.property - 访问解析结果的嵌套属性
|
|
- {{NodeID.key[0].property}} - 数组索引 + 属性访问
|
|
"""
|
|
import re
|
|
import json
|
|
result = template
|
|
|
|
# 匹配 {{...}} 格式的变量引用,以及可选的后续属性访问 .property1.property2...
|
|
pattern = r'\{\{([^}]+)\}\}((?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)'
|
|
|
|
def get_nested_value(obj, path: str):
|
|
"""从对象中获取嵌套属性值,支持数组索引"""
|
|
if not path:
|
|
return obj
|
|
|
|
# 移除开头的点
|
|
if path.startswith('.'):
|
|
path = path[1:]
|
|
|
|
current = obj
|
|
# 使用正则分割路径,支持 .property 和 [index] 格式
|
|
# 例如: "items[0].name" -> ["items", "[0]", "name"]
|
|
parts = re.split(r'(?=\[)|\.', path)
|
|
|
|
for part in parts:
|
|
if not part:
|
|
continue
|
|
|
|
# 如果是字符串,尝试解析为 JSON
|
|
if isinstance(current, str):
|
|
try:
|
|
current = json.loads(current)
|
|
except (json.JSONDecodeError, TypeError):
|
|
return None
|
|
|
|
# 检查是否是数组索引 [n]
|
|
index_match = re.match(r'\[(-?\d+)\]', part)
|
|
if index_match:
|
|
index = int(index_match.group(1))
|
|
if isinstance(current, (list, tuple)):
|
|
try:
|
|
current = current[index]
|
|
except IndexError:
|
|
return None
|
|
else:
|
|
return None
|
|
# 从字典中获取属性
|
|
elif isinstance(current, dict):
|
|
if part in current:
|
|
current = current[part]
|
|
else:
|
|
return None
|
|
else:
|
|
return None
|
|
|
|
return current
|
|
|
|
def replace_var(match):
|
|
var_ref = match.group(1).strip()
|
|
extra_path = match.group(2) or '' # 额外的属性路径,如 .customer_name
|
|
|
|
value = None
|
|
index_path = '' # 变量名后的索引/属性路径
|
|
|
|
# 检查是否有数组索引 [n],如果有则分割
|
|
# 例如: "loop_results[0]" -> var_name="loop_results", index_path="[0]"
|
|
# 例如: "llm-123.llm_response" -> var_name="llm-123.llm_response", index_path=""
|
|
bracket_pos = var_ref.find('[')
|
|
if bracket_pos > 0:
|
|
var_name = var_ref[:bracket_pos]
|
|
index_path = var_ref[bracket_pos:]
|
|
else:
|
|
var_name = var_ref
|
|
|
|
# 尝试解析 NodeID.key.subkey... 格式(支持多层属性访问)
|
|
if '.' in var_name:
|
|
parts = var_name.split('.')
|
|
node_id = parts[0]
|
|
remaining_path = '.'.join(parts[1:]) # 剩余路径,如 "item.module_name"
|
|
|
|
# 先尝试从节点输出命名空间获取
|
|
node_outputs = self.variables.get(f'_node_{node_id}')
|
|
if isinstance(node_outputs, dict):
|
|
# 尝试获取第一层 key
|
|
first_key = parts[1] if len(parts) > 1 else None
|
|
if first_key and first_key in node_outputs:
|
|
value = node_outputs[first_key]
|
|
# 如果还有更多层级,继续递归获取
|
|
if len(parts) > 2:
|
|
nested_path = '.'.join(parts[2:])
|
|
nested_value = get_nested_value(value, nested_path)
|
|
if nested_value is not None:
|
|
value = nested_value
|
|
|
|
# 回退:尝试直接从变量获取(node_id 作为变量名)
|
|
if value is None and node_id in self.variables:
|
|
node_data = self.variables[node_id]
|
|
if isinstance(node_data, dict):
|
|
first_key = parts[1] if len(parts) > 1 else None
|
|
if first_key and first_key in node_data:
|
|
value = node_data[first_key]
|
|
if len(parts) > 2:
|
|
nested_path = '.'.join(parts[2:])
|
|
nested_value = get_nested_value(value, nested_path)
|
|
if nested_value is not None:
|
|
value = nested_value
|
|
|
|
# 再回退:直接从顶层变量获取完整路径
|
|
if value is None and remaining_path in self.variables:
|
|
value = self.variables[remaining_path]
|
|
else:
|
|
# 直接变量引用
|
|
if var_name in self.variables:
|
|
value = self.variables[var_name]
|
|
|
|
# 如果找到了值,处理索引路径和额外的属性路径
|
|
if value is not None:
|
|
# 合并索引路径和额外路径
|
|
full_path = index_path + extra_path
|
|
if full_path:
|
|
nested_value = get_nested_value(value, full_path)
|
|
if nested_value is not None:
|
|
return str(nested_value)
|
|
# 嵌套属性未找到,返回原始值
|
|
return str(value)
|
|
return str(value)
|
|
|
|
# 未找到变量,保持原样
|
|
return match.group(0)
|
|
|
|
result = re.sub(pattern, replace_var, result)
|
|
return result
|
|
|
|
|
|
@dataclass
|
|
class NodeResult:
|
|
"""
|
|
节点执行结果
|
|
"""
|
|
# 是否成功
|
|
success: bool = True
|
|
|
|
# 输出数据
|
|
output: Any = None
|
|
|
|
# 输出变量(会合并到上下文的 variables 中)
|
|
output_variables: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
# 错误信息
|
|
error: str = ''
|
|
|
|
# 下一个节点 ID(用于条件分支)
|
|
next_node_id: str = ''
|
|
|
|
# Token 使用(LLM 节点)
|
|
tokens_used: int = 0
|
|
|
|
# 耗时(毫秒)
|
|
elapsed_time: int = 0
|
|
|
|
# 元数据
|
|
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
# ========== 对话流相关 ==========
|
|
|
|
# 是否等待用户输入(对话流模式)
|
|
waiting_for_input: bool = False
|
|
|
|
# 等待配置(描述需要什么类型的输入)
|
|
waiting_config: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
# 事件列表(如发送消息)
|
|
events: List[Dict[str, Any]] = field(default_factory=list)
|
|
|
|
# ========== 设计预览相关 ==========
|
|
|
|
# 设计预览数据(用于工作流中显示设计结果并允许编辑)
|
|
preview: Optional[Dict[str, Any]] = None
|
|
|
|
|
|
class BaseNode(ABC):
|
|
"""
|
|
节点基类
|
|
|
|
所有节点必须继承此类并实现 execute 方法
|
|
"""
|
|
|
|
# 节点类型标识(必须唯一)
|
|
node_type: str = ''
|
|
|
|
# 节点显示名称
|
|
node_name: str = ''
|
|
|
|
# 节点分类
|
|
node_category: str = 'basic' # basic, llm, logic, data, tool, knowledge
|
|
|
|
# 节点图标
|
|
node_icon: str = ''
|
|
|
|
# 节点描述
|
|
node_description: str = ''
|
|
|
|
# 输入参数定义
|
|
inputs: List[Dict[str, Any]] = []
|
|
|
|
# 输出参数定义
|
|
outputs: List[Dict[str, Any]] = []
|
|
|
|
# 是否支持多个输出分支
|
|
supports_branches: bool = False
|
|
|
|
def __init__(self, config: Dict[str, Any] = None):
|
|
"""
|
|
初始化节点
|
|
|
|
Args:
|
|
config: 节点配置
|
|
"""
|
|
self.config = config or {}
|
|
|
|
@abstractmethod
|
|
def execute(self, context: NodeContext) -> NodeResult:
|
|
"""
|
|
执行节点
|
|
|
|
Args:
|
|
context: 执行上下文
|
|
|
|
Returns:
|
|
NodeResult
|
|
"""
|
|
pass
|
|
|
|
async def execute_async(self, context: NodeContext) -> NodeResult:
|
|
"""
|
|
异步执行节点(默认调用同步方法)
|
|
|
|
Args:
|
|
context: 执行上下文
|
|
|
|
Returns:
|
|
NodeResult
|
|
"""
|
|
return self.execute(context)
|
|
|
|
def validate_config(self) -> tuple:
|
|
"""
|
|
验证节点配置
|
|
|
|
Returns:
|
|
(is_valid, error_message)
|
|
"""
|
|
return True, ''
|
|
|
|
@classmethod
|
|
def get_schema(cls) -> Dict[str, Any]:
|
|
"""
|
|
获取节点 Schema(供前端渲染)
|
|
|
|
Returns:
|
|
节点 Schema
|
|
"""
|
|
return {
|
|
'type': cls.node_type,
|
|
'name': cls.node_name,
|
|
'category': cls.node_category,
|
|
'icon': cls.node_icon,
|
|
'description': cls.node_description,
|
|
'inputs': cls.inputs,
|
|
'outputs': cls.outputs,
|
|
'supports_branches': cls.supports_branches,
|
|
}
|
|
|
|
@classmethod
|
|
def get_config_schema(cls) -> Dict[str, Any]:
|
|
"""
|
|
获取节点配置 Schema(供前端表单渲染)
|
|
|
|
Returns:
|
|
配置 Schema
|
|
"""
|
|
return {}
|
|
|
|
def resolve_require_confirmation(self, context: NodeContext, default: bool = True) -> bool:
|
|
"""
|
|
解析 require_confirmation 配置
|
|
|
|
支持三种模式:
|
|
1. 布尔值:直接使用 True/False
|
|
2. 字符串 'always'/'never':始终确认/从不确认
|
|
3. 变量引用:{{variable_name}} 格式,解析变量值作为布尔值
|
|
|
|
Args:
|
|
context: 节点执行上下文
|
|
default: 默认值
|
|
|
|
Returns:
|
|
是否需要确认
|
|
"""
|
|
value = self.config.get('require_confirmation', default)
|
|
|
|
# 布尔值直接返回
|
|
if isinstance(value, bool):
|
|
return value
|
|
|
|
# 字符串处理
|
|
if isinstance(value, str):
|
|
value_lower = value.lower().strip()
|
|
|
|
# 固定模式
|
|
if value_lower in ('always', 'true', '1', 'yes'):
|
|
return True
|
|
if value_lower in ('never', 'false', '0', 'no'):
|
|
return False
|
|
|
|
# 变量引用模式:{{variable_name}}
|
|
if '{{' in value and '}}' in value:
|
|
resolved = context.resolve_template(value)
|
|
# 解析后的值转换为布尔值
|
|
if isinstance(resolved, bool):
|
|
return resolved
|
|
if isinstance(resolved, str):
|
|
resolved_lower = resolved.lower().strip()
|
|
if resolved_lower in ('true', '1', 'yes'):
|
|
return True
|
|
if resolved_lower in ('false', '0', 'no'):
|
|
return False
|
|
# 非空字符串视为 True
|
|
return bool(resolved and resolved != value)
|
|
|
|
# 其他情况返回默认值
|
|
return default
|