Build lightweight AI agent admin

This commit is contained in:
Codex
2026-06-08 18:14:59 +08:00
commit e164840f43
2530 changed files with 435693 additions and 0 deletions
@@ -0,0 +1,405 @@
"""
循环节点
支持两种循环模式:
1. for_each - 遍历数组,对每个元素执行循环体
2. while - 条件循环,满足条件时持续执行
"""
import ast
import json
import logging
import operator
from typing import Any, Dict, List
from ..base import BaseNode, NodeContext, NodeResult
from ..registry import NodeRegistry
logger = logging.getLogger(__name__)
@NodeRegistry.register
class LoopNode(BaseNode):
"""
循环节点
支持 for_each 和 while 两种循环模式
"""
node_type = 'loop'
node_name = '循环'
node_category = 'logic'
node_icon = 'repeat'
node_description = '循环执行一组节点,支持遍历数组或条件循环'
supports_branches = True
inputs = [
{
'name': 'items',
'type': 'array',
'description': '要遍历的数组(for_each 模式)',
},
]
outputs = [
{
'name': 'results',
'type': 'array',
'description': '每次循环的结果数组',
},
{
'name': 'current_item',
'type': 'any',
'description': '当前循环项(循环体内可用)',
},
{
'name': 'current_index',
'type': 'number',
'description': '当前循环索引(从 0 开始)',
},
]
# 支持的操作符(用于 while 条件判断)
OPERATORS = {
'eq': operator.eq, # 等于
'ne': operator.ne, # 不等于
'gt': operator.gt, # 大于
'gte': operator.ge, # 大于等于
'lt': operator.lt, # 小于
'lte': operator.le, # 小于等于
'is_empty': lambda a, _: not a, # 为空
'is_not_empty': lambda a, _: bool(a), # 不为空
'is_true': lambda a, _: bool(a), # 为真
'is_false': lambda a, _: not bool(a), # 为假
}
def execute(self, context: NodeContext) -> NodeResult:
"""
执行循环节点
循环节点本身只负责初始化循环状态,实际的循环执行由 WorkflowService 处理
"""
try:
loop_mode = self.config.get('loop_mode', 'for_each')
max_iterations = self.config.get('max_iterations', 100)
if loop_mode == 'for_each':
return self._init_for_each(context, max_iterations)
elif loop_mode == 'while':
return self._init_while(context, max_iterations)
else:
return NodeResult(
success=False,
error=f'不支持的循环模式: {loop_mode}',
)
except Exception as e:
logger.exception(f'循环节点执行失败: {e}')
return NodeResult(
success=False,
error=str(e),
)
def _init_for_each(self, context: NodeContext, max_iterations: int) -> NodeResult:
"""初始化 for_each 循环"""
items_var = self.config.get('items_variable', '')
logger.info(f'[LoopNode] 初始化 for_each 循环,items_variable={items_var}')
# 解析数组变量
items = self._resolve_variable(items_var, context)
logger.info(f'[LoopNode] 解析后的 items 类型: {type(items).__name__}, 值: {items}')
# 如果是字符串,尝试解析为 JSON 或 Python 字面量
if isinstance(items, str):
try:
items = json.loads(items)
logger.info(f'[LoopNode] JSON 解析成功,items={items}')
except json.JSONDecodeError:
# 尝试使用 ast.literal_eval 解析 Python 字面量(如 str() 输出的列表)
try:
items = ast.literal_eval(items)
logger.info(f'[LoopNode] Python 字面量解析成功,items={items}')
except (ValueError, SyntaxError):
# 尝试按逗号分割(仅当不是列表/字典格式时)
if not (items.strip().startswith('[') or items.strip().startswith('{')):
items = [item.strip() for item in items.split(',') if item.strip()]
logger.info(f'[LoopNode] 按逗号分割,items={items}')
else:
logger.error(f'[LoopNode] 无法解析 items 字符串: {items[:200]}...')
return NodeResult(
success=False,
error=f'无法解析 items 变量,格式不正确',
)
if not isinstance(items, (list, tuple)):
error_msg = f'items 必须是数组,当前类型: {type(items).__name__}, 值: {items}, items_variable={items_var}'
logger.error(f'[LoopNode] {error_msg}')
return NodeResult(
success=False,
error=error_msg,
)
# 检查数组是否为空
if len(items) == 0:
logger.warning(f'[LoopNode] items 数组为空,items_variable={items_var}')
# 返回成功但不执行循环体
# 限制最大迭代次数
if len(items) > max_iterations:
logger.warning(f'数组长度 {len(items)} 超过最大迭代次数 {max_iterations},将被截断')
items = list(items)[:max_iterations]
return NodeResult(
success=True,
output={
'items': list(items),
'total_count': len(items),
},
output_variables={
'_loop_items': list(items),
'_loop_index': 0,
'_loop_total': len(items),
'_loop_mode': 'for_each',
'_loop_results': [],
},
metadata={
'is_loop': True,
'loop_mode': 'for_each',
'total_iterations': len(items),
'max_iterations': max_iterations,
},
)
def _init_while(self, context: NodeContext, max_iterations: int) -> NodeResult:
"""初始化 while 循环"""
# 检查初始条件
condition_met = self._evaluate_condition(context)
return NodeResult(
success=True,
output={
'condition_met': condition_met,
},
output_variables={
'_loop_index': 0,
'_loop_mode': 'while',
'_loop_condition_met': condition_met,
'_loop_results': [],
'_loop_max_iterations': max_iterations,
},
metadata={
'is_loop': True,
'loop_mode': 'while',
'condition_met': condition_met,
'max_iterations': max_iterations,
},
)
def check_continue(self, context: NodeContext) -> bool:
"""
检查是否继续循环
由 WorkflowService 在每次循环迭代后调用
"""
loop_mode = context.get_variable('_loop_mode')
current_index = context.get_variable('_loop_index', 0)
max_iterations = self.config.get('max_iterations', 100)
# 检查最大迭代次数
if current_index >= max_iterations:
logger.warning(f'达到最大迭代次数 {max_iterations},停止循环')
return False
if loop_mode == 'for_each':
total = context.get_variable('_loop_total', 0)
return current_index < total
elif loop_mode == 'while':
return self._evaluate_condition(context)
return False
def get_current_item(self, context: NodeContext) -> Any:
"""获取当前循环项(for_each 模式)"""
items = context.get_variable('_loop_items', [])
index = context.get_variable('_loop_index', 0)
if 0 <= index < len(items):
return items[index]
return None
def increment_index(self, context: NodeContext) -> int:
"""增加循环索引"""
current_index = context.get_variable('_loop_index', 0)
new_index = current_index + 1
context.set_variable('_loop_index', new_index)
return new_index
def add_result(self, context: NodeContext, result: Any) -> None:
"""添加循环结果"""
results = context.get_variable('_loop_results', [])
results.append(result)
context.set_variable('_loop_results', results)
def _resolve_variable(self, variable: str, context: NodeContext) -> Any:
"""解析变量引用,支持多层路径访问"""
if not isinstance(variable, str):
return variable
# 如果是空字符串,返回 None
if not variable.strip():
return None
# 检查是否是 {{...}} 格式的变量引用
if not (variable.startswith('{{') and variable.endswith('}}')):
# 不是变量引用,直接返回原始字符串值(可能是硬编码的 JSON 数组或逗号分隔的值)
return variable
# 使用 resolve_template 解析变量,它支持多层路径访问(如 node.key.subkey
resolved = context.resolve_template(variable)
# 如果解析结果与原始变量相同,说明变量不存在或解析失败
if resolved == variable:
return None
# 如果解析结果是字符串,尝试解析为 JSON
if isinstance(resolved, str):
try:
import json
return json.loads(resolved)
except (json.JSONDecodeError, TypeError):
return resolved
return resolved
def _evaluate_condition(self, context: NodeContext) -> bool:
"""评估 while 条件"""
conditions = self.config.get('conditions', [])
logic = self.config.get('condition_logic', 'and') # and 或 or
if not conditions:
return False # 无条件默认不继续
results = []
for condition in conditions:
result = self._evaluate_single_condition(condition, context)
results.append(result)
if logic == 'or':
return any(results)
else: # and
return all(results)
def _evaluate_single_condition(self, condition: Dict, context: NodeContext) -> bool:
"""评估单个条件"""
variable = condition.get('variable', '')
op_name = condition.get('operator', 'eq')
value = condition.get('value', '')
# 解析变量和值
left_val = self._resolve_variable(variable, context)
right_val = self._resolve_variable(value, context) if value else None
# 获取操作符函数
op_func = self.OPERATORS.get(op_name, operator.eq)
try:
# 特殊处理不需要右值的操作符
if op_name in ['is_empty', 'is_not_empty', 'is_true', 'is_false']:
return op_func(left_val, None)
# 尝试类型转换
if isinstance(left_val, (int, float)) and isinstance(right_val, str):
try:
if '.' in right_val:
right_val = float(right_val)
else:
right_val = int(right_val)
except ValueError:
pass
return op_func(left_val, right_val)
except Exception as e:
logger.warning(f'条件评估失败: {e}')
return False
@classmethod
def get_config_schema(cls) -> Dict[str, Any]:
"""获取配置 Schema"""
return {
'type': 'object',
'properties': {
'loop_mode': {
'type': 'string',
'title': '循环模式',
'enum': ['for_each', 'while'],
'enumNames': ['遍历数组 (For Each)', '条件循环 (While)'],
'default': 'for_each',
},
'items_variable': {
'type': 'string',
'title': '数组变量',
'description': '要遍历的数组变量名(for_each 模式)',
},
'item_variable_name': {
'type': 'string',
'title': '循环项变量名',
'description': '当前循环项的变量名,默认为 item',
'default': 'item',
},
'index_variable_name': {
'type': 'string',
'title': '索引变量名',
'description': '当前索引的变量名,默认为 index',
'default': 'index',
},
'conditions': {
'type': 'array',
'title': '循环条件',
'description': 'while 模式的循环条件',
'items': {
'type': 'object',
'properties': {
'variable': {
'type': 'string',
'title': '变量',
},
'operator': {
'type': 'string',
'title': '操作符',
'enum': list(cls.OPERATORS.keys()),
'default': 'eq',
},
'value': {
'type': 'string',
'title': '比较值',
},
},
},
},
'condition_logic': {
'type': 'string',
'title': '条件逻辑',
'enum': ['and', 'or'],
'enumNames': ['全部满足 (AND)', '任一满足 (OR)'],
'default': 'and',
},
'max_iterations': {
'type': 'integer',
'title': '最大迭代次数',
'description': '防止无限循环,默认 100 次',
'default': 100,
'minimum': 1,
'maximum': 10000,
},
'output_variable': {
'type': 'string',
'title': '输出变量名',
'description': '存储所有循环结果的变量名',
'default': 'loop_results',
},
},
}