diff --git a/service-ai/service-ai-web/pom.xml b/service-ai/service-ai-web/pom.xml index d216b41..ce3546c 100644 --- a/service-ai/service-ai-web/pom.xml +++ b/service-ai/service-ai-web/pom.xml @@ -86,6 +86,11 @@ org.noear solon-ai-dialect-openai + + com.yomahub + liteflow-script-graaljs + ${liteflow.version} + org.hibernate.orm diff --git a/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/WebApplication.java b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/WebApplication.java index 3e4c180..1d7bdd1 100644 --- a/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/WebApplication.java +++ b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/WebApplication.java @@ -27,6 +27,8 @@ import org.springframework.scheduling.annotation.EnableScheduling; public class WebApplication implements ApplicationRunner { public static void main(String[] args) { + System.setProperty("polyglot.engine.WarnInterpreterOnly", "false"); + SpringApplication.run(WebApplication.class, args); } diff --git a/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/FlowGraphRunner.java b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/FlowGraphRunner.java index 81bfe16..65e16ae 100644 --- a/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/FlowGraphRunner.java +++ b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/FlowGraphRunner.java @@ -1,5 +1,6 @@ package com.lanyuanxiaoyao.service.ai.web.engine; +import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowContext; @@ -44,7 +45,9 @@ public final class FlowGraphRunner { flowStore.init(flowGraph); for (FlowNode node : flowGraph.nodes()) { - executionQueue.offer(node); + if (ObjectUtil.isNull(node.parentId())) { + executionQueue.offer(node); + } } while (!executionQueue.isEmpty()) { var node = executionQueue.poll(); @@ -77,8 +80,17 @@ public final class FlowGraphRunner { var runner = runnerClazz.getDeclaredConstructor().newInstance(); runner.setNodeId(node.id()); runner.setContext(context); + + // 处理子流程节点的逻辑 + if (runner instanceof FlowNodeSubflowRunner subflowRunner) { + var subflowNodes = flowGraph.nodes().select(n -> StrUtil.equals(n.parentId(), node.id())); + var subGraph = new FlowGraph(IdUtil.fastUUID(), subflowNodes, flowGraph.edges()); + subflowRunner.setSubGraph(subGraph); + } + runner.run(); + // 处理选择节点的逻辑 if (runner instanceof FlowNodeOptionalRunner) { var targetPoint = ((FlowNodeOptionalRunner) runner).getTargetPoint(); for (FlowEdge edge : nodeOutputMap.get(node.id())) { diff --git a/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/FlowNodeSubflowRunner.java b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/FlowNodeSubflowRunner.java new file mode 100644 index 0000000..650c6cb --- /dev/null +++ b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/FlowNodeSubflowRunner.java @@ -0,0 +1,17 @@ +package com.lanyuanxiaoyao.service.ai.web.engine; + +import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowGraph; +import lombok.Getter; +import lombok.Setter; + +/** + * 包含子流程的流程 + * + * @author lanyuanxiaoyao + * @version 20250717 + */ +public abstract class FlowNodeSubflowRunner extends FlowNodeRunner { + @Getter + @Setter + private FlowGraph subGraph; +} diff --git a/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/entity/FlowEdge.java b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/entity/FlowEdge.java index 2e5b0dc..7d8f5b0 100644 --- a/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/entity/FlowEdge.java +++ b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/entity/FlowEdge.java @@ -1,5 +1,6 @@ package com.lanyuanxiaoyao.service.ai.web.engine.entity; +import com.fasterxml.jackson.annotation.JsonProperty; import java.time.LocalDateTime; /** @@ -12,7 +13,9 @@ public record FlowEdge( String id, String source, String target, + @JsonProperty("sourceHandle") String sourcePoint, + @JsonProperty("targetHandle") String targetPoint ) { public enum Status { diff --git a/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/entity/FlowNode.java b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/entity/FlowNode.java index 50a65f1..52e5898 100644 --- a/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/entity/FlowNode.java +++ b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/entity/FlowNode.java @@ -10,7 +10,8 @@ import java.time.LocalDateTime; */ public record FlowNode( String id, - String type + String type, + String parentId ) { public enum Status { INITIAL, RUNNING, FINISHED, SKIPPED 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 new file mode 100644 index 0000000..2bbe750 --- /dev/null +++ b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/node/CodeNode.java @@ -0,0 +1,33 @@ +package com.lanyuanxiaoyao.service.ai.web.engine.node; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +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 lombok.extern.slf4j.Slf4j; + +/** + * @author lanyuanxiaoyao + * @version 20250717 + */ +@Slf4j +public class CodeNode extends FlowNodeRunner { + @Override + public void run() { + 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(); + } + if (ObjectUtil.isNull(executor)) { + throw new RuntimeException(StrUtil.format("Unsupported type: {}", type)); + } + var result = executor.execute(script, inputVariablesMap); + result.forEachKeyValue(this::setData); + } +} diff --git a/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/node/LoopNode.java b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/node/LoopNode.java new file mode 100644 index 0000000..bd476ab --- /dev/null +++ b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/node/LoopNode.java @@ -0,0 +1,18 @@ +package com.lanyuanxiaoyao.service.ai.web.engine.node; + +import com.lanyuanxiaoyao.service.ai.web.engine.FlowNodeSubflowRunner; +import lombok.extern.slf4j.Slf4j; + +/** + * 循环节点 + * + * @author lanyuanxiaoyao + * @version 20250717 + */ +@Slf4j +public class LoopNode extends FlowNodeSubflowRunner { + @Override + public void run() { + log.info("{}", getSubGraph()); + } +} 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 new file mode 100644 index 0000000..e264f8a --- /dev/null +++ b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/node/SwitchNode.java @@ -0,0 +1,65 @@ +package com.lanyuanxiaoyao.service.ai.web.engine.node; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.lanyuanxiaoyao.service.ai.web.engine.FlowHelper; +import com.lanyuanxiaoyao.service.ai.web.engine.FlowNodeOptionalRunner; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; + +/** + * @author lanyuanxiaoyao + * @version 20250717 + */ +@SuppressWarnings("unchecked") +@Slf4j +public class SwitchNode extends FlowNodeOptionalRunner { + @Override + public String runOptional() { + var conditions = this.>>getData("conditions"); + for (Map item : conditions) { + var condition = (Map) item.getOrDefault("condition", Map.of()); + var id = (String) condition.getOrDefault("id", ""); + var conjunction = (String) condition.getOrDefault("conjunction", "and"); + var conditionChildren = ((List>) condition.getOrDefault("children", List.>of())); + if ( + StrUtil.equals(conjunction, "and") + && conditionChildren.stream().allMatch(this::check) + ) { + return id; + } else if ( + StrUtil.equals(conjunction, "or") + && conditionChildren.stream().anyMatch(this::check) + ) { + return id; + } + } + return ""; + } + + private Boolean check(Map condition) { + var leftVariable = (String) BeanUtil.getProperty(condition, "left.field"); + 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()); + } + } + 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; + }; + } +} diff --git a/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/node/code/CodeExecutor.java b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/node/code/CodeExecutor.java new file mode 100644 index 0000000..0d56297 --- /dev/null +++ b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/node/code/CodeExecutor.java @@ -0,0 +1,11 @@ +package com.lanyuanxiaoyao.service.ai.web.engine.node.code; + +import org.eclipse.collections.api.map.ImmutableMap; + +/** + * @author lanyuanxiaoyao + * @version 20250717 + */ +public interface CodeExecutor { + ImmutableMap execute(String script, ImmutableMap inputVariablesMap); +} 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 new file mode 100644 index 0000000..78e4014 --- /dev/null +++ b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/engine/node/code/JavaScriptCodeExecutor.java @@ -0,0 +1,45 @@ +package com.lanyuanxiaoyao.service.ai.web.engine.node.code; + +import cn.hutool.core.util.StrUtil; +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{ + @Override + public ImmutableMap execute(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<>() {})); + } + } + } +} diff --git a/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/service/task/FlowTaskService.java b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/service/task/FlowTaskService.java index 4c812ef..ad4927f 100644 --- a/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/service/task/FlowTaskService.java +++ b/service-ai/service-ai-web/src/main/java/com/lanyuanxiaoyao/service/ai/web/service/task/FlowTaskService.java @@ -10,12 +10,16 @@ import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowContext; import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowEdge; import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowGraph; import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowNode; +import com.lanyuanxiaoyao.service.ai.web.engine.node.CodeNode; import com.lanyuanxiaoyao.service.ai.web.engine.node.LlmNode; +import com.lanyuanxiaoyao.service.ai.web.engine.node.LoopNode; import com.lanyuanxiaoyao.service.ai.web.engine.node.OutputNode; +import com.lanyuanxiaoyao.service.ai.web.engine.node.SwitchNode; import com.lanyuanxiaoyao.service.ai.web.engine.store.InMemoryFlowStore; import com.lanyuanxiaoyao.service.ai.web.entity.FlowTask; import com.lanyuanxiaoyao.service.ai.web.repository.FlowTaskRepository; import java.lang.reflect.InvocationTargetException; +import java.util.Map; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.eclipse.collections.api.factory.Maps; @@ -38,14 +42,18 @@ public class FlowTaskService extends SimpleServiceSupport { var flowTask = detailOrThrow(id); var graphVo = mapper.readValue(flowTask.getTemplateFlowGraph(), FlowGraphVo.class); var flowGraph = new FlowGraph(IdUtil.fastUUID(), graphVo.getNodes(), graphVo.getEdges()); + log.info("Graph: {}", flowGraph); var store = new InMemoryFlowStore(); var executor = new FlowExecutor( store, - Maps.immutable.of( - "output-node", OutputNode.class, - "llm-node", LlmNode.class - ) + Maps.immutable.ofAll(Map.of( + "loop-node", LoopNode.class, + "switch-node", SwitchNode.class, + "code-node", CodeNode.class, + "llm-node", LlmNode.class, + "output-node", OutputNode.class + )) ); FlowContext context = new FlowContext(); context.setInput(mapper.readValue(flowTask.getInput(), new TypeReference<>() {})); diff --git a/service-ai/service-ai-web/src/test/java/com/lanyuanxiaoyao/service/ai/web/TestFlow.java b/service-ai/service-ai-web/src/test/java/com/lanyuanxiaoyao/service/ai/web/TestFlow.java index 8180b31..370a4bf 100644 --- a/service-ai/service-ai-web/src/test/java/com/lanyuanxiaoyao/service/ai/web/TestFlow.java +++ b/service-ai/service-ai-web/src/test/java/com/lanyuanxiaoyao/service/ai/web/TestFlow.java @@ -35,15 +35,15 @@ public class TestFlow { var graph = new FlowGraph( "graph-1", Sets.immutable.of( - new FlowNode("node-1", "plain-node"), - new FlowNode("node-2", "plain-node"), - new FlowNode("node-4", "plain-node"), - new FlowNode("node-6", "plain-node"), - new FlowNode("node-7", "plain-node"), - new FlowNode("node-5", "plain-node"), - new FlowNode("node-8", "option-node"), - new FlowNode("node-9", "plain-node"), - new FlowNode("node-3", "plain-node") + new FlowNode("node-1", "plain-node", null), + new FlowNode("node-2", "plain-node", null), + new FlowNode("node-4", "plain-node", null), + new FlowNode("node-6", "plain-node", null), + new FlowNode("node-7", "plain-node", null), + new FlowNode("node-5", "plain-node", null), + new FlowNode("node-8", "option-node", null), + new FlowNode("node-9", "plain-node", null), + new FlowNode("node-3", "plain-node", null) ), Sets.immutable.of( new FlowEdge("edge-1", "node-1", "node-2", null, null), 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 new file mode 100644 index 0000000..e35d7ed --- /dev/null +++ b/service-ai/service-ai-web/src/test/java/com/lanyuanxiaoyao/service/ai/web/TestJsExecutor.java @@ -0,0 +1,57 @@ +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<>() {})); + } + } + } +} diff --git a/service-web/client/src/components/flow/Helper.tsx b/service-web/client/src/components/flow/Helper.tsx index 597a4ce..066cc58 100644 --- a/service-web/client/src/components/flow/Helper.tsx +++ b/service-web/client/src/components/flow/Helper.tsx @@ -1,11 +1,11 @@ import {type Edge, getIncomers, type Node} from '@xyflow/react' import type {Option} from 'amis/lib/Schema' -import {find, has, isEqual, max, min, unique} from 'licia' +import {contain, find, has, isEqual, max, min, unique} from 'licia' import {type DependencyList, type MouseEvent as ReactMouseEvent, useCallback, useRef} from 'react' import Queue from 'yocto-queue' import {originTypeMap} from '../../pages/ai/task/InputSchema.tsx' import {useFlowStore} from './store/FlowStore.ts' -import {type OutputVariable} from './types.ts' +import {type OutputVariable, type OutputVariableType} from './types.ts' export const getAllIncomerNodeById: (id: string, nodes: Node[], edges: Edge[]) => string[] = (id, nodes, edges) => { let queue = new Queue() @@ -82,9 +82,12 @@ export const getAllIncomerNodeOutputVariables: (id: string, inputSchema: Record< ] } -export const generateAllIncomerOutputVariablesFormOptions: (id: string, inputSchema: Record>, nodes: Node[], edges: Edge[], data: any) => Option[] = (id, inputSchema, nodes, edges, data) => { +export const generateAllIncomerOutputVariablesFormOptions: (id: string, inputSchema: Record>, nodes: Node[], edges: Edge[], data: any, targetTypes?: OutputVariableType[]) => Option[] = (id, inputSchema, nodes, edges, data, targetTypes) => { let optionMap: Record = {} for (const item of getAllIncomerNodeOutputVariables(id, inputSchema, nodes, edges, data)) { + if (targetTypes && !contain(targetTypes, item.type)) { + continue + } if (!optionMap[item.group]) { optionMap[item.group] = [] } @@ -142,7 +145,7 @@ export const generateAllIncomerOutputVariablesConditions: (id: string, inputSche options: [ {label: '真', value: true}, {label: '假', value: false}, - ] + ], }, defaultOp: booleanDefaultOperator, operators: booleanOperators,