feat: 默认省略代码节点显式 main 调用

- 新节点仅生成 main 与 return 并同步输入形参

- Python 运行时按配置顺序传入独立参数值

- 保留历史显式调用和单对象参数写法
This commit is contained in:
2026-08-03 16:58:20 +08:00
parent 46398d1365
commit a75ae9a843
7 changed files with 232 additions and 51 deletions

View File

@@ -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 子进程的上下文。
*

View File

@@ -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());