From bd58eadc6097ca3fb337ea7c9acd1181a09768c8 Mon Sep 17 00:00:00 2001 From: VLSMB <2047857654@qq.com> Date: Wed, 17 Sep 2025 17:59:32 +0800 Subject: [PATCH 1/3] feat: support MCP Node for Studio DSL --- .../main/src/pages/App/Workflow/index.tsx | 2 +- .../generator/model/workflow/NodeType.java | 3 +- .../model/workflow/nodedata/MCPNodeData.java | 81 ++++------ .../dsl/converter/EndNodeDataConverter.java | 2 +- .../dsl/converter/MCPNodeDataConverter.java | 145 ++++++++---------- .../workflow/sections/EndNodeSection.java | 6 +- .../workflow/sections/MCPNodeSection.java | 134 ++++++++++------ .../src/main/resources/initializr.yml | 6 + .../templates/default-application.yml | 25 --- .../studio/core/base/manager/MCPManager.java | 7 +- 10 files changed, 198 insertions(+), 213 deletions(-) diff --git a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/frontend/packages/main/src/pages/App/Workflow/index.tsx b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/frontend/packages/main/src/pages/App/Workflow/index.tsx index 00e5e6b8c0..4b1776e462 100644 --- a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/frontend/packages/main/src/pages/App/Workflow/index.tsx +++ b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/frontend/packages/main/src/pages/App/Workflow/index.tsx @@ -292,7 +292,7 @@ export const FlowEditor = memo((props: IProps) => { try { // 准备请求参数 const params = { - dependencies: 'spring-ai-alibaba-graph,web,spring-ai-alibaba-starter-dashscope', + dependencies: 'spring-ai-alibaba-graph,web,spring-ai-alibaba-starter-dashscope,spring-ai-starter-mcp-client', appMode: 'workflow', dslDialectType: 'studio', type: 'maven-project', diff --git a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/model/workflow/NodeType.java b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/model/workflow/NodeType.java index 34820c2e90..c95f7c8019 100644 --- a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/model/workflow/NodeType.java +++ b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/model/workflow/NodeType.java @@ -55,7 +55,8 @@ public enum NodeType { TOOL("tool", "tool", "UNSUPPORTED"), - MCP("mcp", "UNSUPPORTED", "UNSUPPORTED"), + // Dify的MCP使用ToolNode定义 + MCP("mcp", "UNSUPPORTED", "MCP"), TEMPLATE_TRANSFORM("template-transform", "template-transform", "UNSUPPORTED"), diff --git a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/model/workflow/nodedata/MCPNodeData.java b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/model/workflow/nodedata/MCPNodeData.java index a091fcb9b1..3881865628 100644 --- a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/model/workflow/nodedata/MCPNodeData.java +++ b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/model/workflow/nodedata/MCPNodeData.java @@ -16,92 +16,71 @@ package com.alibaba.cloud.ai.studio.admin.generator.model.workflow.nodedata; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import com.alibaba.cloud.ai.studio.admin.generator.model.VariableSelector; import com.alibaba.cloud.ai.studio.admin.generator.model.workflow.NodeData; -/** - * NodeData for McpNode: Contains fields such as url, tool, headers, params, outputKey, - * inputParamKeys, etc. - */ +import java.util.List; + +// 本类仅考虑Studio的MCP使用,Dify的MCP使用ToolNode定义 public class MCPNodeData extends NodeData { - private String url; + private String toolName; - private String tool; + private String serverName; - private Map headers; + private String serverCode; - private Map params; + private String inputJsonTemplate = ""; - private String outputKey; + private List inputKeys = List.of(); - private List inputParamKeys; + private String outputKey = "output"; - public MCPNodeData() { - super(Collections.emptyList(), Collections.emptyList()); + public String getToolName() { + return toolName; } - public MCPNodeData(List inputs, - List outputs) { - super(inputs, outputs); + public void setToolName(String toolName) { + this.toolName = toolName; } - public String getUrl() { - return url; + public String getServerName() { + return serverName; } - public MCPNodeData setUrl(String url) { - this.url = url; - return this; + public void setServerName(String serverName) { + this.serverName = serverName; } - public String getTool() { - return tool; + public String getServerCode() { + return serverCode; } - public MCPNodeData setTool(String tool) { - this.tool = tool; - return this; + public void setServerCode(String serverCode) { + this.serverCode = serverCode; } - public Map getHeaders() { - return headers; + public String getInputJsonTemplate() { + return inputJsonTemplate; } - public MCPNodeData setHeaders(Map headers) { - this.headers = headers; - return this; + public void setInputJsonTemplate(String inputJsonTemplate) { + this.inputJsonTemplate = inputJsonTemplate; } - public Map getParams() { - return params; + public List getInputKeys() { + return inputKeys; } - public MCPNodeData setParams(Map params) { - this.params = params; - return this; + public void setInputKeys(List inputKeys) { + this.inputKeys = inputKeys; } public String getOutputKey() { return outputKey; } - public MCPNodeData setOutputKey(String outputKey) { + public void setOutputKey(String outputKey) { this.outputKey = outputKey; - return this; - } - - public List getInputParamKeys() { - return inputParamKeys; - } - - public MCPNodeData setInputParamKeys(List inputParamKeys) { - this.inputParamKeys = inputParamKeys; - return this; } } diff --git a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/converter/EndNodeDataConverter.java b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/converter/EndNodeDataConverter.java index 6fba5dd479..98211ab9b0 100644 --- a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/converter/EndNodeDataConverter.java +++ b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/converter/EndNodeDataConverter.java @@ -79,7 +79,7 @@ public Map dump(EndNodeData nodeData) { data.put("outputs", outputsMap); return data; } - }), STUDIO(new DialectConverter() { + }), STUDIO(new DialectConverter<>() { @Override public Boolean supportDialect(DSLDialectType dialectType) { return DSLDialectType.STUDIO.equals(dialectType); diff --git a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/converter/MCPNodeDataConverter.java b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/converter/MCPNodeDataConverter.java index af630ba7f2..560ce344d1 100644 --- a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/converter/MCPNodeDataConverter.java +++ b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/converter/MCPNodeDataConverter.java @@ -16,23 +16,22 @@ package com.alibaba.cloud.ai.studio.admin.generator.service.dsl.converter; -import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.function.BiConsumer; import java.util.stream.Collectors; import java.util.stream.Stream; +import com.alibaba.cloud.ai.studio.admin.generator.model.Variable; +import com.alibaba.cloud.ai.studio.admin.generator.model.VariableType; import com.alibaba.cloud.ai.studio.admin.generator.model.workflow.NodeType; import com.alibaba.cloud.ai.studio.admin.generator.model.workflow.nodedata.MCPNodeData; import com.alibaba.cloud.ai.studio.admin.generator.service.dsl.AbstractNodeDataConverter; -import com.alibaba.cloud.ai.studio.admin.generator.service.dsl.DSLDialectType; +import com.alibaba.cloud.ai.studio.admin.generator.service.dsl.DSLDialectType; +import com.alibaba.cloud.ai.studio.admin.generator.utils.MapReadUtil; import org.springframework.stereotype.Component; -/** - * Convert the MCP node configuration in the Dify DSL to and from the MCPNodeData object. - */ @Component public class MCPNodeDataConverter extends AbstractNodeDataConverter { @@ -50,87 +49,60 @@ protected List> getDialectConverters() { private enum MCPNodeConverter { - DIFY(new DialectConverter<>() { - @SuppressWarnings("unchecked") + STUDIO(new DialectConverter<>() { @Override - public MCPNodeData parse(Map data) { - MCPNodeData nd = new MCPNodeData(); - - // url - nd.setUrl((String) data.get("url")); - - // tool - nd.setTool((String) data.get("tool")); - - // headers (Map) - Map hmap = (Map) data.get("headers"); - if (hmap != null) { - nd.setHeaders(new LinkedHashMap<>(hmap)); - } - - // params (Map) - Map pmap = (Map) data.get("params"); - if (pmap != null) { - nd.setParams(new LinkedHashMap<>(pmap)); - } - - // output_key - nd.setOutputKey((String) data.get("output_key")); - - // input_param_keys (List) - List ipk = (List) data.get("input_param_keys"); - if (ipk != null) { - nd.setInputParamKeys(ipk); - } - else { - nd.setInputParamKeys(Collections.emptyList()); - } - - return nd; + public Boolean supportDialect(DSLDialectType dialectType) { + return DSLDialectType.STUDIO.equals(dialectType); } @Override - public Map dump(MCPNodeData nd) { - Map m = new LinkedHashMap<>(); - - // url - if (nd.getUrl() != null) { - m.put("url", nd.getUrl()); - } - - // tool - if (nd.getTool() != null) { - m.put("tool", nd.getTool()); - } - - // headers - if (nd.getHeaders() != null && !nd.getHeaders().isEmpty()) { - m.put("headers", nd.getHeaders()); - } - - // params - if (nd.getParams() != null && !nd.getParams().isEmpty()) { - m.put("params", nd.getParams()); - } - - // output_key - if (nd.getOutputKey() != null) { - m.put("output_key", nd.getOutputKey()); - } - - // input_param_keys - if (nd.getInputParamKeys() != null && !nd.getInputParamKeys().isEmpty()) { - m.put("input_param_keys", nd.getInputParamKeys()); - } - - return m; + public MCPNodeData parse(Map data) { + MCPNodeData nodeData = new MCPNodeData(); + + // 获取基本信息 + String toolName = MapReadUtil.getMapDeepValue(data, String.class, "config", "node_param", "tool_name"); + String serverCode = MapReadUtil.getMapDeepValue(data, String.class, "config", "node_param", + "server_code"); + String serverName = MapReadUtil.getMapDeepValue(data, String.class, "config", "node_param", + "server_name"); + String inputJsonTemplate = MapReadUtil + .safeCastToListWithMap(MapReadUtil.getMapDeepValue(data, List.class, "config", "input_params")) + .stream() + .map(map -> { + String key = map.get("key").toString(); + String value = map.get("value").toString(); + VariableType type = VariableType.fromStudioValue(map.get("type").toString()) + .orElse(VariableType.OBJECT); + + if (VariableType.STRING.equals(type) && value != null) { + value = "\"" + value + "\""; + } + return String.format("\"%s\": %s", key, value); + }) + .collect(Collectors.joining(",\n")); + inputJsonTemplate = "{" + inputJsonTemplate + "}"; + String outputKey = MapReadUtil + .safeCastToListWithMap(MapReadUtil.getMapDeepValue(data, List.class, "config", "output_params")) + .get(0) + .get("key") + .toString(); + + // 设置节点数据 + nodeData.setToolName(toolName); + nodeData.setServerCode(serverCode); + nodeData.setServerName(serverName); + nodeData.setInputJsonTemplate(inputJsonTemplate); + nodeData.setOutputKey(outputKey); + return nodeData; } @Override - public Boolean supportDialect(DSLDialectType dialect) { - return DSLDialectType.DIFY.equals(dialect); + public Map dump(MCPNodeData nodeData) { + throw new UnsupportedOperationException(); } - }), CUSTOM(defaultCustomDialectConverter(MCPNodeData.class)); + }) + + , CUSTOM(defaultCustomDialectConverter(MCPNodeData.class)); private final DialectConverter converter; @@ -149,4 +121,19 @@ public String generateVarName(int count) { return "mcpNode" + count; } + @Override + public BiConsumer> postProcessConsumer(DSLDialectType dialectType) { + return switch (dialectType) { + case STUDIO -> emptyProcessConsumer().andThen((nodeData, idToVarName) -> { + nodeData.setOutputs(List.of(new Variable(nodeData.getOutputKey(), VariableType.STRING))); + nodeData.setInputJsonTemplate( + this.convertVarTemplate(dialectType, nodeData.getInputJsonTemplate(), idToVarName)); + nodeData.setInputKeys(this.getVarTemplateKeys(nodeData.getInputJsonTemplate())); + }) + .andThen(super.postProcessConsumer(dialectType)) + .andThen((nodeData, idToVarName) -> nodeData.setOutputKey(nodeData.getOutputs().get(0).getName())); + default -> super.postProcessConsumer(dialectType); + }; + } + } diff --git a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/generator/workflow/sections/EndNodeSection.java b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/generator/workflow/sections/EndNodeSection.java index 461833a1ee..9df54c6135 100644 --- a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/generator/workflow/sections/EndNodeSection.java +++ b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/generator/workflow/sections/EndNodeSection.java @@ -50,7 +50,7 @@ public String render(Node node, String varName) { if ("text".equalsIgnoreCase(data.getOutputType())) { // 如果输出类型为text,则使用对应的输出模板输出最终结果 if (data.getTextTemplateVars().isEmpty()) { - codeStr = String.format("state -> Map.of(\"output\", %s)", + codeStr = String.format("state -> Map.of(\"%s\", %s)", data.getOutputKey(), ObjectToCodeUtil.toCode(data.getTextTemplate())); } else { @@ -63,11 +63,11 @@ public String render(Node node, String varName) { key -> state.value(key).orElse(""), (o1, o2) -> o2)); template = new PromptTemplate(template).render(params); - return Map.of("output", template); + return Map.of("%s", template); } """, ObjectToCodeUtil.toCode(data.getTextTemplate()), - ObjectToCodeUtil.toCode(data.getTextTemplateVars())); + ObjectToCodeUtil.toCode(data.getTextTemplateVars()), data.getOutputKey()); } } else { diff --git a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/generator/workflow/sections/MCPNodeSection.java b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/generator/workflow/sections/MCPNodeSection.java index a8c58e6b1e..1753258aac 100644 --- a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/generator/workflow/sections/MCPNodeSection.java +++ b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/generator/workflow/sections/MCPNodeSection.java @@ -17,19 +17,32 @@ package com.alibaba.cloud.ai.studio.admin.generator.service.generator.workflow.sections; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; import com.alibaba.cloud.ai.studio.admin.generator.model.workflow.Node; import com.alibaba.cloud.ai.studio.admin.generator.model.workflow.NodeType; import com.alibaba.cloud.ai.studio.admin.generator.model.workflow.nodedata.MCPNodeData; +import com.alibaba.cloud.ai.studio.admin.generator.service.dsl.DSLDialectType; import com.alibaba.cloud.ai.studio.admin.generator.service.generator.workflow.NodeSection; +import com.alibaba.cloud.ai.studio.admin.generator.utils.ObjectToCodeUtil; +import com.alibaba.cloud.ai.studio.core.base.entity.McpServerEntity; +import com.alibaba.cloud.ai.studio.core.base.service.McpServerService; +import com.alibaba.cloud.ai.studio.core.context.RequestContextHolder; +import com.alibaba.cloud.ai.studio.runtime.domain.RequestContext; +import com.alibaba.cloud.ai.studio.runtime.domain.mcp.McpServerDeployConfig; +import com.alibaba.cloud.ai.studio.runtime.utils.JsonUtils; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class MCPNodeSection implements NodeSection { + private final McpServerService mcpServerService; + + public MCPNodeSection(@Autowired(required = false) McpServerService mcpServerService) { + this.mcpServerService = mcpServerService; + } + @Override public boolean support(NodeType nodeType) { return NodeType.MCP.equals(nodeType); @@ -37,62 +50,85 @@ public boolean support(NodeType nodeType) { @Override public String render(Node node, String varName) { - MCPNodeData d = (MCPNodeData) node.getData(); - String id = node.getId(); - StringBuilder sb = new StringBuilder(); - - sb.append(String.format("// —— McpNode [%s] ——%n", id)); - sb.append(String.format("McpNode %s = McpNode.builder()%n", varName)); - - if (d.getUrl() != null) { - sb.append(String.format(".url(\"%s\")%n", escape(d.getUrl()))); - } - - if (d.getTool() != null) { - sb.append(String.format(".tool(\"%s\")%n", escape(d.getTool()))); + if (this.mcpServerService == null) { + throw new IllegalArgumentException( + "The current mode does not support Studio's MCP node code generation. Please start the complete StudioApplication class"); } + MCPNodeData nodeData = (MCPNodeData) node.getData(); - Map headers = d.getHeaders(); - if (headers != null) { - for (Map.Entry entry : headers.entrySet()) { - sb.append(String.format(".header(\"%s\", \"%s\")%n", escape(entry.getKey()), escape(entry.getValue()))); - } + RequestContext context = RequestContextHolder.getRequestContext(); + McpServerEntity mcpServerEntity = mcpServerService.getMcpByCode(context.getWorkspaceId(), + nodeData.getServerCode(), null); + if (mcpServerEntity == null) { + throw new IllegalArgumentException("MCP Server [" + nodeData.getServerCode() + "] not found!"); } - Map params = d.getParams(); - if (params != null) { - for (Map.Entry entry : params.entrySet()) { - Object val = entry.getValue(); - String valLiteral; - if (val instanceof String) { - valLiteral = String.format("\"%s\"", escape((String) val)); - } - else { - valLiteral = String.valueOf(val); - } - sb.append(String.format(".param(\"%s\", %s)%n", escape(entry.getKey()), valLiteral)); - } - } - - if (d.getOutputKey() != null) { - sb.append(String.format(".outputKey(\"%s\")%n", escape(d.getOutputKey()))); - } - - List ipk = d.getInputParamKeys(); - if (ipk != null && !ipk.isEmpty()) { - String joined = ipk.stream().map(this::escape).map(s -> "\"" + s + "\"").collect(Collectors.joining(", ")); - sb.append(String.format(".inputParamKeys(List.of(%s))%n", joined)); - } - - sb.append(".build();\n"); - sb.append(String.format("stateGraph.addNode(\"%s\", AsyncNodeAction.node_async(%s));%n%n", varName, varName)); + McpServerDeployConfig deployConfig = JsonUtils.fromJson(mcpServerEntity.getDeployConfig(), + McpServerDeployConfig.class); + String endPoint = deployConfig.getRemoteEndpoint(); + String baseUri = deployConfig.getRemoteAddress(); + + return String.format(""" + // -- MCP Node [%s] -- + stateGraph.addNode("%s", AsyncNodeAction.node_async( + createMcpNodeAction(%s, %s, %s, %s, %s, %s) + )); + + """, nodeData.getServerName(), varName, ObjectToCodeUtil.toCode(baseUri), + ObjectToCodeUtil.toCode(endPoint), ObjectToCodeUtil.toCode(nodeData.getToolName()), + ObjectToCodeUtil.toCode(nodeData.getInputJsonTemplate()), + ObjectToCodeUtil.toCode(nodeData.getInputKeys()), ObjectToCodeUtil.toCode(nodeData.getOutputKey())); + } - return sb.toString(); + @Override + public String assistMethodCode(DSLDialectType dialectType) { + return switch (dialectType) { + case STUDIO -> + """ + private NodeAction createMcpNodeAction(String baseUri, String endPoint, String toolName, + String inputTemplate, List keys, String outputKey) { + return state -> { + // create client + McpSyncClient client = McpClient.sync( + HttpClientSseClientTransport + .builder(baseUri) + .sseEndpoint(endPoint) + .build() + ).build(); + client.initialize(); + SyncMcpToolCallbackProvider provider = new SyncMcpToolCallbackProvider(client); + + // get tools + ToolCallback[] toolCallbacks = provider.getToolCallbacks(); + ToolCallback toolCallback = Arrays.stream(toolCallbacks) + .filter(t -> t.getToolDefinition().name().contains(toolName)) + .findFirst() + .orElseThrow(() -> new IllegalStateException("toolName [" + toolName + "] not found!")); + + // prepare input + String input = inputTemplate; + for(String key : keys) { + input = input.replace("{" + key + "}", state.value(key).orElse("").toString()); + } + + // call + String call = toolCallback.call(input); + System.out.println(call); + client.close(); + return Map.of(outputKey, call); + }; + } + """; + default -> NodeSection.super.assistMethodCode(dialectType); + }; } @Override public List getImports() { - return List.of(); + return List.of("io.modelcontextprotocol.client.McpSyncClient", "io.modelcontextprotocol.client.McpClient", + "io.modelcontextprotocol.client.transport.HttpClientSseClientTransport", + "org.springframework.ai.mcp.SyncMcpToolCallbackProvider", "org.springframework.ai.tool.ToolCallback", + "java.util.Arrays"); } } diff --git a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/resources/initializr.yml b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/resources/initializr.yml index 69979c6296..49bf0ebdd9 100644 --- a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/resources/initializr.yml +++ b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/resources/initializr.yml @@ -216,6 +216,12 @@ initializr: artifact-id: spring-ai-alibaba-starter-dashscope version: 1.0.0.4-SNAPSHOT description: DashScope Model adapted Starter + - name: Spring AI Starter MCP Client + id: spring-ai-starter-mcp-client + group-id: org.springframework.ai + artifact-id: spring-ai-starter-mcp-client + version: 1.0.0 + description: Spring AI Starter MCP Client - name: Developer Tools content: - name: GraalVM Native Support diff --git a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/resources/templates/default-application.yml b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/resources/templates/default-application.yml index e46a399962..c390a593f6 100644 --- a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/resources/templates/default-application.yml +++ b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/resources/templates/default-application.yml @@ -19,31 +19,6 @@ spring: application: name: spring-ai-alibaba-graph-example ai: - mcp: - server: - name: my-weather-server - version: 0.0.1 - type: ASYNC # Recommended for reactive applications - # Configure the root path for sse, default is /sse - # The final path below will be ip:port/sse/mcp - sse-endpoint: /sse - sse-message-endpoint: /mcp - capabilities: - tool: true - resource: true - prompt: true - completion: true - alibaba: - toolcalling: - weather: - enabled: true - api-key: ${WEATHER_API_KEY} - baidu: - search: - enabled: true - tavilysearch: - api-key: ${TAVILY_API_KEY} - enabled: true dashscope: api-key: ${DASHSCOPE_API_KEY} openai: diff --git a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-core/src/main/java/com/alibaba/cloud/ai/studio/core/base/manager/MCPManager.java b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-core/src/main/java/com/alibaba/cloud/ai/studio/core/base/manager/MCPManager.java index 6c423acd53..d47d1f694a 100644 --- a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-core/src/main/java/com/alibaba/cloud/ai/studio/core/base/manager/MCPManager.java +++ b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-core/src/main/java/com/alibaba/cloud/ai/studio/core/base/manager/MCPManager.java @@ -31,7 +31,6 @@ import com.alibaba.cloud.ai.studio.core.base.entity.McpServerEntity; import com.alibaba.cloud.ai.studio.core.utils.LogUtils; import com.alibaba.cloud.ai.studio.core.utils.concurrent.ThreadPoolUtils; -import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; @@ -90,8 +89,10 @@ public McpSyncClient getMcpSyncClient(McpServerEntity entity) { HttpClient.Builder builder1 = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) .connectTimeout(Duration.ofSeconds(60)); - McpClientTransport transport = new HttpClientSseClientTransport(builder1, host, remoteEndpoint, - new ObjectMapper()); + McpClientTransport transport = HttpClientSseClientTransport.builder(host) + .clientBuilder(builder1) + .sseEndpoint(remoteEndpoint) + .build(); McpClient.SyncSpec builder = McpClient.sync(transport) .requestTimeout(Duration.ofSeconds(60)) .initializationTimeout(Duration.ofSeconds(60)) From 040252aab5222e70f5170472f8bb1a6d7bb16717 Mon Sep 17 00:00:00 2001 From: VLSMB <2047857654@qq.com> Date: Wed, 17 Sep 2025 21:22:53 +0800 Subject: [PATCH 2/3] feat: support API(HTTP) Node for Studio DSL --- .../generator/model/workflow/NodeType.java | 2 +- .../model/workflow/nodedata/HttpNodeData.java | 52 ++++--- .../dsl/adapters/StudioDSLAdapter.java | 2 +- .../dsl/converter/HttpNodeDataConverter.java | 141 ++++++++++-------- .../workflow/sections/HttpNodeSection.java | 18 ++- 5 files changed, 129 insertions(+), 86 deletions(-) diff --git a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/model/workflow/NodeType.java b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/model/workflow/NodeType.java index c95f7c8019..b8a9b184c8 100644 --- a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/model/workflow/NodeType.java +++ b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/model/workflow/NodeType.java @@ -47,7 +47,7 @@ public enum NodeType { QUESTION_CLASSIFIER("question-classifier", "question-classifier", "Classifier"), - HTTP("http", "http-request", "UNSUPPORTED"), + HTTP("http", "http-request", "API"), LIST_OPERATOR("list-operator", "list-operator", "UNSUPPORTED"), diff --git a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/model/workflow/nodedata/HttpNodeData.java b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/model/workflow/nodedata/HttpNodeData.java index 370343ba76..43d99cda8c 100644 --- a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/model/workflow/nodedata/HttpNodeData.java +++ b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/model/workflow/nodedata/HttpNodeData.java @@ -29,6 +29,7 @@ import com.alibaba.cloud.ai.studio.admin.generator.model.VariableType; import com.alibaba.cloud.ai.studio.admin.generator.model.workflow.NodeData; +import com.alibaba.cloud.ai.studio.admin.generator.service.dsl.DSLDialectType; import org.springframework.http.HttpMethod; /** @@ -37,25 +38,30 @@ */ public class HttpNodeData extends NodeData { - public static List getDefaultOutputSchemas() { - return List.of(new Variable("body", VariableType.STRING), new Variable("status_code", VariableType.NUMBER), - new Variable("headers", VariableType.OBJECT), new Variable("files", VariableType.ARRAY_FILE)); + public static List getDefaultOutputSchemas(DSLDialectType dialectType) { + return switch (dialectType) { + case DIFY -> + List.of(new Variable("body", VariableType.STRING), new Variable("status_code", VariableType.NUMBER), + new Variable("headers", VariableType.OBJECT), new Variable("files", VariableType.ARRAY_FILE)); + case STUDIO -> List.of(new Variable("output", VariableType.STRING)); + default -> List.of(); + }; } /** HTTP method, default GET */ - private HttpMethod method = HttpMethod.GET; + private HttpMethod method; /** Request URL */ private String url; /** Request header */ - private Map headers = Collections.emptyMap(); + private Map headers; /** queryParams */ - private Map queryParams = Collections.emptyMap(); + private Map queryParams; /** body */ - private HttpRequestNodeBody body = new HttpRequestNodeBody(); + private HttpRequestNodeBody body; /** * rawBodyMap @@ -66,7 +72,7 @@ public static List getDefaultOutputSchemas() { private AuthConfig authConfig; /** retryConfig */ - private RetryConfig retryConfig = new RetryConfig(3, 1000, true); + private RetryConfig retryConfig; /** TimeoutConfig */ private TimeoutConfig timeoutConfig; @@ -90,12 +96,6 @@ public HttpNodeData(List inputs, List outputs, HttpM this.rawBodyMap = null; } - public HttpNodeData(List inputs, List outputs) { - this(inputs, outputs, HttpMethod.GET, null, Collections.emptyMap(), Collections.emptyMap(), - new HttpRequestNodeBody(), null, new RetryConfig(3, 1000, true), - new TimeoutConfig(10, 60, 20, 300, 600, 6000), null); - } - public HttpMethod getMethod() { return method; } @@ -136,6 +136,14 @@ public void setBody(HttpRequestNodeBody body) { this.body = body; } + public Map getRawBodyMap() { + return rawBodyMap; + } + + public void setRawBodyMap(Map rawBodyMap) { + this.rawBodyMap = rawBodyMap; + } + public AuthConfig getAuthConfig() { return authConfig; } @@ -152,20 +160,20 @@ public void setRetryConfig(RetryConfig retryConfig) { this.retryConfig = retryConfig; } - public String getOutputKey() { - return outputKey; + public TimeoutConfig getTimeoutConfig() { + return timeoutConfig; } - public void setOutputKey(String outputKey) { - this.outputKey = outputKey; + public void setTimeoutConfig(TimeoutConfig timeoutConfig) { + this.timeoutConfig = timeoutConfig; } - public Map getRawBodyMap() { - return rawBodyMap; + public String getOutputKey() { + return outputKey; } - public void setRawBodyMap(Map rawBodyMap) { - this.rawBodyMap = rawBodyMap; + public void setOutputKey(String outputKey) { + this.outputKey = outputKey; } } diff --git a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/adapters/StudioDSLAdapter.java b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/adapters/StudioDSLAdapter.java index 4040058649..dd0f931534 100644 --- a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/adapters/StudioDSLAdapter.java +++ b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/adapters/StudioDSLAdapter.java @@ -137,7 +137,7 @@ private Graph constructGraph(Map data) { // 展开迭代节点内部的Node和Edge nodeMap.forEach(map -> { NodeType type = NodeType.fromStudioValue(MapReadUtil.getMapDeepValue(map, String.class, "type")) - .orElseThrow(); + .orElseThrow(() -> new UnsupportedOperationException("unsupported node type " + map.get("type"))); if (NodeType.ITERATION.equals(type)) { List> innerNode = MapReadUtil.safeCastToListWithMap( MapReadUtil.getMapDeepValue(map, List.class, "config", "node_param", "block", "nodes")); diff --git a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/converter/HttpNodeDataConverter.java b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/converter/HttpNodeDataConverter.java index 4d837b6d35..133a33ea9b 100644 --- a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/converter/HttpNodeDataConverter.java +++ b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/converter/HttpNodeDataConverter.java @@ -18,7 +18,6 @@ import java.util.Arrays; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -35,6 +34,7 @@ import com.alibaba.cloud.ai.studio.admin.generator.model.workflow.nodedata.HttpNodeData; import com.alibaba.cloud.ai.studio.admin.generator.service.dsl.AbstractNodeDataConverter; import com.alibaba.cloud.ai.studio.admin.generator.service.dsl.DSLDialectType; +import com.alibaba.cloud.ai.studio.admin.generator.utils.MapReadUtil; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; @@ -177,64 +177,79 @@ else if (paramsObj instanceof String str) { @Override public Map dump(HttpNodeData nd) { - Map m = new LinkedHashMap<>(); + throw new UnsupportedOperationException(); + } + }), STUDIO(new DialectConverter<>() { + @Override + public Boolean supportDialect(DSLDialectType dialectType) { + return DSLDialectType.STUDIO.equals(dialectType); + } - // variable_selector - if (!nd.getInputs().isEmpty()) { - VariableSelector sel = nd.getInputs().get(0); - m.put("variable_selector", List.of(sel.getNamespace(), sel.getName())); - } - // method - if (nd.getMethod() != HttpMethod.GET) { - m.put("method", nd.getMethod().name().toLowerCase()); - } - // url - if (nd.getUrl() != null) { - m.put("url", nd.getUrl()); - } - // headers - if (!nd.getHeaders().isEmpty()) { - m.put("headers", nd.getHeaders()); - } - // query_params - if (!nd.getQueryParams().isEmpty()) { - m.put("query_params", nd.getQueryParams()); - } - // body - HttpRequestNodeBody body = nd.getBody(); - if (body != null && body.getType() != null) { - m.put("body", body); - } - // auth - AuthConfig ac = nd.getAuthConfig(); - if (ac != null) { - Map am = new LinkedHashMap<>(); - am.put("type", ac.getTypeName()); - if (ac.isBasic()) { - am.put("username", ac.getUsername()); - am.put("password", ac.getPassword()); - } - else if (ac.isBearer()) { - am.put("token", ac.getToken()); - } - m.put("auth", am); - } - // retry_config - RetryConfig rc = nd.getRetryConfig(); - if (rc != null) { - Map rm = new LinkedHashMap<>(); - rm.put("max_retries", rc.getMaxRetries()); - rm.put("max_retry_interval", rc.getMaxRetryInterval()); - rm.put("enable", rc.isEnable()); - m.put("retry_config", rm); - } - // output_key - if (nd.getOutputKey() != null) { - m.put("output_key", nd.getOutputKey()); - } - return m; + @Override + public HttpNodeData parse(Map data) throws JsonProcessingException { + // 获取必要信息 + HttpMethod httpMethod = HttpMethod.valueOf(Optional + .ofNullable(MapReadUtil.getMapDeepValue(data, String.class, "config", "node_param", "method")) + .orElse("GET") + .toString() + .toUpperCase()); + String url = MapReadUtil.getMapDeepValue(data, String.class, "config", "node_param", "url"); + + Map headers = Optional + .ofNullable(MapReadUtil.safeCastToListWithMap( + MapReadUtil.getMapDeepValue(data, Object.class, "config", "node_param", "headers"))) + .orElse(List.of()) + .stream() + .filter(map -> map.containsKey("key") && map.containsKey("value")) + .collect(Collectors.toUnmodifiableMap(map -> map.get("key").toString(), + map -> map.get("value").toString())); + Map queryParams = Optional + .ofNullable(MapReadUtil.safeCastToListWithMap( + MapReadUtil.getMapDeepValue(data, Object.class, "config", "node_param", "params"))) + .orElse(List.of()) + .stream() + .filter(map -> map.containsKey("key") && map.containsKey("value")) + .collect(Collectors.toUnmodifiableMap(map -> map.get("key").toString(), + map -> map.get("value").toString())); + + Object rawBody = MapReadUtil.getMapDeepValue(data, Object.class, "config", "node_param", "body"); + HttpRequestNodeBody body = HttpRequestNodeBody.from(rawBody); + AuthConfig auth = Optional + .ofNullable(MapReadUtil.getMapDeepValue(data, String.class, "config", "node_param", "authorization", + "auth_config", "value")) + .stream() + .map(AuthConfig::bearer) + .findFirst() + .orElse(null); + + int maxRetries = Optional + .ofNullable(MapReadUtil.getMapDeepValue(data, String.class, "config", "node_param", "retry_config", + "max_retries")) + .map(Integer::parseInt) + .orElse(1); + long maxRetryInterval = Optional + .ofNullable(MapReadUtil.getMapDeepValue(data, String.class, "config", "node_param", "retry_config", + "retry_interval")) + .map(Long::parseLong) + .orElse(1000L); + boolean enable = Optional + .ofNullable(MapReadUtil.getMapDeepValue(data, String.class, "config", "node_param", "retry_config", + "retry_enabled")) + .map(Boolean::parseBoolean) + .orElse(false); + RetryConfig retryConfig = new RetryConfig(maxRetries, maxRetryInterval, enable); + String outputKey = "output"; + return new HttpNodeData(List.of(), List.of(), httpMethod, url, headers, queryParams, body, auth, + retryConfig, new TimeoutConfig(10, 60, 20, 300, 600, 6000), outputKey); + } + + @Override + public Map dump(HttpNodeData nodeData) { + throw new UnsupportedOperationException(); } - }), CUSTOM(defaultCustomDialectConverter(HttpNodeData.class)); + }) + + , CUSTOM(defaultCustomDialectConverter(HttpNodeData.class)); private final DialectConverter converter; @@ -258,9 +273,9 @@ public BiConsumer> postProcessConsumer(DSLDial return switch (dialectType) { case DIFY -> emptyProcessConsumer().andThen((httpNodeData, idToVarName) -> { // 设置输出键 - httpNodeData.setOutputKey( - httpNodeData.getVarName() + "_" + HttpNodeData.getDefaultOutputSchemas().get(0).getName()); - httpNodeData.setOutputs(HttpNodeData.getDefaultOutputSchemas()); + httpNodeData.setOutputKey(httpNodeData.getVarName() + "_" + + HttpNodeData.getDefaultOutputSchemas(dialectType).get(0).getName()); + httpNodeData.setOutputs(HttpNodeData.getDefaultOutputSchemas(dialectType)); }).andThen(super.postProcessConsumer(dialectType)).andThen((httpNodeData, idToVarName) -> { // 将headers,params,body的Dify参数占位符转化为SAA中间变量 httpNodeData.setHeaders(httpNodeData.getHeaders() @@ -303,6 +318,12 @@ public BiConsumer> postProcessConsumer(DSLDial } } }); + case STUDIO -> emptyProcessConsumer().andThen((httpNodeData, idToVarName) -> { + // 设置输出键 + httpNodeData.setOutputKey(httpNodeData.getVarName() + "_" + + HttpNodeData.getDefaultOutputSchemas(dialectType).get(0).getName()); + httpNodeData.setOutputs(HttpNodeData.getDefaultOutputSchemas(dialectType)); + }).andThen(super.postProcessConsumer(dialectType)); default -> super.postProcessConsumer(dialectType); }; } diff --git a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/generator/workflow/sections/HttpNodeSection.java b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/generator/workflow/sections/HttpNodeSection.java index ac40e57073..7cbc0ce287 100644 --- a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/generator/workflow/sections/HttpNodeSection.java +++ b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/generator/workflow/sections/HttpNodeSection.java @@ -31,6 +31,7 @@ import org.springframework.http.HttpMethod; import org.springframework.stereotype.Component; +// TODO: 支持失败时默认值以及异常分支、支持Studio的多字段输出模式 @Component public class HttpNodeSection implements NodeSection { @@ -78,11 +79,11 @@ public String render(Node node, String varName) { HttpNode.AuthConfig ac = d.getAuthConfig(); if (ac != null) { if (ac.isBasic()) { - sb.append(String.format(".auth(AuthConfig.basic(\"%s\", \"%s\"))%n", escape(ac.getUsername()), + sb.append(String.format(".auth(HttpNode.AuthConfig.basic(\"%s\", \"%s\"))%n", escape(ac.getUsername()), escape(ac.getPassword()))); } else if (ac.isBearer()) { - sb.append(String.format(".auth(AuthConfig.bearer(\"%s\"))%n", escape(ac.getToken()))); + sb.append(String.format(".auth(HttpNode.AuthConfig.bearer(\"%s\"))%n", escape(ac.getToken()))); } } @@ -123,6 +124,19 @@ private NodeAction wrapperHttpNodeAction(NodeAction httpNodeAction, String varNa }; } """; + case STUDIO -> """ + private NodeAction wrapperHttpNodeAction(NodeAction httpNodeAction, String varName) { + return state -> { + String key = varName + "_output"; + Map result = httpNodeAction.apply(state); + Object object = result.get(key); + if(!(object instanceof Map map)) { + return Map.of(); + } + return Map.of(key, map.get("body")); + }; + } + """; default -> ""; }; } From 8d047a25fc64e2e143e3f2fb52f24c7097b041ea Mon Sep 17 00:00:00 2001 From: VLSMB <2047857654@qq.com> Date: Thu, 18 Sep 2025 16:15:07 +0800 Subject: [PATCH 3/3] fix some bugs --- .../ai/graph/node/ParameterParsingNode.java | 3 +++ .../ai/graph/node/QuestionClassifierNode.java | 3 +++ .../generator/model/workflow/NodeType.java | 5 +++++ .../service/dsl/adapters/DifyDSLAdapter.java | 2 +- .../service/dsl/adapters/StudioDSLAdapter.java | 2 +- .../dsl/converter/EmptyNodeDataConverter.java | 4 +--- .../sections/ParameterParsingNodeSection.java | 1 + .../templates/GraphRunController.java.mustache | 17 ++++++----------- 8 files changed, 21 insertions(+), 16 deletions(-) diff --git a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/node/ParameterParsingNode.java b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/node/ParameterParsingNode.java index 7bb083bc17..6645bb324d 100644 --- a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/node/ParameterParsingNode.java +++ b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/node/ParameterParsingNode.java @@ -138,6 +138,9 @@ public ParameterParsingNode(ChatClient chatClient, String inputText, String inpu } private String renderTemplate(OverAllState state, String template) { + if (!StringUtils.hasText(template)) { + return template; + } Map params = Stream.of(template) .map(VAR_TEMPLATE_PATTERN::matcher) .map(Matcher::results) diff --git a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/node/QuestionClassifierNode.java b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/node/QuestionClassifierNode.java index c28c12ca06..6389a827de 100644 --- a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/node/QuestionClassifierNode.java +++ b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/node/QuestionClassifierNode.java @@ -104,6 +104,9 @@ public QuestionClassifierNode(ChatClient chatClient, String inputTextKey, Map params = Stream.of(template) .map(VAR_TEMPLATE_PATTERN::matcher) .map(Matcher::results) diff --git a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/model/workflow/NodeType.java b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/model/workflow/NodeType.java index b8a9b184c8..1b1ab78d0c 100644 --- a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/model/workflow/NodeType.java +++ b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/model/workflow/NodeType.java @@ -94,6 +94,11 @@ public String studioValue() { return this.studioValue; } + public static boolean isEmpty(NodeType nodeType) { + return NodeType.EMPTY.equals(nodeType) || NodeType.ITERATION_START.equals(nodeType) + || NodeType.ITERATION_END.equals(nodeType); + } + public static Optional fromValue(String value) { return Arrays.stream(NodeType.values()).filter(nodeType -> nodeType.value.equals(value)).findFirst(); } diff --git a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/adapters/DifyDSLAdapter.java b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/adapters/DifyDSLAdapter.java index 7038c2c598..6e40c2ea04 100644 --- a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/adapters/DifyDSLAdapter.java +++ b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/adapters/DifyDSLAdapter.java @@ -291,7 +291,7 @@ private List constructNodes(List> nodeMaps) { NodeData data = converter.parseMapData(nodeDataMap, DSLDialectType.DIFY); // Generate a readable varName and inject it into NodeData - int count = counters.merge(nodeType, 1, Integer::sum); + int count = counters.merge(NodeType.isEmpty(nodeType) ? NodeType.EMPTY : nodeType, 1, Integer::sum); String varName = converter.generateVarName(count); data.setVarName(varName); diff --git a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/adapters/StudioDSLAdapter.java b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/adapters/StudioDSLAdapter.java index dd0f931534..e67ea12321 100644 --- a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/adapters/StudioDSLAdapter.java +++ b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/adapters/StudioDSLAdapter.java @@ -242,7 +242,7 @@ private List constructNodes(List> nodeMaps) { NodeData data = converter.parseMapData(nodeMap, DSLDialectType.STUDIO); // Generate a readable varName and inject it into NodeData - int count = counters.merge(nodeType, 1, Integer::sum); + int count = counters.merge(NodeType.isEmpty(nodeType) ? NodeType.EMPTY : nodeType, 1, Integer::sum); String varName = converter.generateVarName(count); data.setVarName(varName); diff --git a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/converter/EmptyNodeDataConverter.java b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/converter/EmptyNodeDataConverter.java index be7103331a..afde72b260 100644 --- a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/converter/EmptyNodeDataConverter.java +++ b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/dsl/converter/EmptyNodeDataConverter.java @@ -75,9 +75,7 @@ protected List> getDialectConverters() { @Override public Boolean supportNodeType(NodeType nodeType) { - // 迭代节点的起始节点与迭代节点共享一个data,故转换时不需要提取数据 - return NodeType.EMPTY.equals(nodeType) || NodeType.ITERATION_START.equals(nodeType) - || NodeType.ITERATION_END.equals(nodeType); + return NodeType.isEmpty(nodeType); } @Override diff --git a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/generator/workflow/sections/ParameterParsingNodeSection.java b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/generator/workflow/sections/ParameterParsingNodeSection.java index 851d065339..fa0168524d 100644 --- a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/generator/workflow/sections/ParameterParsingNodeSection.java +++ b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/java/com/alibaba/cloud/ai/studio/admin/generator/service/generator/workflow/sections/ParameterParsingNodeSection.java @@ -97,6 +97,7 @@ private NodeAction createParameterParsingAction( Map data = (Map) finalRes.remove(dataKey); finalRes.putAll(data.entrySet() .stream() + .filter(e -> e.getValue() != null) .map(e -> Map.entry(outputKeyPrefix + "_" + e.getKey(), e.getValue())) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) diff --git a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/resources/templates/GraphRunController.java.mustache b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/resources/templates/GraphRunController.java.mustache index b665f42a0f..342f24d89a 100644 --- a/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/resources/templates/GraphRunController.java.mustache +++ b/spring-ai-alibaba-studio/spring-ai-alibaba-studio-server/spring-ai-alibaba-studio-server-admin/src/main/resources/templates/GraphRunController.java.mustache @@ -3,7 +3,6 @@ package {{packageName}}.graph; import com.alibaba.cloud.ai.graph.CompiledGraph; import com.alibaba.cloud.ai.graph.NodeOutput; import com.alibaba.cloud.ai.graph.OverAllState; -import com.alibaba.cloud.ai.graph.async.AsyncGenerator; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PostMapping; @@ -13,7 +12,6 @@ import reactor.core.publisher.Flux; import org.springframework.web.bind.annotation.RequestMapping; import java.util.Map; -import java.util.HashMap; @RestController @RequestMapping("/run") @@ -26,21 +24,18 @@ public class GraphRunController { } @PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) - public Flux stream(@RequestBody Map inputs) throws Exception { - AsyncGenerator nodeOutputs = graph.stream(inputs); - return Flux.fromStream(nodeOutputs.stream()); + public Flux stream(@RequestBody Map inputs) { + return graph.fluxStream(inputs); } - @PostMapping(value = "/invoke") - public OverAllState invoke(@RequestBody Map inputs) throws Exception{ - return graph.invoke(inputs).orElse(null); + public OverAllState invoke(@RequestBody Map inputs) { + return graph.call(inputs).orElse(null); } @PostMapping(value = "/start") - public Map startInvoke(@RequestBody Map inputs) throws Exception { - return graph.invoke(inputs).get().data(); + public Map startInvoke(@RequestBody Map inputs) { + return graph.call(inputs).orElseThrow().data(); } - }