diff --git a/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/FlowHelper.java b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/FlowHelper.java index 7d9188a..24b914f 100644 --- a/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/FlowHelper.java +++ b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/FlowHelper.java @@ -6,6 +6,7 @@ import cn.hutool.extra.template.TemplateEngine; import cn.hutool.extra.template.TemplateUtil; import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowContext; import java.util.Map; +import lombok.extern.slf4j.Slf4j; import org.eclipse.collections.api.factory.Maps; import org.eclipse.collections.api.map.ImmutableMap; @@ -13,6 +14,7 @@ import org.eclipse.collections.api.map.ImmutableMap; * @author lanyuanxiaoyao * @version 20250711 */ +@Slf4j public class FlowHelper { private static final TemplateEngine TEMPLATE_ENGINE = TemplateUtil.createEngine(); diff --git a/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/node/CodeNode.java b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/node/CodeNode.java index 2bbe750..54395e4 100644 --- a/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/node/CodeNode.java +++ b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/node/CodeNode.java @@ -2,10 +2,13 @@ package com.lanyuanxiaoyao.service.ai.web.engine.node; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.lanyuanxiaoyao.service.ai.web.configuration.SpringBeanGetter; import com.lanyuanxiaoyao.service.ai.web.engine.FlowHelper; import com.lanyuanxiaoyao.service.ai.web.engine.FlowNodeRunner; import com.lanyuanxiaoyao.service.ai.web.engine.node.code.CodeExecutor; import com.lanyuanxiaoyao.service.ai.web.engine.node.code.JavaScriptCodeExecutor; +import com.lanyuanxiaoyao.service.ai.web.engine.node.code.PythonCodeExecutor; import lombok.extern.slf4j.Slf4j; /** @@ -16,14 +19,15 @@ import lombok.extern.slf4j.Slf4j; public class CodeNode extends FlowNodeRunner { @Override public void run() { + var mapper = SpringBeanGetter.getBean(ObjectMapper.class); var inputVariablesMap = FlowHelper.generateInputVariablesMap(getNodeId(), getContext()); var type = this.getData("type"); var script = this.getData("content"); - CodeExecutor executor = null; - switch (type) { - case "javascript": - executor = new JavaScriptCodeExecutor(); - } + CodeExecutor executor = switch (type) { + case "javascript" -> new JavaScriptCodeExecutor(mapper); + case "python" -> new PythonCodeExecutor(mapper); + default -> null; + }; if (ObjectUtil.isNull(executor)) { throw new RuntimeException(StrUtil.format("Unsupported type: {}", type)); } diff --git a/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/node/SwitchNode.java b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/node/SwitchNode.java index e264f8a..74a21f2 100644 --- a/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/node/SwitchNode.java +++ b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/node/SwitchNode.java @@ -1,10 +1,13 @@ package com.lanyuanxiaoyao.service.ai.web.engine.node; import cn.hutool.core.bean.BeanUtil; -import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.StrUtil; import com.lanyuanxiaoyao.service.ai.web.engine.FlowHelper; import com.lanyuanxiaoyao.service.ai.web.engine.FlowNodeOptionalRunner; +import java.math.BigDecimal; +import java.util.Collection; import java.util.List; import java.util.Map; import lombok.extern.slf4j.Slf4j; @@ -44,22 +47,41 @@ public class SwitchNode extends FlowNodeOptionalRunner { var left = FlowHelper.generateVariable(leftVariable, getContext()); var operator = (String) condition.get("op"); var right = condition.get("right"); - if (right instanceof Map) { - var rightVariable = (String) BeanUtil.getProperty(condition, "right.field"); - if (StrUtil.isNotBlank(rightVariable)) { - right = FlowHelper.generateVariable(rightVariable, getContext()); - } + if (left instanceof CharSequence || left instanceof Boolean) { + String source = StrUtil.toStringOrNull(left); + String target = StrUtil.toStringOrNull(right); + return switch (operator) { + case "equal" -> StrUtil.equals(source, target); + case "not_equal" -> !StrUtil.equals(source, target); + case "is_empty" -> StrUtil.isBlank(source); + case "is_not_empty" -> StrUtil.isNotBlank(source); + case "like" -> StrUtil.contains(source, target); + case "not_like" -> !StrUtil.contains(source, target); + case "starts_with" -> StrUtil.startWith(source, target); + case "ends_with" -> StrUtil.endWith(source, target); + default -> false; + }; + } else if (left instanceof Number source) { + var sourceNumber = new BigDecimal(StrUtil.toString(source)); + var targetNumber = new BigDecimal(StrUtil.toString(right)); + return switch (operator) { + case "equal" -> NumberUtil.equals(sourceNumber, targetNumber); + case "not_equal" -> !NumberUtil.equals(sourceNumber, targetNumber); + case "greater" -> NumberUtil.isGreater(sourceNumber, targetNumber); + case "greater_equal" -> NumberUtil.isGreaterOrEqual(sourceNumber, targetNumber); + case "less" -> NumberUtil.isLess(sourceNumber, targetNumber); + case "less_equal" -> NumberUtil.isLessOrEqual(sourceNumber, targetNumber); + default -> false; + }; + } else if (left instanceof Collection source) { + return switch (operator) { + case "is_empty" -> CollectionUtil.isEmpty(source); + case "is_not_empty" -> CollectionUtil.isNotEmpty(source); + case "contain" -> CollectionUtil.safeContains(source, right); + case "not_contain" -> !CollectionUtil.safeContains(source, right); + default -> false; + }; } - return switch (operator) { - case "equal" -> ObjectUtil.equals(left, right); - case "not_equal" -> ObjectUtil.notEqual(left, right); - case "is_empty" -> ObjectUtil.isEmpty(left); - case "is_not_empty" -> ObjectUtil.isNotEmpty(left); - case "like" -> StrUtil.contains((String) left, (String) right); - case "not_like" -> !StrUtil.contains((String) left, (String) right); - case "starts_with" -> StrUtil.startWith((String) left, (String) right); - case "ends_with" -> StrUtil.endWith((String) left, (String) right); - default -> false; - }; + return false; } } diff --git a/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/node/code/JavaScriptCodeExecutor.java b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/node/code/JavaScriptCodeExecutor.java index 78e4014..a810b98 100644 --- a/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/node/code/JavaScriptCodeExecutor.java +++ b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/node/code/JavaScriptCodeExecutor.java @@ -1,18 +1,29 @@ package com.lanyuanxiaoyao.service.ai.web.engine.node.code; import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; import org.eclipse.collections.api.factory.Maps; import org.eclipse.collections.api.map.ImmutableMap; import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Engine; import org.graalvm.polyglot.Source; -import org.graalvm.polyglot.TypeLiteral; /** * @author lanyuanxiaoyao * @version 20250717 */ -public class JavaScriptCodeExecutor implements CodeExecutor{ +@Slf4j +public class JavaScriptCodeExecutor implements CodeExecutor { + private final ObjectMapper mapper; + + public JavaScriptCodeExecutor(ObjectMapper mapper) { + this.mapper = mapper; + } + + @SneakyThrows @Override public ImmutableMap execute(String script, ImmutableMap inputVariablesMap) { if (StrUtil.isBlank(script)) { @@ -26,7 +37,7 @@ public class JavaScriptCodeExecutor implements CodeExecutor{ .build() ) { var bindings = context.getBindings("js"); - inputVariablesMap.forEachKeyValue(bindings::putMember); + bindings.putMember("context", inputVariablesMap); var result = context.eval( Source.create( "js", @@ -34,11 +45,12 @@ public class JavaScriptCodeExecutor implements CodeExecutor{ function process() { %s } - process(); + var result = process(); + JSON.stringify(result? result: {}) """.formatted(script) ) ); - return Maps.immutable.ofAll(result.as(new TypeLiteral<>() {})); + return mapper.readValue(result.asString(), new TypeReference<>() {}); } } } diff --git a/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/node/code/PythonCodeExecutor.java b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/node/code/PythonCodeExecutor.java new file mode 100644 index 0000000..b7c1250 --- /dev/null +++ b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/node/code/PythonCodeExecutor.java @@ -0,0 +1,50 @@ +package com.lanyuanxiaoyao.service.ai.web.engine.node.code; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Arrays; +import java.util.stream.Collectors; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.collections.api.map.ImmutableMap; +import org.python.core.PySystemState; +import org.python.util.PythonInterpreter; + +/** + * @author lanyuanxiaoyao + * @version 20250718 + */ +@Slf4j +public class PythonCodeExecutor implements CodeExecutor { + private final ObjectMapper mapper; + + public PythonCodeExecutor(ObjectMapper mapper) { + this.mapper = mapper; + } + + @SneakyThrows + @Override + public ImmutableMap execute(String script, ImmutableMap inputVariablesMap) { + try (var systemState = new PySystemState()) { + systemState.setdefaultencoding("UTF-8"); + var interpreter = new PythonInterpreter(null, systemState); + script = Arrays.stream(script.split("\n")) + .map(line -> " " + line) + .collect(Collectors.joining("\n")); + interpreter.set("context", inputVariablesMap); + var pythonScript = interpreter.compile( + """ + import json + + def process(): + %s + + result = json.dumps(process()) + """.formatted(script) + ); + interpreter.exec(pythonScript); + var result = interpreter.get("result"); + return mapper.readValue((String) result.__tojava__(String.class), new TypeReference<>() {}); + } + } +} diff --git a/service-ai/service-ai-web/src/test/java/com/lanyuanxiaoyao/service/ai/web/TestCodeExecutor.java b/service-ai/service-ai-web/src/test/java/com/lanyuanxiaoyao/service/ai/web/TestCodeExecutor.java new file mode 100644 index 0000000..08651ee --- /dev/null +++ b/service-ai/service-ai-web/src/test/java/com/lanyuanxiaoyao/service/ai/web/TestCodeExecutor.java @@ -0,0 +1,29 @@ +package com.lanyuanxiaoyao.service.ai.web; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.eclipsecollections.EclipseCollectionsModule; +import com.lanyuanxiaoyao.service.ai.web.engine.node.code.JavaScriptCodeExecutor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.collections.api.factory.Maps; + +/** + * @author lanyuanxiaoyao + * @version 20250717 + */ +@Slf4j +public class TestCodeExecutor { + public static void main(String[] args) { + var mapper = new ObjectMapper(); + mapper.registerModule(new EclipseCollectionsModule()); + var executor = new JavaScriptCodeExecutor(mapper); + var result = executor.execute( + """ + return {'code': 1, 'text': context.get('text')} + """, + Maps.immutable.of( + "text", "hello world" + ) + ); + log.info("Result: {}", result); + } +} diff --git a/service-ai/service-ai-web/src/test/java/com/lanyuanxiaoyao/service/ai/web/TestJsExecutor.java b/service-ai/service-ai-web/src/test/java/com/lanyuanxiaoyao/service/ai/web/TestJsExecutor.java deleted file mode 100644 index e35d7ed..0000000 --- a/service-ai/service-ai-web/src/test/java/com/lanyuanxiaoyao/service/ai/web/TestJsExecutor.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.lanyuanxiaoyao.service.ai.web; - -import cn.hutool.core.util.StrUtil; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.collections.api.factory.Maps; -import org.eclipse.collections.api.map.ImmutableMap; -import org.graalvm.polyglot.Context; -import org.graalvm.polyglot.Engine; -import org.graalvm.polyglot.Source; -import org.graalvm.polyglot.TypeLiteral; - -/** - * @author lanyuanxiaoyao - * @version 20250717 - */ -@Slf4j -public class TestJsExecutor { - public static void main(String[] args) { - var result = executeJavascript( - // language=JavaScript - "return 'hello'", - Maps.immutable.of( - "code", 1 - ) - ); - log.info("Result: {}", result); - } - - private static ImmutableMap executeJavascript(String script, ImmutableMap inputVariablesMap) { - if (StrUtil.isBlank(script)) { - return Maps.immutable.empty(); - } - try (var engin = Engine.create()) { - try ( - var context = Context.newBuilder() - .allowAllAccess(true) - .engine(engin) - .build() - ) { - var bindings = context.getBindings("js"); - inputVariablesMap.forEachKeyValue(bindings::putMember); - var result = context.eval( - Source.create( - "js", - """ - function process() { - %s - } - process(); - """.formatted(script) - ) - ); - return Maps.immutable.ofAll(result.as(new TypeLiteral<>() {})); - } - } - } -}