工作流动画效果
This commit is contained in:
@@ -6,6 +6,7 @@ import asyncio
|
||||
from collections import defaultdict, deque
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from app.services.llm_service import llm_service
|
||||
from app.services.condition_parser import condition_parser
|
||||
from app.services.data_transformer import data_transformer
|
||||
@@ -123,10 +124,89 @@ class WorkflowEngine:
|
||||
else:
|
||||
# 否则合并所有输入
|
||||
if isinstance(source_output, dict):
|
||||
input_data.update(source_output)
|
||||
# 如果source_output包含output字段,展开它
|
||||
if 'output' in source_output and isinstance(source_output['output'], dict):
|
||||
# 将output中的内容展开到顶层
|
||||
input_data.update(source_output['output'])
|
||||
# 保留其他字段(如status)
|
||||
for key, value in source_output.items():
|
||||
if key != 'output':
|
||||
input_data[key] = value
|
||||
else:
|
||||
input_data.update(source_output)
|
||||
else:
|
||||
input_data['input'] = source_output
|
||||
|
||||
# 如果input_data中没有query字段,尝试从所有已执行的节点中查找(特别是start节点)
|
||||
if 'query' not in input_data:
|
||||
# 优先查找start节点
|
||||
for node_id_key in ['start-1', 'start']:
|
||||
if node_id_key in node_outputs:
|
||||
node_output = node_outputs[node_id_key]
|
||||
if isinstance(node_output, dict):
|
||||
# 检查顶层字段(因为node_outputs存储的是output字段的内容)
|
||||
if 'query' in node_output:
|
||||
input_data['query'] = node_output['query']
|
||||
logger.debug(f"[rjb] 从节点 {node_id_key} 中获取query: {input_data['query']}")
|
||||
break
|
||||
# 检查output字段(兼容性)
|
||||
elif 'output' in node_output and isinstance(node_output['output'], dict):
|
||||
if 'query' in node_output['output']:
|
||||
input_data['query'] = node_output['output']['query']
|
||||
logger.debug(f"[rjb] 从节点 {node_id_key} 的output中获取query: {input_data['query']}")
|
||||
break
|
||||
|
||||
# 如果还没找到,遍历所有节点
|
||||
if 'query' not in input_data:
|
||||
for node_id_key, node_output in node_outputs.items():
|
||||
if isinstance(node_output, dict):
|
||||
# 检查顶层字段
|
||||
if 'query' in node_output:
|
||||
input_data['query'] = node_output['query']
|
||||
logger.debug(f"[rjb] 从节点 {node_id_key} 中获取query: {input_data['query']}")
|
||||
break
|
||||
# 检查output字段(兼容性)
|
||||
elif 'output' in node_output and isinstance(node_output['output'], dict):
|
||||
if 'query' in node_output['output']:
|
||||
input_data['query'] = node_output['output']['query']
|
||||
logger.debug(f"[rjb] 从节点 {node_id_key} 的output中获取query: {input_data['query']}")
|
||||
break
|
||||
|
||||
# 如果input_data中没有requirement_analysis字段,尝试从所有已执行的节点中查找
|
||||
if 'requirement_analysis' not in input_data:
|
||||
# 优先查找requirement-analysis节点
|
||||
for node_id_key in ['llm-requirement-analysis', 'requirement-analysis']:
|
||||
if node_id_key in node_outputs:
|
||||
node_output = node_outputs[node_id_key]
|
||||
if isinstance(node_output, dict):
|
||||
# 检查顶层字段(因为node_outputs存储的是output字段的内容)
|
||||
if 'requirement_analysis' in node_output:
|
||||
input_data['requirement_analysis'] = node_output['requirement_analysis']
|
||||
logger.debug(f"[rjb] 从节点 {node_id_key} 中获取requirement_analysis")
|
||||
break
|
||||
# 检查output字段(兼容性)
|
||||
elif 'output' in node_output and isinstance(node_output['output'], dict):
|
||||
if 'requirement_analysis' in node_output['output']:
|
||||
input_data['requirement_analysis'] = node_output['output']['requirement_analysis']
|
||||
logger.debug(f"[rjb] 从节点 {node_id_key} 的output中获取requirement_analysis")
|
||||
break
|
||||
|
||||
# 如果还没找到,遍历所有节点
|
||||
if 'requirement_analysis' not in input_data:
|
||||
for node_id_key, node_output in node_outputs.items():
|
||||
if isinstance(node_output, dict):
|
||||
# 检查顶层字段
|
||||
if 'requirement_analysis' in node_output:
|
||||
input_data['requirement_analysis'] = node_output['requirement_analysis']
|
||||
logger.debug(f"[rjb] 从节点 {node_id_key} 中获取requirement_analysis")
|
||||
break
|
||||
# 检查output字段(兼容性)
|
||||
elif 'output' in node_output and isinstance(node_output['output'], dict):
|
||||
if 'requirement_analysis' in node_output['output']:
|
||||
input_data['requirement_analysis'] = node_output['output']['requirement_analysis']
|
||||
logger.debug(f"[rjb] 从节点 {node_id_key} 的output中获取requirement_analysis")
|
||||
break
|
||||
|
||||
logger.debug(f"[rjb] 节点输入结果: node_id={node_id}, input_data={input_data}")
|
||||
return input_data
|
||||
|
||||
@@ -344,27 +424,50 @@ class WorkflowEngine:
|
||||
try:
|
||||
# 将input_data转换为字符串用于格式化
|
||||
if isinstance(input_data, dict):
|
||||
# 如果prompt中包含变量,尝试格式化
|
||||
if '{' in prompt and '}' in prompt:
|
||||
# 尝试格式化所有input_data中的键
|
||||
formatted_prompt = prompt
|
||||
for key, value in input_data.items():
|
||||
placeholder = f'{{{key}}}'
|
||||
if placeholder in formatted_prompt:
|
||||
formatted_prompt = formatted_prompt.replace(
|
||||
placeholder,
|
||||
json_module.dumps(value, ensure_ascii=False) if isinstance(value, (dict, list)) else str(value)
|
||||
)
|
||||
# 如果还有{input}占位符,替换为整个input_data
|
||||
if '{input}' in formatted_prompt:
|
||||
# 支持两种格式的变量:{key} 和 {{key}}
|
||||
formatted_prompt = prompt
|
||||
has_unfilled_variables = False
|
||||
|
||||
# 首先处理 {{variable}} 格式(模板节点常用)
|
||||
import re
|
||||
double_brace_vars = re.findall(r'\{\{(\w+)\}\}', prompt)
|
||||
for var_name in double_brace_vars:
|
||||
if var_name in input_data:
|
||||
# 替换 {{variable}} 为实际值
|
||||
value = input_data[var_name]
|
||||
replacement = json_module.dumps(value, ensure_ascii=False) if isinstance(value, (dict, list)) else str(value)
|
||||
formatted_prompt = formatted_prompt.replace(f'{{{{{var_name}}}}}', replacement)
|
||||
else:
|
||||
has_unfilled_variables = True
|
||||
|
||||
# 然后处理 {key} 格式
|
||||
for key, value in input_data.items():
|
||||
placeholder = f'{{{key}}}'
|
||||
if placeholder in formatted_prompt:
|
||||
formatted_prompt = formatted_prompt.replace(
|
||||
'{input}',
|
||||
json_module.dumps(input_data, ensure_ascii=False)
|
||||
placeholder,
|
||||
json_module.dumps(value, ensure_ascii=False) if isinstance(value, (dict, list)) else str(value)
|
||||
)
|
||||
prompt = formatted_prompt
|
||||
else:
|
||||
# 如果没有占位符,将input_data作为JSON附加到prompt
|
||||
prompt = f"{prompt}\n\n输入数据:\n{json_module.dumps(input_data, ensure_ascii=False)}"
|
||||
|
||||
# 如果还有{input}占位符,替换为整个input_data
|
||||
if '{input}' in formatted_prompt:
|
||||
formatted_prompt = formatted_prompt.replace(
|
||||
'{input}',
|
||||
json_module.dumps(input_data, ensure_ascii=False)
|
||||
)
|
||||
|
||||
# 如果仍有未填充的变量({{variable}}格式),将用户输入作为上下文附加
|
||||
if has_unfilled_variables or re.search(r'\{\{(\w+)\}\}', formatted_prompt):
|
||||
# 提取用户的实际查询内容
|
||||
user_query = input_data.get('query', input_data.get('input', input_data.get('text', '')))
|
||||
if not user_query and isinstance(input_data, dict):
|
||||
# 如果没有明确的query字段,尝试从整个input_data中提取文本内容
|
||||
user_query = json_module.dumps(input_data, ensure_ascii=False)
|
||||
|
||||
if user_query:
|
||||
formatted_prompt = f"{formatted_prompt}\n\n用户需求:{user_query}\n\n请根据以上用户需求,忽略未填充的变量占位符(如{{{{variable}}}}),直接基于用户需求来完成任务。"
|
||||
|
||||
prompt = formatted_prompt
|
||||
else:
|
||||
# 如果input_data不是dict,直接转换为字符串
|
||||
if '{input}' in prompt:
|
||||
@@ -373,6 +476,7 @@ class WorkflowEngine:
|
||||
prompt = f"{prompt}\n\n输入:{str(input_data)}"
|
||||
except Exception as e:
|
||||
# 格式化失败,使用原始prompt和input_data
|
||||
logger.warning(f"[rjb] Prompt格式化失败: {str(e)}")
|
||||
try:
|
||||
prompt = f"{prompt}\n\n输入数据:\n{json_module.dumps(input_data, ensure_ascii=False)}"
|
||||
except:
|
||||
@@ -381,8 +485,27 @@ class WorkflowEngine:
|
||||
# 获取LLM配置
|
||||
provider = node_data.get('provider', 'openai')
|
||||
model = node_data.get('model', 'gpt-3.5-turbo')
|
||||
temperature = node_data.get('temperature', 0.7)
|
||||
max_tokens = node_data.get('max_tokens')
|
||||
# 确保temperature是浮点数(节点模板中可能是字符串)
|
||||
temperature_raw = node_data.get('temperature', 0.7)
|
||||
if isinstance(temperature_raw, str):
|
||||
try:
|
||||
temperature = float(temperature_raw)
|
||||
except (ValueError, TypeError):
|
||||
temperature = 0.7
|
||||
else:
|
||||
temperature = float(temperature_raw) if temperature_raw is not None else 0.7
|
||||
# 确保max_tokens是整数(节点模板中可能是字符串)
|
||||
max_tokens_raw = node_data.get('max_tokens')
|
||||
if max_tokens_raw is not None:
|
||||
if isinstance(max_tokens_raw, str):
|
||||
try:
|
||||
max_tokens = int(max_tokens_raw)
|
||||
except (ValueError, TypeError):
|
||||
max_tokens = None
|
||||
else:
|
||||
max_tokens = int(max_tokens_raw) if max_tokens_raw is not None else None
|
||||
else:
|
||||
max_tokens = None
|
||||
# 不传递 api_key 和 base_url,让 LLM 服务使用系统默认配置(与节点测试保持一致)
|
||||
api_key = None
|
||||
base_url = None
|
||||
@@ -461,13 +584,68 @@ class WorkflowEngine:
|
||||
mode = node_data.get('mode', 'mapping')
|
||||
|
||||
try:
|
||||
result = data_transformer.transform_data(
|
||||
input_data=input_data,
|
||||
mapping=mapping,
|
||||
filter_rules=filter_rules,
|
||||
compute_rules=compute_rules,
|
||||
mode=mode
|
||||
)
|
||||
# 处理mapping中的{{variable}}格式,从input_data中提取值
|
||||
# 首先,如果input_data包含output字段,需要展开它
|
||||
expanded_input = input_data.copy()
|
||||
if 'output' in input_data and isinstance(input_data['output'], dict):
|
||||
# 将output中的内容展开到顶层,但保留output字段
|
||||
expanded_input.update(input_data['output'])
|
||||
|
||||
processed_mapping = {}
|
||||
import re
|
||||
for target_key, source_expr in mapping.items():
|
||||
if isinstance(source_expr, str):
|
||||
# 支持{{variable}}格式
|
||||
double_brace_vars = re.findall(r'\{\{(\w+)\}\}', source_expr)
|
||||
if double_brace_vars:
|
||||
# 从expanded_input中获取变量值
|
||||
var_value = None
|
||||
for var_name in double_brace_vars:
|
||||
# 尝试从expanded_input中获取,支持嵌套路径
|
||||
var_value = self._get_nested_value(expanded_input, var_name)
|
||||
if var_value is not None:
|
||||
break
|
||||
|
||||
if var_value is not None:
|
||||
# 如果只有一个变量,直接使用值;否则替换表达式
|
||||
if len(double_brace_vars) == 1:
|
||||
processed_mapping[target_key] = var_value
|
||||
else:
|
||||
# 多个变量,替换表达式
|
||||
processed_expr = source_expr
|
||||
for var_name in double_brace_vars:
|
||||
var_val = self._get_nested_value(expanded_input, var_name)
|
||||
if var_val is not None:
|
||||
replacement = json_module.dumps(var_val, ensure_ascii=False) if isinstance(var_val, (dict, list)) else str(var_val)
|
||||
processed_expr = processed_expr.replace(f'{{{{{var_name}}}}}', replacement)
|
||||
processed_mapping[target_key] = processed_expr
|
||||
else:
|
||||
# 变量不存在,保持原表达式
|
||||
processed_mapping[target_key] = source_expr
|
||||
else:
|
||||
# 不是{{variable}}格式,直接使用
|
||||
processed_mapping[target_key] = source_expr
|
||||
else:
|
||||
# 不是字符串,直接使用
|
||||
processed_mapping[target_key] = source_expr
|
||||
|
||||
# 如果mode是merge,需要合并所有输入数据
|
||||
if mode == 'merge':
|
||||
# 合并所有上游节点的输出(使用展开后的数据)
|
||||
result = expanded_input.copy()
|
||||
# 添加mapping的结果
|
||||
for key, value in processed_mapping.items():
|
||||
result[key] = value
|
||||
else:
|
||||
# 使用处理后的mapping进行转换(使用展开后的数据)
|
||||
result = data_transformer.transform_data(
|
||||
input_data=expanded_input,
|
||||
mapping=processed_mapping,
|
||||
filter_rules=filter_rules,
|
||||
compute_rules=compute_rules,
|
||||
mode=mode
|
||||
)
|
||||
|
||||
exec_result = {'output': result, 'status': 'success'}
|
||||
if self.logger:
|
||||
duration = int((time.time() - start_time) * 1000)
|
||||
@@ -477,6 +655,7 @@ class WorkflowEngine:
|
||||
if self.logger:
|
||||
duration = int((time.time() - start_time) * 1000)
|
||||
self.logger.log_node_error(node_id, node_type, e, duration)
|
||||
logger.error(f"[rjb] Transform节点执行失败: {str(e)}", exc_info=True)
|
||||
return {
|
||||
'output': None,
|
||||
'status': 'failed',
|
||||
@@ -1430,45 +1609,120 @@ class WorkflowEngine:
|
||||
|
||||
elif node_type == 'output' or node_type == 'end':
|
||||
# 输出节点:返回最终结果
|
||||
# 对于人机交互场景,End节点应该返回纯文本字符串,而不是JSON
|
||||
logger.debug(f"[rjb] End节点处理: node_id={node_id}, input_data={input_data}, input_data type={type(input_data)}")
|
||||
# 读取节点配置中的输出格式设置
|
||||
node_data = node.get('data', {})
|
||||
output_format = node_data.get('output_format', 'text') # 默认纯文本
|
||||
|
||||
logger.debug(f"[rjb] End节点处理: node_id={node_id}, output_format={output_format}, input_data={input_data}, input_data type={type(input_data)}")
|
||||
final_output = input_data
|
||||
|
||||
# 递归解包,提取实际的文本内容
|
||||
if isinstance(input_data, dict):
|
||||
# 如果只有一个 key 且是 'input',提取其值
|
||||
if len(input_data) == 1 and 'input' in input_data:
|
||||
final_output = input_data['input']
|
||||
logger.debug(f"[rjb] End节点提取第一层: final_output={final_output}, type={type(final_output)}")
|
||||
# 如果提取的值仍然是字典且只有一个 'input' key,继续提取
|
||||
if isinstance(final_output, dict) and len(final_output) == 1 and 'input' in final_output:
|
||||
final_output = final_output['input']
|
||||
logger.debug(f"[rjb] End节点提取第二层: final_output={final_output}, type={type(final_output)}")
|
||||
|
||||
# 确保最终输出是字符串(对于人机交互场景)
|
||||
# 如果是字典,尝试转换为字符串;如果是其他类型,也转换为字符串
|
||||
if not isinstance(final_output, str):
|
||||
if isinstance(final_output, dict):
|
||||
# 如果是字典,尝试提取文本内容或转换为JSON字符串
|
||||
# 优先查找常见的文本字段
|
||||
if 'text' in final_output:
|
||||
final_output = str(final_output['text'])
|
||||
elif 'content' in final_output:
|
||||
final_output = str(final_output['content'])
|
||||
elif 'message' in final_output:
|
||||
final_output = str(final_output['message'])
|
||||
elif 'response' in final_output:
|
||||
final_output = str(final_output['response'])
|
||||
elif len(final_output) == 1:
|
||||
# 如果只有一个key,直接使用其值
|
||||
final_output = str(list(final_output.values())[0])
|
||||
else:
|
||||
# 否则转换为JSON字符串
|
||||
final_output = json_module.dumps(final_output, ensure_ascii=False)
|
||||
# 如果配置为JSON格式,直接返回原始数据(或格式化的JSON)
|
||||
if output_format == 'json':
|
||||
# 如果是字典,直接返回JSON格式
|
||||
if isinstance(input_data, dict):
|
||||
final_output = json_module.dumps(input_data, ensure_ascii=False, indent=2)
|
||||
elif isinstance(input_data, str):
|
||||
# 尝试解析为JSON,如果成功则格式化,否则直接返回
|
||||
try:
|
||||
parsed = json_module.loads(input_data)
|
||||
final_output = json_module.dumps(parsed, ensure_ascii=False, indent=2)
|
||||
except:
|
||||
final_output = input_data
|
||||
else:
|
||||
final_output = str(final_output)
|
||||
final_output = json_module.dumps({'output': input_data}, ensure_ascii=False, indent=2)
|
||||
else:
|
||||
# 默认纯文本格式:递归解包,提取实际的文本内容
|
||||
if isinstance(input_data, dict):
|
||||
# 优先提取 'output' 字段(LLM节点的标准输出格式)
|
||||
if 'output' in input_data and isinstance(input_data['output'], str):
|
||||
final_output = input_data['output']
|
||||
# 如果只有一个 key 且是 'input',提取其值
|
||||
elif len(input_data) == 1 and 'input' in input_data:
|
||||
final_output = input_data['input']
|
||||
# 如果包含 'solution' 字段,提取其值
|
||||
elif 'solution' in input_data and isinstance(input_data['solution'], str):
|
||||
final_output = input_data['solution']
|
||||
# 如果input_data是字符串类型的字典(JSON字符串),尝试解析
|
||||
elif isinstance(input_data, str):
|
||||
try:
|
||||
parsed = json_module.loads(input_data)
|
||||
if isinstance(parsed, dict) and 'output' in parsed:
|
||||
final_output = parsed['output']
|
||||
elif isinstance(parsed, str):
|
||||
final_output = parsed
|
||||
except:
|
||||
final_output = input_data
|
||||
logger.debug(f"[rjb] End节点提取第一层: final_output={final_output}, type={type(final_output)}")
|
||||
# 如果提取的值仍然是字典且只有一个 'input' key,继续提取
|
||||
if isinstance(final_output, dict) and len(final_output) == 1 and 'input' in final_output:
|
||||
final_output = final_output['input']
|
||||
logger.debug(f"[rjb] End节点提取第二层: final_output={final_output}, type={type(final_output)}")
|
||||
|
||||
# 确保最终输出是字符串(对于人机交互场景)
|
||||
# 如果是字典,尝试转换为字符串;如果是其他类型,也转换为字符串
|
||||
if not isinstance(final_output, str):
|
||||
if isinstance(final_output, dict):
|
||||
# 如果是字典,尝试提取文本内容或转换为JSON字符串
|
||||
# 优先查找常见的文本字段
|
||||
if 'text' in final_output:
|
||||
final_output = str(final_output['text'])
|
||||
elif 'content' in final_output:
|
||||
final_output = str(final_output['content'])
|
||||
elif 'message' in final_output:
|
||||
final_output = str(final_output['message'])
|
||||
elif 'response' in final_output:
|
||||
final_output = str(final_output['response'])
|
||||
elif len(final_output) == 1:
|
||||
# 如果只有一个key,直接使用其值
|
||||
final_output = str(list(final_output.values())[0])
|
||||
else:
|
||||
# 否则转换为纯文本(不是JSON)
|
||||
# 尝试提取所有文本字段并组合,但排除系统字段
|
||||
text_parts = []
|
||||
exclude_keys = {'status', 'error', 'timestamp', 'node_id', 'execution_time'}
|
||||
for key, value in final_output.items():
|
||||
if key in exclude_keys:
|
||||
continue
|
||||
if isinstance(value, str) and value.strip():
|
||||
# 如果值本身已经包含 "key: " 格式,直接使用
|
||||
if value.strip().startswith(f"{key}:"):
|
||||
text_parts.append(value.strip())
|
||||
else:
|
||||
text_parts.append(value.strip())
|
||||
elif isinstance(value, (int, float, bool)):
|
||||
text_parts.append(f"{key}: {value}")
|
||||
if text_parts:
|
||||
final_output = "\n".join(text_parts)
|
||||
else:
|
||||
final_output = str(final_output)
|
||||
else:
|
||||
final_output = str(final_output)
|
||||
|
||||
# 清理输出文本:移除常见的字段前缀(如 "input: ", "query: " 等)
|
||||
if isinstance(final_output, str):
|
||||
import re
|
||||
# 移除行首的 "input: ", "query: ", "output: " 等前缀
|
||||
lines = final_output.split('\n')
|
||||
cleaned_lines = []
|
||||
for line in lines:
|
||||
# 匹配行首的 "字段名: " 格式并移除
|
||||
# 但保留内容本身
|
||||
line = re.sub(r'^(input|query|output|result|response|message|content|text):\s*', '', line, flags=re.IGNORECASE)
|
||||
if line.strip(): # 只保留非空行
|
||||
cleaned_lines.append(line)
|
||||
|
||||
# 如果清理后还有内容,使用清理后的版本
|
||||
if cleaned_lines:
|
||||
final_output = '\n'.join(cleaned_lines)
|
||||
# 如果清理后为空,使用原始输出(避免丢失所有内容)
|
||||
elif final_output.strip():
|
||||
# 如果原始输出不为空,但清理后为空,说明可能格式特殊,尝试更宽松的清理
|
||||
# 只移除明显的 "input: " 和 "query: " 前缀,保留其他内容
|
||||
final_output = re.sub(r'^(input|query):\s*', '', final_output, flags=re.IGNORECASE | re.MULTILINE)
|
||||
if not final_output.strip():
|
||||
final_output = str(input_data) # 如果还是空,使用原始输入
|
||||
|
||||
logger.debug(f"[rjb] End节点最终输出: final_output={final_output}, type={type(final_output)}")
|
||||
logger.debug(f"[rjb] End节点最终输出: output_format={output_format}, final_output={final_output[:100] if isinstance(final_output, str) else type(final_output)}")
|
||||
result = {'output': final_output, 'status': 'success'}
|
||||
if self.logger:
|
||||
duration = int((time.time() - start_time) * 1000)
|
||||
|
||||
Reference in New Issue
Block a user