feat: 默认省略代码节点显式 main 调用
- 新节点仅生成 main 与 return 并同步输入形参 - Python 运行时按配置顺序传入独立参数值 - 保留历史显式调用和单对象参数写法
This commit is contained in:
@@ -4,6 +4,7 @@ import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.ChainState;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.easyagents.flow.core.code.CodeRuntimeEngine;
|
||||
import com.easyagents.flow.core.node.CodeNode;
|
||||
import com.easyagents.flow.core.util.StringUtil;
|
||||
@@ -17,6 +18,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -91,6 +93,7 @@ public class PythonRuntimeEngine implements CodeRuntimeEngine {
|
||||
"",
|
||||
" local_ctx = dict(context)",
|
||||
" local_ctx['_result'] = {}",
|
||||
" initial_result = local_ctx['_result']",
|
||||
" local_ctx['__builtins__'] = __builtins__",
|
||||
"",
|
||||
" captured_stdout = _BoundedTextWriter(max_output_bytes)",
|
||||
@@ -101,12 +104,17 @@ public class PythonRuntimeEngine implements CodeRuntimeEngine {
|
||||
" with contextlib.redirect_stdout(captured_stdout), contextlib.redirect_stderr(captured_stderr):",
|
||||
" exec(code, local_ctx, local_ctx)",
|
||||
" result = local_ctx.get('_result')",
|
||||
" if isinstance(result, dict) and not result:",
|
||||
" legacy_result_assigned = result is not initial_result",
|
||||
" if isinstance(result, dict) and not result and not legacy_result_assigned:",
|
||||
" main_func = local_ctx.get('main')",
|
||||
" if callable(main_func):",
|
||||
" main_invoked = True",
|
||||
" main_input = {name: context.get(name) for name in parameter_names if isinstance(name, str)}",
|
||||
" result = main_func(main_input)",
|
||||
" if payload.get('mainArgsMode') == 'named':",
|
||||
" main_args = [context.get(name) for name in parameter_names]",
|
||||
" result = main_func(*main_args)",
|
||||
" else:",
|
||||
" main_input = {name: context.get(name) for name in parameter_names if isinstance(name, str)}",
|
||||
" result = main_func(main_input)",
|
||||
" except Exception:",
|
||||
" sys.stderr.write(traceback.format_exc())",
|
||||
" sys.exit(2)",
|
||||
@@ -253,9 +261,8 @@ public class PythonRuntimeEngine implements CodeRuntimeEngine {
|
||||
payload.put("context", context);
|
||||
payload.put(
|
||||
"parameterNames",
|
||||
parameterValues == null
|
||||
? Collections.emptyList()
|
||||
: new ArrayList<>(parameterValues.keySet()));
|
||||
resolveParameterNames(node, parameterValues));
|
||||
payload.put("mainArgsMode", node.getMainArgsMode());
|
||||
payload.put("maxOutputBytes", maxOutputBytes);
|
||||
|
||||
String payloadText = JSON.toJSONString(payload);
|
||||
@@ -312,6 +319,30 @@ public class PythonRuntimeEngine implements CodeRuntimeEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按节点配置顺序解析 main 自动调用参数名。
|
||||
*
|
||||
* @param node 当前代码节点
|
||||
* @param parameterValues 已解析的节点参数
|
||||
* @return 有序参数名
|
||||
*/
|
||||
private List<String> resolveParameterNames(
|
||||
CodeNode node, Map<String, Object> parameterValues) {
|
||||
if (node != null
|
||||
&& node.getParameters() != null
|
||||
&& !node.getParameters().isEmpty()) {
|
||||
List<String> names =
|
||||
new ArrayList<>(node.getParameters().size());
|
||||
for (Parameter parameter : node.getParameters()) {
|
||||
names.add(parameter.getName());
|
||||
}
|
||||
return names;
|
||||
}
|
||||
return parameterValues == null
|
||||
? Collections.emptyList()
|
||||
: new ArrayList<>(parameterValues.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建传入 Python 子进程的上下文。
|
||||
*
|
||||
|
||||
@@ -2,12 +2,14 @@ package tech.easyflow.ai.easyagentsflow.code;
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.easyagents.flow.core.chain.repository.InMemoryChainStateRepository;
|
||||
import com.easyagents.flow.core.node.CodeNode;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Assume;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -64,6 +66,38 @@ public class PythonRuntimeEngineTest {
|
||||
Assert.assertEquals(Boolean.FALSE, result.get("has_ambient"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证新节点按配置顺序把输入值传给 main 的独立形参。
|
||||
*/
|
||||
@Test
|
||||
public void testExecuteMainWithNamedArguments() {
|
||||
Assume.assumeTrue(PythonRuntimeEngine.probe(PYTHON_COMMAND, 1500L).isAvailable());
|
||||
|
||||
Chain chain = createChain();
|
||||
CodeNode node = (CodeNode) chain.getDefinition().getNodeById("code-test");
|
||||
node.setParameters(Arrays.asList(
|
||||
new Parameter("data"),
|
||||
new Parameter("suffix")));
|
||||
node.setMainArgsMode(CodeNode.MAIN_ARGS_MODE_NAMED);
|
||||
PythonRuntimeEngine engine = new PythonRuntimeEngine(
|
||||
PYTHON_COMMAND,
|
||||
3000L,
|
||||
65536,
|
||||
System.getProperty("java.io.tmpdir"));
|
||||
|
||||
Map<String, Object> result = engine.execute(
|
||||
String.join("\n",
|
||||
"def main(data, suffix):",
|
||||
" return {'answer': data + suffix}"),
|
||||
node,
|
||||
chain,
|
||||
Map.of(
|
||||
"suffix", "111",
|
||||
"data", "hello"));
|
||||
|
||||
Assert.assertEquals("hello111", result.get("answer"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLegacyResultTakesPrecedenceOverMain() {
|
||||
Assume.assumeTrue(PythonRuntimeEngine.probe(PYTHON_COMMAND, 1500L).isAvailable());
|
||||
@@ -88,6 +122,35 @@ public class PythonRuntimeEngineTest {
|
||||
Assert.assertEquals("legacy", result.get("source"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证显式调用返回空字典时不会再次自动调用 main。
|
||||
*/
|
||||
@Test
|
||||
public void testEmptyExplicitResultDoesNotInvokeMainAgain() {
|
||||
Assume.assumeTrue(PythonRuntimeEngine.probe(PYTHON_COMMAND, 1500L).isAvailable());
|
||||
|
||||
Chain chain = createChain();
|
||||
CodeNode node = (CodeNode) chain.getDefinition().getNodeById("code-test");
|
||||
PythonRuntimeEngine engine = new PythonRuntimeEngine(
|
||||
PYTHON_COMMAND,
|
||||
3000L,
|
||||
65536,
|
||||
System.getProperty("java.io.tmpdir"));
|
||||
|
||||
Map<String, Object> result = engine.execute(
|
||||
String.join("\n",
|
||||
"def main(data):",
|
||||
" if not isinstance(data, str):",
|
||||
" raise RuntimeError('main invoked twice')",
|
||||
" return {}",
|
||||
"_result = main(data)"),
|
||||
node,
|
||||
chain,
|
||||
Map.of("data", "hello"));
|
||||
|
||||
Assert.assertTrue(result.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMainMustReturnDict() {
|
||||
Assume.assumeTrue(PythonRuntimeEngine.probe(PYTHON_COMMAND, 1500L).isAvailable());
|
||||
|
||||
Reference in New Issue
Block a user