From 418428b5c0d58ecbc712e07ae52942e983f120a4 Mon Sep 17 00:00:00 2001 From: shiyiyue1102 Date: Mon, 8 Sep 2025 23:14:55 +0800 Subject: [PATCH 1/6] nacos proxy react agent (#2335) --- pom.xml | 4 + spring-ai-alibaba-agent-nacos/pom.xml | 144 ++++ .../agent/nacos/NacosAgentBuilderFactory.java | 18 + .../ai/agent/nacos/NacosAgentInjector.java | 93 +++ .../ai/agent/nacos/NacosMcpToolsInjector.java | 76 ++ .../ai/agent/nacos/NacosModelInjector.java | 161 ++++ .../cloud/ai/agent/nacos/NacosOptions.java | 44 ++ .../ai/agent/nacos/NacosPromptInjector.java | 179 +++++ .../agent/nacos/NacosReactAgentBuilder.java | 98 +++ .../agent/nacos/ObservationConfigration.java | 29 + .../tools/NacosMcpGatewayToolCallback.java | 734 ++++++++++++++++++ .../NacosMcpGatewayToolsInitializer.java | 143 ++++ .../cloud/ai/agent/nacos/vo/AgentVO.java | 11 + .../cloud/ai/agent/nacos/vo/McpServersVO.java | 31 + .../cloud/ai/agent/nacos/vo/ModelVO.java | 16 + .../cloud/ai/agent/nacos/vo/PromptVO.java | 18 + spring-ai-alibaba-bom/pom.xml | 3 + .../alibaba/cloud/ai/graph/agent/Builder.java | 164 ++++ .../cloud/ai/graph/agent/DefaultBuilder.java | 55 ++ .../cloud/ai/graph/agent/ReactAgent.java | 305 +++----- .../agent/factory/AgentBuilderFactory.java | 8 + .../factory/DefaultAgentBuilderFactory.java | 12 + .../alibaba/cloud/ai/graph/node/LlmNode.java | 22 +- .../alibaba/cloud/ai/graph/node/ToolNode.java | 2 +- 24 files changed, 2166 insertions(+), 204 deletions(-) create mode 100644 spring-ai-alibaba-agent-nacos/pom.xml create mode 100644 spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosAgentBuilderFactory.java create mode 100644 spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosAgentInjector.java create mode 100644 spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosMcpToolsInjector.java create mode 100644 spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosModelInjector.java create mode 100644 spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosOptions.java create mode 100644 spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosPromptInjector.java create mode 100644 spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosReactAgentBuilder.java create mode 100644 spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/ObservationConfigration.java create mode 100644 spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/tools/NacosMcpGatewayToolCallback.java create mode 100644 spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/tools/NacosMcpGatewayToolsInitializer.java create mode 100644 spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/AgentVO.java create mode 100644 spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/McpServersVO.java create mode 100644 spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/ModelVO.java create mode 100644 spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/PromptVO.java create mode 100644 spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/Builder.java create mode 100644 spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/DefaultBuilder.java create mode 100644 spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/factory/AgentBuilderFactory.java create mode 100644 spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/factory/DefaultAgentBuilderFactory.java diff --git a/pom.xml b/pom.xml index fec9bb05af..41a1b7dd08 100644 --- a/pom.xml +++ b/pom.xml @@ -59,6 +59,7 @@ spring-ai-alibaba-studio spring-ai-alibaba-jmanus spring-ai-alibaba-deepresearch + spring-ai-alibaba-agent-nacos community/tool-calls/spring-ai-alibaba-starter-tool-calling-common @@ -411,6 +412,9 @@ io.spring.javaformat spring-javaformat-maven-plugin ${spring-javaformat-maven-plugin.version} + + true + diff --git a/spring-ai-alibaba-agent-nacos/pom.xml b/spring-ai-alibaba-agent-nacos/pom.xml new file mode 100644 index 0000000000..e7d812c0e6 --- /dev/null +++ b/spring-ai-alibaba-agent-nacos/pom.xml @@ -0,0 +1,144 @@ + + + 4.0.0 + + com.alibaba.cloud.ai + spring-ai-alibaba + ${revision} + ../pom.xml + + spring-ai-alibaba-agent-nacos + Spring AI Alibaba Agent Nacos + spring-ai-alibaba-agent-nacos + + + + + + + + + + + + + + + 17 + 1.18.30 + + + + + org.springframework.boot + spring-boot-starter + + + + com.alibaba.nacos + nacos-maintainer-client + ${nacos3.version} + + + + com.alibaba.nacos + nacos-common + + + com.alibaba.nacos + nacos-api + + + + + + org.springframework.ai + spring-ai-commons + + + + org.springframework.ai + spring-ai-retry + + + + org.springframework.ai + spring-ai-autoconfigure-retry + + + + com.alibaba.cloud.ai + spring-ai-alibaba-core + ${revision} + + + + com.alibaba.cloud.ai + spring-ai-alibaba-mcp-registry + ${revision} + + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.alibaba.cloud.ai + spring-ai-alibaba-graph-core + ${revision} + + + + com.alibaba.cloud.ai + spring-ai-alibaba-mcp-router + ${revision} + + + com.alibaba.nacos + nacos-client + + + com.alibaba.nacos + nacos-common + + + + + + com.alibaba.nacos + nacos-client + ${nacos3.version} + + + + com.alibaba.nacos + nacos-common + + + + + + org.projectlombok + lombok + ${lombok.version} + + + + org.springframework.ai + spring-ai-openai + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosAgentBuilderFactory.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosAgentBuilderFactory.java new file mode 100644 index 0000000000..2691f40662 --- /dev/null +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosAgentBuilderFactory.java @@ -0,0 +1,18 @@ +package com.alibaba.cloud.ai.agent.nacos; + +import com.alibaba.cloud.ai.graph.agent.Builder; +import com.alibaba.cloud.ai.graph.agent.factory.AgentBuilderFactory; + +public class NacosAgentBuilderFactory implements AgentBuilderFactory { + + private NacosOptions nacosOptions; + + public NacosAgentBuilderFactory(NacosOptions nacosOptions) { + this.nacosOptions = nacosOptions; + } + + @Override + public Builder builder() { + return new NacosReactAgentBuilder().nacosOptions(this.nacosOptions); + } +} diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosAgentInjector.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosAgentInjector.java new file mode 100644 index 0000000000..f59c1e0e7f --- /dev/null +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosAgentInjector.java @@ -0,0 +1,93 @@ +package com.alibaba.cloud.ai.agent.nacos; + +import com.alibaba.cloud.ai.agent.nacos.vo.AgentVO; +import com.alibaba.cloud.ai.agent.nacos.vo.ModelVO; +import com.alibaba.cloud.ai.agent.nacos.vo.PromptVO; +import com.alibaba.fastjson.JSON; +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.client.config.NacosConfigService; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.model.ChatModel; + +public class NacosAgentInjector { + + public static void injectPrompt(NacosConfigService nacosConfigService, ChatClient chatClient, String promptKey) { + + try { + PromptVO promptVO = NacosPromptInjector.getPromptByKey(nacosConfigService, promptKey); + if (promptVO != null) { + NacosPromptInjector.replacePrompt(chatClient, promptVO); + } + NacosPromptInjector.registerPromptListener(nacosConfigService, chatClient, promptKey); + } + + catch (Exception e) { + throw new RuntimeException(e); + } + + } + + /** + * load prompt by agent id. + * + * @param nacosConfigService + * @param agentId + * @return + */ + public static AgentVO loadAgentVO(NacosConfigService nacosConfigService, String agentId) { + try { + String config = nacosConfigService.getConfig(String.format("agent-%s.json", agentId), "nacos-ai-agent", + 3000L); + return JSON.parseObject(config, AgentVO.class); + } + catch (NacosException e) { + throw new RuntimeException(e); + } + } + + public static void injectPromptByAgentId(NacosConfigService nacosConfigService, ChatClient chatClient, String agentId) { + + try { + AgentVO agentVO = loadAgentVO(nacosConfigService, agentId); + if (agentVO == null) { + return; + } + PromptVO promptVO = NacosPromptInjector.loadPromptByAgentId(nacosConfigService, agentVO); + if (promptVO != null) { + NacosPromptInjector.replacePrompt(chatClient, promptVO); + } + NacosPromptInjector.registryPromptByAgentId(chatClient, nacosConfigService, agentId, promptVO); + } + + catch (Exception e) { + throw new RuntimeException(e); + } + + } + + public static void injectModel(NacosOptions nacosOptions, ChatClient chatClient, String agentId) { + ModelVO modelVO = NacosModelInjector.getModelByAgentId(nacosOptions, agentId); + if (modelVO != null) { + try { + ChatModel chatModel = NacosModelInjector.initModel(nacosOptions, modelVO); + NacosModelInjector.replaceModel(chatClient, chatModel); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + NacosModelInjector.registerModelListener(chatClient, nacosOptions, agentId); + } + + + public static ChatModel initModel(NacosOptions nacosOptions, String agentId) { + ModelVO modelVo = NacosModelInjector.getModelByAgentId(nacosOptions, agentId); + if (modelVo == null) { + return null; + } + + return NacosModelInjector.initModel(nacosOptions, modelVo); + } + +} diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosMcpToolsInjector.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosMcpToolsInjector.java new file mode 100644 index 0000000000..bda3b1f144 --- /dev/null +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosMcpToolsInjector.java @@ -0,0 +1,76 @@ +package com.alibaba.cloud.ai.agent.nacos; + +import java.util.List; + +import com.alibaba.cloud.ai.agent.nacos.tools.NacosMcpGatewayToolsInitializer; +import com.alibaba.cloud.ai.agent.nacos.vo.McpServersVO; +import com.alibaba.cloud.ai.graph.node.LlmNode; +import com.alibaba.cloud.ai.graph.node.ToolNode; +import com.alibaba.cloud.ai.mcp.gateway.nacos.properties.NacosMcpGatewayProperties; +import com.alibaba.fastjson.JSON; +import com.alibaba.nacos.api.config.listener.AbstractListener; +import com.alibaba.nacos.api.exception.NacosException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.ai.tool.ToolCallback; + +public class NacosMcpToolsInjector { + + private static final Logger logger = LoggerFactory.getLogger(NacosMcpToolsInjector.class); + + public static List loadMcpTools(NacosOptions nacosOptions, String agentId) { + McpServersVO mcpServersVO = getMcpServersVO(nacosOptions, agentId); + if (mcpServersVO != null) { + return convert(nacosOptions, mcpServersVO); + } + return null; + } + + public static void registry(LlmNode llmNode, ToolNode toolNode, NacosOptions nacosOptions, String agentId) { + + try { + nacosOptions.getNacosConfigService() + .addListener(String.format("mcp-servers-%s.json", agentId), "nacos-ai-agent", new AbstractListener() { + @Override + public void receiveConfigInfo(String configInfo) { + McpServersVO mcpServersVO = JSON.parseObject(configInfo, McpServersVO.class); + List toolCallbacks = convert(nacosOptions, mcpServersVO); + if (toolCallbacks != null) { + toolNode.setToolCallbacks(toolCallbacks); + llmNode.setToolCallbacks(toolCallbacks); + } + + } + }); + } + catch (NacosException e) { + throw new RuntimeException(e); + } + + } + + public static McpServersVO getMcpServersVO(NacosOptions nacosOptions, String agentId) { + try { + String config = nacosOptions.getNacosConfigService() + .getConfig(String.format("mcp-servers-%s.json", agentId), "nacos-ai-agent", 3000L); + return JSON.parseObject(config, McpServersVO.class); + } + catch (NacosException e) { + throw new RuntimeException(e); + } + } + + public static List convert(NacosOptions nacosOptions, McpServersVO mcpServersVO) { + + NacosMcpGatewayProperties nacosMcpGatewayProperties = new NacosMcpGatewayProperties(); + nacosMcpGatewayProperties.setServiceNames(mcpServersVO.getMcpServers().stream() + .map(McpServersVO.McpServerVO::getMcpServerName).toList()); + NacosMcpGatewayToolsInitializer nacosMcpGatewayToolsInitializer = new NacosMcpGatewayToolsInitializer( + nacosOptions.mcpOperationService, nacosMcpGatewayProperties, mcpServersVO.getMcpServers()); + List toolCallbacks = nacosMcpGatewayToolsInitializer.initializeTools(); + + return toolCallbacks; + } + +} diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosModelInjector.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosModelInjector.java new file mode 100644 index 0000000000..11676d94d9 --- /dev/null +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosModelInjector.java @@ -0,0 +1,161 @@ +package com.alibaba.cloud.ai.agent.nacos; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import com.alibaba.cloud.ai.agent.nacos.vo.ModelVO; +import com.alibaba.fastjson.JSON; +import com.alibaba.nacos.api.config.listener.AbstractListener; +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.common.utils.StringUtils; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.api.OpenAiApi; + +public class NacosModelInjector { + + + public static ModelVO getModelByAgentId(NacosOptions nacosOptions, String agentId) { + try { + String dataIdT = String.format(nacosOptions.isModelConfigEncrypted() ? "cipher-kms-aes-256-model-%s.json" : "model-%s.json", agentId); + String config = nacosOptions.getNacosConfigService().getConfig(dataIdT, "nacos-ai-agent", 3000L); + return JSON.parseObject(config, ModelVO.class); + } + catch (NacosException e) { + throw new RuntimeException(e); + } + } + + public static ChatModel initModel(NacosOptions nacosOptions, ModelVO model) { + + OpenAiApi openAiApi = OpenAiApi.builder() + .apiKey(model.getApiKey()).baseUrl(model.getBaseUrl()) + .build(); + + OpenAiChatOptions.Builder chatOptionsBuilder = OpenAiChatOptions.builder(); + if (model.getTemperature() != null) { + chatOptionsBuilder.temperature(Double.parseDouble(model.getTemperature())); + } + if (model.getMaxTokens() != null) { + chatOptionsBuilder.maxTokens(Integer.parseInt(model.getMaxTokens())); + } + chatOptionsBuilder.internalToolExecutionEnabled(false); + OpenAiChatOptions openaiChatOptions = chatOptionsBuilder + .model(model.getModel()) + .build(); + OpenAiChatModel.Builder builder = OpenAiChatModel.builder().defaultOptions(openaiChatOptions) + .openAiApi(openAiApi); + + //inject observation config. + ObservationConfigration observationConfigration = nacosOptions.getObservationConfigration(); + if (observationConfigration != null) { + if (observationConfigration.getToolCallingManager() != null) { + builder.toolCallingManager(observationConfigration.getToolCallingManager()); + } + if (observationConfigration.getObservationRegistry() != null) { + builder.observationRegistry(observationConfigration.getObservationRegistry()); + } + } + + OpenAiChatModel openAiChatModel = builder.build(); + if (observationConfigration != null && observationConfigration.getChatModelObservationConvention() != null) { + openAiChatModel.setObservationConvention(observationConfigration + .getChatModelObservationConvention()); + } + + return openAiChatModel; + } + + public static void registerModelListener(ChatClient chatClient, NacosOptions nacosOptions, String agentId) { + if (StringUtils.isBlank(agentId)) { + return; + } + try { + String dataIdT = String.format(nacosOptions.isModelConfigEncrypted() ? "cipher-kms-aes-256-model-%s.json" : "model-%s.json", agentId); + + nacosOptions.getNacosConfigService().addListener(dataIdT, "nacos-ai-agent", new AbstractListener() { + @Override + public void receiveConfigInfo(String configInfo) { + ModelVO modelVO = JSON.parseObject(configInfo, ModelVO.class); + try { + ChatModel chatModelNew = initModel(nacosOptions, modelVO); + replaceModel(chatClient, chatModelNew); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + }); + } + catch (NacosException e) { + throw new RuntimeException(e); + } + } + + public static void replaceModel(ChatClient chatClient, ChatModel chatModel) throws Exception { + Object defaultChatClientRequest = getField(chatClient, "defaultChatClientRequest"); + modifyFinalField(defaultChatClientRequest, "chatModel", chatModel); + } + + private static Object getField(Object obj, String fieldName) throws Exception { + Field field = obj.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(obj); + } + + public static void modifyFinalField(Object targetObject, String fieldName, Object newValue) throws Exception { + Field field = targetObject.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + + try { + // Java 8及以下版本的方式 + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~java.lang.reflect.Modifier.FINAL); + field.set(targetObject, newValue); + } + catch (NoSuchFieldException e) { + // Java 9及以上版本的方式 + try { + // 使用反射修改final字段 + Field[] fields = field.getClass().getDeclaredFields(); + for (Field f : fields) { + if ("modifiers".equals(f.getName())) { + f.setAccessible(true); + f.setInt(field, field.getModifiers() & ~java.lang.reflect.Modifier.FINAL); + break; + } + } + field.set(targetObject, newValue); + } + catch (Exception ex) { + // 如果上述方式都不行,尝试使用Unsafe(不推荐但有时有效) + modifyFinalFieldWithUnsafe(field, targetObject, newValue); + } + } + } + + /** + * 使用Unsafe修改final字段(适用于Java 12+) + */ + private static void modifyFinalFieldWithUnsafe(Field field, Object targetObject, Object newValue) throws Exception { + try { + Class unsafeClass = Class.forName("sun.misc.Unsafe"); + Field unsafeInstanceField = unsafeClass.getDeclaredField("theUnsafe"); + unsafeInstanceField.setAccessible(true); + Object unsafeInstance = unsafeInstanceField.get(null); + + Method putObjectMethod = unsafeClass.getMethod("putObject", Object.class, long.class, Object.class); + Method staticFieldOffsetMethod = unsafeClass.getMethod("staticFieldOffset", Field.class); + + long offset = (Long) staticFieldOffsetMethod.invoke(unsafeInstance, field); + putObjectMethod.invoke(unsafeInstance, targetObject, offset, newValue); + } + catch (Exception e) { + throw new RuntimeException("无法修改final字段: " + field.getName(), e); + } + } +} diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosOptions.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosOptions.java new file mode 100644 index 0000000000..957e8b7ce7 --- /dev/null +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosOptions.java @@ -0,0 +1,44 @@ +package com.alibaba.cloud.ai.agent.nacos; + +import java.util.Properties; + +import com.alibaba.cloud.ai.mcp.nacos.service.NacosMcpOperationService; +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.client.config.NacosConfigService; +import com.alibaba.nacos.maintainer.client.ai.AiMaintainerService; +import com.alibaba.nacos.maintainer.client.ai.NacosAiMaintainerServiceImpl; +import lombok.Data; + +@Data +public class NacosOptions { + + protected boolean modelSpecified; + + protected boolean modelConfigEncrypted; + + protected boolean promptSpecified; + + String promptKey; + + private NacosConfigService nacosConfigService; + + private AiMaintainerService nacosAiMaintainerService; + + NacosMcpOperationService mcpOperationService; + + private ObservationConfigration observationConfigration; + + private String agentName; + + private String mcpNamespace; + + public NacosOptions(Properties properties) throws NacosException { + nacosConfigService = new NacosConfigService(properties); + nacosAiMaintainerService = new NacosAiMaintainerServiceImpl(properties); + mcpOperationService = new NacosMcpOperationService(properties); + agentName = properties.getProperty("agentName"); + mcpNamespace = properties.getProperty("mcpNamespace", properties.getProperty("namespace")); + + } + +} diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosPromptInjector.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosPromptInjector.java new file mode 100644 index 0000000000..4ad28fb5bd --- /dev/null +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosPromptInjector.java @@ -0,0 +1,179 @@ +package com.alibaba.cloud.ai.agent.nacos; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import com.alibaba.cloud.ai.agent.nacos.vo.AgentVO; +import com.alibaba.cloud.ai.agent.nacos.vo.PromptVO; +import com.alibaba.fastjson.JSON; +import com.alibaba.nacos.api.config.listener.AbstractListener; +import com.alibaba.nacos.api.config.listener.Listener; +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.client.config.NacosConfigService; +import com.alibaba.nacos.common.utils.StringUtils; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.client.DefaultChatClient; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.util.ReflectionUtils; + +public class NacosPromptInjector { + + static Map promptKeyListener = new HashMap<>(); + + + /** + * load prompt by agent id. + * + * @param nacosConfigService + * @param agentId + * @return + */ + public static PromptVO loadPromptByAgentId(NacosConfigService nacosConfigService, AgentVO agentVO) { + try { + return getPromptByKey(nacosConfigService, agentVO.getPromptKey()); + } + catch (NacosException e) { + throw new RuntimeException(e); + } + } + + + /** + * load promot by prompt key. + * + * @param nacosConfigService + * @param promptKey + * @return + * @throws NacosException + */ + public static PromptVO getPromptByKey(NacosConfigService nacosConfigService, String promptKey) + throws NacosException { + String promptConfig = nacosConfigService.getConfig(String.format("prompt-%s.json", promptKey), "nacos-ai-meta", + 3000L); + PromptVO promptVO = JSON.parseObject(promptConfig, PromptVO.class); + promptVO.setPromptKey(promptKey); + return promptVO; + } + + /** + * replace prompt info by key and registry. + * + * @param nacosConfigService + * @param chatClient + * @param promptKey + * @throws Exception + */ + public static void injectPromptByKey(NacosConfigService nacosConfigService, ChatClient chatClient, String promptKey) + throws Exception { + PromptVO promptVO = NacosPromptInjector.getPromptByKey(nacosConfigService, promptKey); + if (promptVO != null) { + NacosPromptInjector.replacePrompt(chatClient, promptVO); + } + NacosPromptInjector.registerPromptListener(nacosConfigService, chatClient, promptKey); + } + + public static void registryPromptByAgentId(ChatClient chatClient, NacosConfigService nacosConfigService, + String agentId, PromptVO promptVO) { + + try { + + // register agent prompt listener + nacosConfigService.addListener(String.format("prompt-%s.json", agentId), "nacos-ai-agent", + new AbstractListener() { + + String currentPromptKey; + + @Override + public void receiveConfigInfo(String configInfo) { + if (StringUtils.isBlank(configInfo)) { + return; + } + String newPromptKey = (String) JSON.parseObject(configInfo).get("promptKey"); + if (StringUtils.isBlank(newPromptKey) || newPromptKey.equals(currentPromptKey)) { + return; + } + try { + injectPromptByKey(nacosConfigService, chatClient, newPromptKey); + } + catch (Exception e) { + throw new RuntimeException(e); + } + if (promptKeyListener.containsKey(currentPromptKey)) { + Listener listener = promptKeyListener.remove(currentPromptKey); + nacosConfigService.removeListener(String.format("prompt-%s.json", currentPromptKey), + "nacos-ai-meta", listener); + } + currentPromptKey = newPromptKey; + } + }); + if (promptVO != null && promptVO.getPromptKey() != null) { + registerPromptListener(nacosConfigService, chatClient, promptVO.getPromptKey()); + } + + } + catch (NacosException e) { + throw new RuntimeException(e); + } + } + + /** + * register prompt listener + * + * @param nacosConfigService + * @param chatClient + * @param promptKey + * @throws NacosException + */ + public static void registerPromptListener(NacosConfigService nacosConfigService, ChatClient chatClient, + String promptKey) throws NacosException { + try { + + Listener listener = new AbstractListener() { + @Override + public void receiveConfigInfo(String configInfo) { + PromptVO promptVO = JSON.parseObject(configInfo, PromptVO.class); + try { + replacePrompt(chatClient, promptVO); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + }; + + nacosConfigService.addListener(String.format("prompt-%s.json", promptKey), "nacos-ai-meta", listener); + promptKeyListener.put(promptKey, listener); + } + catch (NacosException e) { + throw new RuntimeException(e); + } + } + + + public static void replacePrompt(ChatClient chatClient, PromptVO promptVO) throws Exception { + Field defaultChatClientRequest = chatClient.getClass().getDeclaredField("defaultChatClientRequest"); + ReflectionUtils.makeAccessible(defaultChatClientRequest); + DefaultChatClient.DefaultChatClientRequestSpec object = (DefaultChatClient.DefaultChatClientRequestSpec) defaultChatClientRequest.get( + chatClient); + Field systemText = object.getClass().getDeclaredField("systemText"); + ReflectionUtils.makeAccessible(systemText); + ReflectionUtils.setField(systemText, object, promptVO.getTemplate()); + + Field chatOptionsFeild = object.getClass().getDeclaredField("chatOptions"); + chatOptionsFeild.setAccessible(true); + ChatOptions chatOptions = (ChatOptions) chatOptionsFeild.get(object); + Field metadataFiled = chatOptions.getClass().getDeclaredField("metadata"); + metadataFiled.setAccessible(true); + Map metadata = (Map) metadataFiled.get(chatOptions); + if (metadata == null) { + metadata = new HashMap<>(); + } + metadata.put("promptKey", promptVO.getPromptKey()); + metadata.put("promptVersion", promptVO.getVersion()); + metadataFiled.set(chatOptions, metadata); + + } + +} diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosReactAgentBuilder.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosReactAgentBuilder.java new file mode 100644 index 0000000000..da7101082c --- /dev/null +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosReactAgentBuilder.java @@ -0,0 +1,98 @@ +package com.alibaba.cloud.ai.agent.nacos; + +import java.util.List; + +import com.alibaba.cloud.ai.graph.agent.Builder; +import com.alibaba.cloud.ai.graph.agent.DefaultBuilder; +import com.alibaba.cloud.ai.graph.agent.ReactAgent; +import com.alibaba.cloud.ai.graph.exception.GraphStateException; +import com.alibaba.cloud.ai.graph.node.LlmNode; +import com.alibaba.cloud.ai.graph.node.ToolNode; +import com.alibaba.nacos.common.utils.StringUtils; +import io.micrometer.observation.ObservationRegistry; +import org.apache.commons.collections4.CollectionUtils; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.tool.ToolCallback; + +public class NacosReactAgentBuilder extends DefaultBuilder { + + private NacosOptions nacosOptions; + + public NacosReactAgentBuilder nacosOptions(NacosOptions nacosOptions) { + this.nacosOptions = nacosOptions; return this; + } + + @Override + public Builder model(ChatModel model) { + super.model(model); nacosOptions.modelSpecified = true; return this; + } + + public Builder instruction(String instruction) { + super.instruction(instruction); nacosOptions.promptSpecified = true; return this; + } + + @Override + public ReactAgent build() throws GraphStateException { + if (super.name == null) { + this.name = nacosOptions.getAgentName(); + } if (model == null && StringUtils.isNotBlank(this.name)) { + this.model = NacosAgentInjector.initModel(nacosOptions, this.name); + } if (chatClient == null) { + ChatClient.Builder clientBuilder = null; + + ObservationConfigration observationConfigration = nacosOptions.getObservationConfigration(); + if (observationConfigration == null) { + clientBuilder = ChatClient.builder(model); + } + else { + clientBuilder = ChatClient.builder(model, observationConfigration.getObservationRegistry() == null ? ObservationRegistry.NOOP : observationConfigration.getObservationRegistry(), nacosOptions.getObservationConfigration() + .getChatClientObservationConvention()); + } + + if (chatOptions != null) { + clientBuilder.defaultOptions(chatOptions); + } if (instruction != null) { + clientBuilder.defaultSystem(instruction); + } chatClient = clientBuilder.build(); + } + + if (!nacosOptions.modelSpecified) { + NacosAgentInjector.injectModel(nacosOptions, chatClient, this.name); + } if (!nacosOptions.promptSpecified) { + if (nacosOptions.promptKey != null) { + NacosAgentInjector.injectPrompt(nacosOptions.getNacosConfigService(), chatClient, nacosOptions.promptKey); + + } + else { + NacosAgentInjector.injectPromptByAgentId(nacosOptions.getNacosConfigService(), chatClient, nacosOptions.getAgentName()); + + } + } + + List toolCallbacks = NacosMcpToolsInjector.loadMcpTools(nacosOptions, this.name); + + this.tools = toolCallbacks; + + LlmNode.Builder llmNodeBuilder = LlmNode.builder().stream(true).chatClient(chatClient) + .messagesKey(this.inputKey); if (outputKey != null && !outputKey.isEmpty()) { + llmNodeBuilder.outputKey(outputKey); + } + + if (CollectionUtils.isNotEmpty(tools)) { + llmNodeBuilder.toolCallbacks(tools); + } LlmNode llmNode = llmNodeBuilder.build(); + + ToolNode toolNode = null; if (resolver != null) { + toolNode = ToolNode.builder().toolCallbackResolver(resolver).build(); + } + else if (tools != null) { + toolNode = ToolNode.builder().toolCallbacks(tools).build(); + } + else { + toolNode = ToolNode.builder().build(); + } NacosMcpToolsInjector.registry(llmNode, toolNode, nacosOptions, this.name); + return new ReactAgent(llmNode, toolNode, this); + } +} diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/ObservationConfigration.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/ObservationConfigration.java new file mode 100644 index 0000000000..d5f22d55c4 --- /dev/null +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/ObservationConfigration.java @@ -0,0 +1,29 @@ +package com.alibaba.cloud.ai.agent.nacos; + +import io.micrometer.observation.ObservationRegistry; +import lombok.Data; + +import org.springframework.ai.chat.client.observation.ChatClientObservationConvention; +import org.springframework.ai.chat.observation.ChatModelObservationConvention; +import org.springframework.ai.model.tool.ToolCallingManager; + +@Data +public class ObservationConfigration { + + private ObservationRegistry observationRegistry; + + private ToolCallingManager toolCallingManager; + + private ChatModelObservationConvention chatModelObservationConvention; + + private ChatClientObservationConvention chatClientObservationConvention; + + public ObservationConfigration(ObservationRegistry observationRegistry, ToolCallingManager toolCallingManager, + ChatModelObservationConvention chatModelObservationConvention, + ChatClientObservationConvention chatClientObservationConvention) { + this.observationRegistry = observationRegistry; + this.toolCallingManager = toolCallingManager; + this.chatModelObservationConvention = chatModelObservationConvention; + this.chatClientObservationConvention = chatClientObservationConvention; + } +} diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/tools/NacosMcpGatewayToolCallback.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/tools/NacosMcpGatewayToolCallback.java new file mode 100644 index 0000000000..8dcbae2e1f --- /dev/null +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/tools/NacosMcpGatewayToolCallback.java @@ -0,0 +1,734 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.ai.agent.nacos.tools; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.alibaba.cloud.ai.agent.nacos.vo.McpServersVO; +import com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayToolDefinition; +import com.alibaba.cloud.ai.mcp.gateway.core.jsontemplate.RequestTemplateInfo; +import com.alibaba.cloud.ai.mcp.gateway.core.jsontemplate.RequestTemplateParser; +import com.alibaba.cloud.ai.mcp.gateway.core.jsontemplate.ResponseTemplateParser; +import com.alibaba.cloud.ai.mcp.gateway.nacos.definition.NacosMcpGatewayToolDefinition; +import com.alibaba.cloud.ai.mcp.nacos.service.NacosMcpOperationService; +import com.alibaba.nacos.api.ai.model.mcp.McpEndpointInfo; +import com.alibaba.nacos.api.ai.model.mcp.McpServerRemoteServiceConfig; +import com.alibaba.nacos.api.ai.model.mcp.McpServiceRef; +import com.alibaba.nacos.api.ai.model.mcp.McpToolMeta; +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.common.utils.JacksonUtils; +import com.alibaba.nacos.common.utils.StringUtils; +import com.alibaba.nacos.shaded.com.google.common.collect.Maps; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.client.McpClient; +import io.modelcontextprotocol.client.McpSyncClient; +import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; +import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpSchema.CallToolResult; +import io.modelcontextprotocol.spec.McpSchema.InitializeResult; +import io.modelcontextprotocol.spec.McpSchema.TextContent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +import org.springframework.ai.chat.model.ToolContext; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.definition.ToolDefinition; +import org.springframework.http.HttpMethod; +import org.springframework.lang.NonNull; +import org.springframework.util.CollectionUtils; +import org.springframework.web.reactive.function.client.WebClient; + +public class NacosMcpGatewayToolCallback implements ToolCallback { + + private static final Logger logger = LoggerFactory.getLogger(NacosMcpGatewayToolCallback.class); + + private final NacosMcpGatewayToolDefinition toolDefinition; + + private NacosMcpOperationService nacosMcpOperationService; + + private static final Pattern TEMPLATE_PATTERN = Pattern.compile("\\{\\{\\s*\\.([\\w]*)\\s*\\}\\}"); + + private final WebClient.Builder webClientBuilder; + + private McpServersVO.McpServerVO mcpServerVO; + static ObjectMapper objectMapper = new ObjectMapper(); + + static { + objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + objectMapper.setSerializationInclusion(Include.NON_NULL); + } + + public NacosMcpGatewayToolCallback(final McpGatewayToolDefinition toolDefinition, NacosMcpOperationService nacosMcpOperationService, McpServersVO.McpServerVO mcpServerVO) { + this.webClientBuilder = null; + this.toolDefinition = (NacosMcpGatewayToolDefinition) toolDefinition; + this.nacosMcpOperationService = nacosMcpOperationService; + this.mcpServerVO = mcpServerVO; + // 尝试获取配置属性 + // try { + // NacosMcpGatewayProperties properties = SpringBeanUtils.getInstance() + // .getBean(NacosMcpGatewayProperties.class); + // if (properties != null) { + // logger.info("Loaded gateway properties: maxConnections={}, + // connectionTimeout={}, readTimeout={}", + // properties.getMaxConnections(), properties.getConnectionTimeout(), + // properties.getReadTimeout()); + // } + // } + // catch (Exception e) { + // logger.debug("Failed to load gateway properties, using defaults", e); + // } + } + + /** + * 处理工具请求 + */ + private Mono processToolRequest(String configJson, Map args, String baseUrl) { + try { + JsonNode toolConfig = objectMapper.readTree(configJson); + logger.info("[processToolRequest] toolConfig: {} args: {} baseUrl: {}", toolConfig, args, baseUrl); + + // 验证配置完整性 + if (toolConfig == null || toolConfig.isEmpty()) { + return Mono.error(new IllegalArgumentException("Tool configuration is empty or invalid")); + } + + JsonNode argsNode = toolConfig.path("args"); + Map processedArgs; + if (!argsNode.isMissingNode() && argsNode.isArray() && argsNode.size() > 0) { + processedArgs = processArguments(argsNode, args); + logger.info("[processToolRequest] processedArgs from args: {}", processedArgs); + } + else if (!toolConfig.path("inputSchema").isMissingNode() && toolConfig.path("inputSchema").isObject()) { + // 从 inputSchema.properties 解析参数 + JsonNode properties = toolConfig.path("inputSchema").path("properties"); + if (properties.isObject()) { + processedArgs = new HashMap<>(); + properties.fieldNames().forEachRemaining(field -> { + if (args.containsKey(field)) { + processedArgs.put(field, args.get(field)); + } + }); + logger.info("[processToolRequest] processedArgs from inputSchema: {}", processedArgs); + } + else { + processedArgs = args; + logger.info("[processToolRequest] inputSchema.properties missing, use original args: {}", + processedArgs); + } + } + else { + processedArgs = args; + logger.info("[processToolRequest] no args or inputSchema, use original args: {}", processedArgs); + } + + JsonNode requestTemplate = toolConfig.path("requestTemplate"); + String url = requestTemplate.path("url").asText(); + String method = requestTemplate.path("method").asText(); + logger.info("[processToolRequest] requestTemplate: {} url: {} method: {}", requestTemplate, url, method); + + // 检查URL和方法 + if (url.isEmpty() || method.isEmpty()) { + return Mono.error(new IllegalArgumentException("URL and method are required in requestTemplate")); + } + + // 验证HTTP方法 + try { + HttpMethod.valueOf(method.toUpperCase()); + } + catch (IllegalArgumentException e) { + return Mono.error(new IllegalArgumentException("Invalid HTTP method: " + method)); + } + + // 创建WebClient + baseUrl = baseUrl != null ? baseUrl : "http://localhost"; + WebClient client = webClientBuilder.baseUrl(baseUrl).build(); + + // 构建并执行请求 + return buildAndExecuteRequest(client, requestTemplate, toolConfig.path("responseTemplate"), processedArgs, + baseUrl) + .onErrorResume(e -> { + logger.error("Failed to execute tool request: {}", e.getMessage(), e); + return Mono.error(new RuntimeException("Tool execution failed: " + e.getMessage(), e)); + }); + } + catch (Exception e) { + logger.error("Failed to process tool request", e); + return Mono.error(new RuntimeException("Failed to process tool request: " + e.getMessage(), e)); + } + } + + /** + * 处理参数定义和值 + */ + private Map processArguments(JsonNode argsDefinition, Map providedArgs) { + Map processedArgs = new HashMap<>(); + + if (argsDefinition.isArray()) { + for (JsonNode argDef : argsDefinition) { + String name = argDef.path("name").asText(); + boolean required = argDef.path("required").asBoolean(false); + Object defaultValue = argDef.has("default") + ? objectMapper.convertValue(argDef.path("default"), Object.class) : null; + + // 检查参数 + if (providedArgs.containsKey(name)) { + processedArgs.put(name, providedArgs.get(name)); + } + else if (defaultValue != null) { + processedArgs.put(name, defaultValue); + } + else if (required) { + throw new IllegalArgumentException("Required argument missing: " + name); + } + } + } + + return processedArgs; + } + + /** + * 构建并执行WebClient请求 + */ + private Mono buildAndExecuteRequest(WebClient client, JsonNode requestTemplate, JsonNode responseTemplate, + Map args, String baseUrl) { + + RequestTemplateInfo info = RequestTemplateParser.parseRequestTemplate(requestTemplate); + String url = info.url; + String method = info.method; + HttpMethod httpMethod = HttpMethod.valueOf(method.toUpperCase()); + + // 处理URL中的路径参数 + String processedUrl = processTemplateString(url, args); + logger.info("[buildAndExecuteRequest] original url template: {} processed url: {}", url, processedUrl); + + // 构建请求 + WebClient.RequestBodySpec requestBodySpec = client.method(httpMethod) + .uri(builder -> RequestTemplateParser.buildUri(builder, processedUrl, info, args)); + + // 添加请求头 + RequestTemplateParser.addHeaders(requestBodySpec, info.headers, args, this::processTemplateString); + + // 处理请求体 + WebClient.RequestHeadersSpec headersSpec = RequestTemplateParser.addRequestBody(requestBodySpec, info, args, + this::processTemplateString, objectMapper, logger); + + // 输出最终请求信息 + String fullUrl = baseUrl.endsWith("/") && processedUrl.startsWith("/") ? baseUrl + processedUrl.substring(1) + : baseUrl + processedUrl; + logger.info("[buildAndExecuteRequest] final request: method={} url={} args={}", method, fullUrl, args); + + return headersSpec.retrieve() + .onStatus(status -> status.is4xxClientError(), + response -> Mono.error(new RuntimeException("Client error: " + response.statusCode()))) + .onStatus(status -> status.is5xxServerError(), + response -> Mono.error(new RuntimeException("Server error: " + response.statusCode()))) + .bodyToMono(String.class) + .timeout(getTimeoutDuration()) // 使用配置的超时时间 + .doOnNext(responseBody -> logger.info("[buildAndExecuteRequest] received responseBody: {}", responseBody)) + .map(responseBody -> processResponse(responseBody, responseTemplate, args)) + .onErrorResume(e -> { + logger.error("[buildAndExecuteRequest] Request failed: {}", e.getMessage(), e); + return Mono.error(new RuntimeException("HTTP request failed: " + e.getMessage(), e)); + }); + } + + /** + * 处理响应 + */ + private String processResponse(String responseBody, JsonNode responseTemplate, Map args) { + logger.info("[processResponse] received responseBody: {}", responseBody); + String result = null; + if (!responseTemplate.isEmpty()) { + if (responseTemplate.has("body") && !responseTemplate.path("body").asText().isEmpty()) { + String bodyTemplate = responseTemplate.path("body").asText(); + // 统一交给 ResponseTemplateParser 处理 + result = ResponseTemplateParser.parse(responseBody, bodyTemplate); + logger.info("[processResponse] ResponseTemplateParser result: {}", result); + return result; + } + else if (responseTemplate.has("prependBody") || responseTemplate.has("appendBody")) { + String prependText = responseTemplate.path("prependBody").asText(""); + String appendText = responseTemplate.path("appendBody").asText(""); + result = processTemplateString(prependText, args) + responseBody + + processTemplateString(appendText, args); + logger.info("[processResponse] prepend/append result: {}", result); + return result; + } + } + result = responseBody; + logger.info("[processResponse] default result: {}", result); + return result; + } + + /** + * 处理模板字符串中的变量 + */ + private String processTemplateString(String template, Map data) { + logger.debug("[processTemplateString] template: {} data: {}", template, data); + if (template == null || template.isEmpty()) { + return ""; + } + + Matcher matcher = TEMPLATE_PATTERN.matcher(template); + StringBuilder result = new StringBuilder(); + while (matcher.find()) { + String variable = matcher.group(1); + String replacement; + if ("".equals(variable) || ".".equals(variable)) { + // 特殊处理{{.}},输出data唯一值或整个data + if (data != null && data.size() == 1) { + replacement = String.valueOf(data.values().iterator().next()); + } + else if (data != null && !data.isEmpty()) { + replacement = data.toString(); + } + else { + replacement = ""; + } + } + else { + Object value = data != null ? data.get(variable) : null; + if (value == null) { + logger.warn("[processTemplateString] Variable '{}' not found in data, using empty string", + variable); + replacement = ""; + } + else { + replacement = value.toString(); + } + } + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } + matcher.appendTail(result); + String finalResult = result.toString(); + logger.debug("[processTemplateString] final result: {}", finalResult); + + // 验证是否还存在未被替换的{{.}},如有则输出警告 + if (finalResult.contains("{{.}}")) { + logger.warn("[processTemplateString] WARNING: {{.}} was not replaced in result: {}", finalResult); + } + + return finalResult; + } + + @Override + public ToolDefinition getToolDefinition() { + return this.toolDefinition; + } + + @Override + public String call(@NonNull final String input) { + return call(input, new ToolContext(Maps.newHashMap())); + } + + @Override + @SuppressWarnings("unchecked") + public String call(@NonNull final String input, final ToolContext toolContext) { + try { + logger.info("[call] input: {} toolContext: {}", input, JacksonUtils.toJson(toolContext)); + + // 参数验证 + if (this.toolDefinition == null) { + throw new IllegalStateException("Tool definition is null"); + } + + // input解析 + logger.info("[call] input string: {}", input); + Map args = new HashMap<>(); + if (!input.isEmpty()) { + try { + args = objectMapper.readValue(input, Map.class); + logger.info("[call] parsed args: {}", args); + } + catch (Exception e) { + logger.error("[call] Failed to parse input to args", e); + // 如果解析失败,尝试作为单个参数处理 + args.put("input", input); + } + } + + String protocol = this.toolDefinition.getProtocol(); + if (protocol == null) { + throw new IllegalStateException("Protocol is null"); + } + + // 根据协议类型分发到不同的处理方法 + if ("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) { + McpServerRemoteServiceConfig remoteServerConfig = this.toolDefinition.getRemoteServerConfig(); + if (remoteServerConfig == null) { + throw new IllegalStateException("Remote server config is null"); + } + + return handleHttpHttpsProtocol(args, remoteServerConfig, protocol); + } + else if ("mcp-sse".equalsIgnoreCase(protocol)) { + McpServerRemoteServiceConfig remoteServerConfig = this.toolDefinition.getRemoteServerConfig(); + if (remoteServerConfig == null) { + throw new IllegalStateException("Remote server config is null"); + } + return handleMcpStreamProtocol(args, remoteServerConfig, protocol); + } + else if ("mcp-streamable".equalsIgnoreCase(protocol)) { + + logger.error("[call] Unsupported protocol: {}", protocol); + return "Error: Unsupported protocol " + protocol; + // McpServerRemoteServiceConfig remoteServerConfig = + // this.toolDefinition.getRemoteServerConfig(); + // if (remoteServerConfig == null) { + // throw new IllegalStateException("Remote server config is null"); + // } + // return handleMcpStreamableProtocol(args, remoteServerConfig, protocol); + } + else { + logger.error("[call] Unsupported protocol: {}", protocol); + return "Error: Unsupported protocol " + protocol; + } + } + catch (Exception e) { + logger.error("[call] Unexpected error occurred", e); + return "Error: " + e.getMessage(); + } + } + + /** + * 处理HTTP/HTTPS协议的工具调用 + */ + private String handleHttpHttpsProtocol(Map args, McpServerRemoteServiceConfig remoteServerConfig, + String protocol) throws NacosException { + McpServiceRef serviceRef = remoteServerConfig.getServiceRef(); + if (serviceRef != null) { + McpEndpointInfo mcpEndpointInfo = nacosMcpOperationService.selectEndpoint(serviceRef); + if (mcpEndpointInfo == null) { + throw new RuntimeException("No available endpoint found for service: " + serviceRef.getServiceName()); + } + + logger.info("Tool callback instance: {}", JacksonUtils.toJson(mcpEndpointInfo)); + McpToolMeta toolMeta = this.toolDefinition.getToolMeta(); + String baseUrl = protocol + "://" + mcpEndpointInfo.getAddress() + ":" + mcpEndpointInfo.getPort(); + + if (toolMeta != null && toolMeta.getTemplates() != null) { + Map templates = toolMeta.getTemplates(); + if (templates != null && templates.containsKey("json-go-template")) { + Object jsonGoTemplate = templates.get("json-go-template"); + try { + logger.info("[handleHttpHttpsProtocol] json-go-template: {}", + objectMapper.writeValueAsString(jsonGoTemplate)); + } + catch (JsonProcessingException e) { + logger.error("[handleHttpHttpsProtocol] Failed to serialize json-go-template", e); + } + try { + // 调用executeToolRequest + String configJson = objectMapper.writeValueAsString(jsonGoTemplate); + logger.info("[handleHttpHttpsProtocol] configJson: {} args: {} baseUrl: {}", configJson, args, + baseUrl); + return processToolRequest(configJson, args, baseUrl).block(); + } + catch (Exception e) { + logger.error("Failed to execute tool request", e); + return "Error: " + e.getMessage(); + } + } + else { + logger.warn("[handleHttpHttpsProtocol] json-go-template not found in templates"); + return "Error: json-go-template not found in tool configuration"; + } + } + else { + logger.warn("[handleHttpHttpsProtocol] templates not found in toolsMeta"); + return "Error: templates not found in tool metadata"; + } + } + else { + logger.error("[handleHttpHttpsProtocol] serviceRef is null"); + return "Error: service reference is null"; + } + } + + /** + * 处理MCP流式协议的工具调用 (mcp-sse, mcp-streamable) + */ + private String handleMcpStreamProtocol(Map args, McpServerRemoteServiceConfig remoteServerConfig, + String protocol) throws NacosException { + McpServiceRef serviceRef = remoteServerConfig.getServiceRef(); + if (serviceRef != null) { + McpEndpointInfo mcpEndpointInfo = nacosMcpOperationService.selectEndpoint(serviceRef); + if (mcpEndpointInfo == null) { + throw new RuntimeException("No available endpoint found for service: " + serviceRef.getServiceName()); + } + + logger.info("[handleMcpStreamProtocol] Tool callback instance: {}", JacksonUtils.toJson(mcpEndpointInfo)); + String exportPath = remoteServerConfig.getExportPath(); + + // 构建基础URL,根据协议类型调整 + String transportProtocol = StringUtils.isNotBlank(serviceRef.getTransportProtocol()) ? serviceRef.getTransportProtocol() : "http"; + StringBuilder baseUrl; + if ("mcp-sse".equalsIgnoreCase(protocol)) { + baseUrl = new StringBuilder(transportProtocol + "://" + mcpEndpointInfo.getAddress() + ":" + mcpEndpointInfo.getPort()); + } + else { + // mcp-streamable 或其他协议 + baseUrl = new StringBuilder(transportProtocol + "://" + mcpEndpointInfo.getAddress() + ":" + mcpEndpointInfo.getPort()); + } + + logger.info("[handleMcpStreamProtocol] Processing {} protocol with args: {} and baseUrl: {}", protocol, + args, baseUrl.toString()); + + try { + // 获取工具名称 - 从工具定义名称中提取实际的工具名称 + String toolDefinitionName = this.toolDefinition.name(); + if (toolDefinitionName == null || toolDefinitionName.isEmpty()) { + throw new RuntimeException("Tool definition name is not available"); + } + + // 工具定义名称格式为: serverName_tools_toolName + // 需要提取最后的 toolName 部分 + String toolName; + if (toolDefinitionName.contains("_tools_")) { + toolName = toolDefinitionName.substring(toolDefinitionName.lastIndexOf("_tools_") + 7); + } + else { + // 如果没有 _tools_ 分隔符,使用整个名称 + toolName = toolDefinitionName; + } + + if (toolName.isEmpty()) { + throw new RuntimeException("Extracted tool name is empty"); + } + + // 构建传输层 + StringBuilder sseEndpoint = new StringBuilder("/sse"); + if (exportPath != null && !exportPath.isEmpty()) { + sseEndpoint = new StringBuilder(exportPath); + if (mcpServerVO.getPassQueryParams() != null) { + + + if (!sseEndpoint.toString().contains("?")) { + sseEndpoint.append("?"); + } + Iterator> iterator = mcpServerVO.getPassQueryParams().entrySet() + .iterator(); + while (iterator.hasNext()) { + Map.Entry next = iterator.next(); + sseEndpoint.append(next.getKey()).append("=").append(next.getValue()) + .append(iterator.hasNext() ? "&" : ""); + } + } + } + + HttpClientSseClientTransport.Builder transportBuilder = HttpClientSseClientTransport.builder(baseUrl.toString()) + .sseEndpoint(sseEndpoint.toString()); + + // 添加自定义请求头(如果需要) + // 这里可以根据需要添加认证头等 + HttpClientSseClientTransport transport = transportBuilder.build(); + + // 创建MCP同步客户端 + McpSyncClient client = McpClient.sync(transport).build(); + + try { + // 初始化客户端 + InitializeResult initializeResult = client.initialize(); + logger.info("[handleMcpStreamProtocol] MCP Client initialized: {}", initializeResult); + + // 调用工具 + McpSchema.CallToolRequest request = new McpSchema.CallToolRequest(toolName, args); + logger.info("[handleMcpStreamProtocol] CallToolRequest: {}", request); + + CallToolResult result = client.callTool(request); + logger.info("[handleMcpStreamProtocol] tool call result: {}", result); + + // 处理结果 + Object content = result.content(); + if (content instanceof List list && !CollectionUtils.isEmpty(list)) { + Object first = list.get(0); + // 兼容TextContent的text字段 + if (first instanceof TextContent textContent) { + return textContent.text(); + } + else if (first instanceof Map map && map.containsKey("text")) { + return map.get("text").toString(); + } + else { + return first.toString(); + } + } + else { + return content != null ? content.toString() : "No content returned"; + } + } + catch (Exception e) { + throw new RuntimeException(e); + } + finally { + // 清理资源 + try { + if (client != null) { + client.close(); + } + } + catch (Exception e) { + logger.warn("[handleMcpStreamProtocol] Failed to close MCP client", e); + } + } + } + catch (Exception e) { + logger.error("[handleMcpStreamProtocol] MCP call failed:", e); + return "Error: MCP call failed - " + e.getMessage(); + } + } + else { + logger.error("[handleMcpStreamProtocol] serviceRef is null"); + return "Error: service reference is null"; + } + } + + // /** + // * 处理MCP Streamable HTTP协议的工具调用 + // */ + // private String handleMcpStreamableProtocol(Map args, + // McpServerRemoteServiceConfig remoteServerConfig, String protocol) throws + // NacosException { + // McpServiceRef serviceRef = remoteServerConfig.getServiceRef(); + // if (serviceRef != null) { + // McpEndpointInfo mcpEndpointInfo = + // nacosMcpOperationService.selectEndpoint(serviceRef); + // if (mcpEndpointInfo == null) { + // throw new RuntimeException("No available endpoint found for service: " + + // serviceRef.getServiceName()); + // } + // + // logger.info("[handleMcpStreamableProtocol] Tool callback instance: {}", + // JacksonUtils.toJson(mcpEndpointInfo)); + // String exportPath = remoteServerConfig.getExportPath(); + // + // // 构建基础URL + // String baseUrl = "http://" + mcpEndpointInfo.getAddress() + ":" + + // mcpEndpointInfo.getPort(); + // + // // 构建streamable endpoint + // String streamableEndpoint = "/streamable"; + // if (exportPath != null && !exportPath.isEmpty()) { + // streamableEndpoint = exportPath; + // } + // + // logger.info( + // "[handleMcpStreamableProtocol] Processing {} protocol with args: {} and baseUrl: {} + // endpoint: {}", + // protocol, args, baseUrl, streamableEndpoint); + // + // try { + // // 获取工具名称 + // String toolDefinitionName = this.toolDefinition.name(); + // if (toolDefinitionName == null || toolDefinitionName.isEmpty()) { + // throw new RuntimeException("Tool definition name is not available"); + // } + // + // String toolName; + // if (toolDefinitionName.contains("_tools_")) { + // toolName = toolDefinitionName.substring(toolDefinitionName.lastIndexOf("_tools_") + + // 7); + // } + // else { + // toolName = toolDefinitionName; + // } + // + // if (toolName.isEmpty()) { + // throw new RuntimeException("Extracted tool name is empty"); + // } + // + // // HTTP协议版本 + // + // // 创建MCP同步客户端,使用Streamable HTTP传输 + // HttpClientStreamableHttpTransport.Builder transportBuilder = + // HttpClientStreamableHttpTransport + // .builder(baseUrl) + // .endpoint(streamableEndpoint); + // + // HttpClientStreamableHttpTransport transport = transportBuilder.build(); + // McpSyncClient client = McpClient.sync(transport).build(); + // + // try { + // // 初始化客户端 + // InitializeResult initializeResult = client.initialize(); + // logger.info("[handleMcpStreamableProtocol] MCP Client initialized: {}", + // initializeResult); + // + // // 调用工具 + // McpSchema.CallToolRequest request = new McpSchema.CallToolRequest(toolName, args); + // logger.info("[handleMcpStreamableProtocol] CallToolRequest: {}", request); + // + // CallToolResult result = client.callTool(request); + // logger.info("[handleMcpStreamableProtocol] tool call result: {}", result); + // + // // 处理结果 + // Object content = result.content(); + // if (content instanceof List list && !CollectionUtils.isEmpty(list)) { + // Object first = list.get(0); + // if (first instanceof TextContent textContent) { + // return textContent.text(); + // } + // else if (first instanceof Map map && map.containsKey("text")) { + // return map.get("text").toString(); + // } + // else { + // return first.toString(); + // } + // } + // else { + // return content != null ? content.toString() : "No content returned"; + // } + // } + // finally { + // // 清理资源 + // try { + // if (client != null) { + // client.close(); + // } + // } + // catch (Exception e) { + // logger.warn("[handleMcpStreamableProtocol] Failed to close MCP client", e); + // } + // } + // } + // catch (Exception e) { + // logger.error("[handleMcpStreamableProtocol] MCP streamable call failed:", e); + // return "Error: MCP streamable call failed - " + e.getMessage(); + // } + // } + // else { + // logger.error("[handleMcpStreamableProtocol] serviceRef is null"); + // return "Error: service reference is null"; + // } + // } + + private java.time.Duration getTimeoutDuration() { + + return java.time.Duration.ofSeconds(30); // 默认超时时间 + } + +} diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/tools/NacosMcpGatewayToolsInitializer.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/tools/NacosMcpGatewayToolsInitializer.java new file mode 100644 index 0000000000..e30bb4366b --- /dev/null +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/tools/NacosMcpGatewayToolsInitializer.java @@ -0,0 +1,143 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.ai.agent.nacos.tools; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.alibaba.cloud.ai.agent.nacos.vo.McpServersVO; +import com.alibaba.cloud.ai.mcp.gateway.nacos.definition.NacosMcpGatewayToolDefinition; +import com.alibaba.cloud.ai.mcp.gateway.nacos.properties.NacosMcpGatewayProperties; +import com.alibaba.cloud.ai.mcp.nacos.service.NacosMcpOperationService; +import com.alibaba.nacos.api.ai.model.mcp.McpServerDetailInfo; +import com.alibaba.nacos.api.ai.model.mcp.McpServerRemoteServiceConfig; +import com.alibaba.nacos.api.ai.model.mcp.McpTool; +import com.alibaba.nacos.api.ai.model.mcp.McpToolMeta; +import com.alibaba.nacos.api.ai.model.mcp.McpToolSpecification; +import org.apache.commons.collections.CollectionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.ai.tool.ToolCallback; + +public class NacosMcpGatewayToolsInitializer { + + private static final Logger logger = LoggerFactory.getLogger(NacosMcpGatewayToolsInitializer.class); + + private final NacosMcpGatewayProperties nacosMcpGatewayProperties; + + private final NacosMcpOperationService nacosMcpOperationService; + + private List mcpServers; + + public NacosMcpGatewayToolsInitializer(NacosMcpOperationService nacosMcpOperationService, + NacosMcpGatewayProperties nacosMcpGatewayProperties, List mcpServers) { + this.nacosMcpGatewayProperties = nacosMcpGatewayProperties; + this.nacosMcpOperationService = nacosMcpOperationService; + this.mcpServers = mcpServers; + } + + public List initializeTools() { + List serviceNames = nacosMcpGatewayProperties.getServiceNames(); + if (serviceNames == null || serviceNames.isEmpty()) { + logger.warn("No service names configured, no tools will be initialized"); + return new ArrayList<>(); + } + List allTools = new ArrayList<>(); + for (McpServersVO.McpServerVO serverVO : mcpServers) { + String serviceName = serverVO.getMcpServerName(); + try { + McpServerDetailInfo serviceDetail = nacosMcpOperationService.getServerDetail(serviceName); + if (serviceDetail == null) { + logger.warn("No service detail info found for service: {}", serviceName); + continue; + } + String protocol = serviceDetail.getProtocol(); + if ("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol) + || "mcp-sse".equalsIgnoreCase(protocol) || "mcp-streamable".equalsIgnoreCase(protocol)) { + List tools = parseToolsFromMcpServerDetailInfo(serviceDetail, serverVO); + if (CollectionUtils.isEmpty(tools)) { + logger.warn("No tools defined for service: {}", serviceName); + continue; + } + allTools.addAll(tools); + } + else { + logger.error("protocol {} is not supported yet. Check your configuration for valid tool protocols", + protocol); + } + + } + catch (Exception e) { + logger.error("Failed to initialize tools for service: {}", serviceName, e); + } + } + if (logger.isDebugEnabled()) { + logger.debug("Initial dynamic tools loading completed - Found {} tools", allTools.size()); + } + return allTools; + } + + private List parseToolsFromMcpServerDetailInfo(McpServerDetailInfo mcpServerDetailInfo, McpServersVO.McpServerVO serverVO) { + try { + McpToolSpecification toolSpecification = mcpServerDetailInfo.getToolSpec(); + String protocol = mcpServerDetailInfo.getProtocol(); + McpServerRemoteServiceConfig mcpServerRemoteServiceConfig = mcpServerDetailInfo.getRemoteServerConfig(); + List toolCallbacks = new ArrayList<>(); + if (toolSpecification != null) { + List toolsList = toolSpecification.getTools(); + Map toolsMeta = toolSpecification.getToolsMeta(); + if (toolsList == null || toolsMeta == null) { + return new ArrayList<>(); + } + for (McpTool tool : toolsList) { + + if (!CollectionUtils.isEmpty(serverVO.getWhiteTools()) && !serverVO.getWhiteTools() + .contains(tool.getName())) { + continue; + } + String toolName = tool.getName(); + String toolDescription = tool.getDescription(); + Map inputSchema = tool.getInputSchema(); + McpToolMeta metaInfo = toolsMeta.get(toolName); + boolean enabled = metaInfo == null || metaInfo.isEnabled(); + if (!enabled) { + logger.info("Tool {} is disabled by metaInfo, skipping.", toolName); + continue; + } + NacosMcpGatewayToolDefinition toolDefinition = NacosMcpGatewayToolDefinition.builder() + .name(mcpServerDetailInfo.getName() + "_tools_" + toolName) + .description(toolDescription) + .inputSchema(inputSchema) + .protocol(protocol) + .remoteServerConfig(mcpServerRemoteServiceConfig) + .toolsMeta(metaInfo) + .build(); + toolCallbacks.add(new NacosMcpGatewayToolCallback(toolDefinition, nacosMcpOperationService, serverVO)); + } + } + return toolCallbacks; + } + catch (Exception e) { + logger.warn("Failed to get or parse nacos mcp service tools info (mcpName {})", + mcpServerDetailInfo.getName() + mcpServerDetailInfo.getVersionDetail().getVersion(), e); + } + return null; + } + +} diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/AgentVO.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/AgentVO.java new file mode 100644 index 0000000000..41d68dc79e --- /dev/null +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/AgentVO.java @@ -0,0 +1,11 @@ +package com.alibaba.cloud.ai.agent.nacos.vo; + +import lombok.Data; + +@Data +public class AgentVO { + + String promptKey; + + String description; +} diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/McpServersVO.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/McpServersVO.java new file mode 100644 index 0000000000..e067b544a9 --- /dev/null +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/McpServersVO.java @@ -0,0 +1,31 @@ +package com.alibaba.cloud.ai.agent.nacos.vo; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import lombok.Data; + +@Data +public class McpServersVO { + + List mcpServers; + + @Data + public static class McpServerVO { + + String mcpServerName; + + String version; + + Set whiteTools; + + Map passHeaders; + + Map passQueryParams; + + } + +} + + diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/ModelVO.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/ModelVO.java new file mode 100644 index 0000000000..23fadc1ae3 --- /dev/null +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/ModelVO.java @@ -0,0 +1,16 @@ +package com.alibaba.cloud.ai.agent.nacos.vo; + +import lombok.Data; + +@Data +public class ModelVO { + private String baseUrl; + + private String apiKey; + + private String model; + + private String temperature; + + private String maxTokens; +} diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/PromptVO.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/PromptVO.java new file mode 100644 index 0000000000..7963b37950 --- /dev/null +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/PromptVO.java @@ -0,0 +1,18 @@ +package com.alibaba.cloud.ai.agent.nacos.vo; + +import java.util.List; + +import lombok.Data; + +@Data +public class PromptVO { + + String promptKey; + + String version; + + String template; + + List variables; + +} diff --git a/spring-ai-alibaba-bom/pom.xml b/spring-ai-alibaba-bom/pom.xml index dc7af32592..c2d534d270 100644 --- a/spring-ai-alibaba-bom/pom.xml +++ b/spring-ai-alibaba-bom/pom.xml @@ -730,6 +730,9 @@ io.spring.javaformat spring-javaformat-maven-plugin ${spring-javaformat-maven-plugin.version} + + true + validate diff --git a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/Builder.java b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/Builder.java new file mode 100644 index 0000000000..fc53004fe1 --- /dev/null +++ b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/Builder.java @@ -0,0 +1,164 @@ +package com.alibaba.cloud.ai.graph.agent; + +import java.util.List; +import java.util.function.Function; + +import com.alibaba.cloud.ai.graph.CompileConfig; +import com.alibaba.cloud.ai.graph.KeyStrategyFactory; +import com.alibaba.cloud.ai.graph.OverAllState; +import com.alibaba.cloud.ai.graph.action.NodeAction; +import com.alibaba.cloud.ai.graph.exception.GraphStateException; +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.client.observation.ChatClientObservationConvention; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.resolution.ToolCallbackResolver; + +public abstract class Builder { + + protected String name; + + protected String description; + + protected String instruction; + + protected String outputKey; + + protected ChatModel model; + + protected ChatOptions chatOptions; + + protected ChatClient chatClient; + + protected List tools; + + protected ToolCallbackResolver resolver; + + protected int maxIterations = 10; + + protected CompileConfig compileConfig; + + protected KeyStrategyFactory keyStrategyFactory; + + protected Function shouldContinueFunc; + + protected NodeAction preLlmHook; + + protected NodeAction postLlmHook; + + protected NodeAction preToolHook; + + protected NodeAction postToolHook; + + protected String inputKey = "messages"; + + protected ObservationRegistry observationRegistry; + + protected ChatClientObservationConvention customObservationConvention; + + public Builder observationRegistry(ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + return this; + } + + public Builder customObservationConvention(ChatClientObservationConvention customObservationConvention) { + this.customObservationConvention = customObservationConvention; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder chatClient(ChatClient chatClient) { + this.chatClient = chatClient; + return this; + } + + public Builder model(ChatModel model) { + this.model = model; + return this; + } + + public Builder chatOptions(ChatOptions chatOptions) { + this.chatOptions = chatOptions; + return this; + } + + public Builder tools(List tools) { + this.tools = tools; + return this; + } + + public Builder resolver(ToolCallbackResolver resolver) { + this.resolver = resolver; + return this; + } + + public Builder maxIterations(int maxIterations) { + this.maxIterations = maxIterations; + return this; + } + + public Builder state(KeyStrategyFactory keyStrategyFactory) { + this.keyStrategyFactory = keyStrategyFactory; + return this; + } + + public Builder compileConfig(CompileConfig compileConfig) { + this.compileConfig = compileConfig; + return this; + } + + public Builder shouldContinueFunction(Function shouldContinueFunc) { + this.shouldContinueFunc = shouldContinueFunc; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder instruction(String instruction) { + this.instruction = instruction; + return this; + } + + public Builder outputKey(String outputKey) { + this.outputKey = outputKey; + return this; + } + + public Builder preLlmHook(NodeAction preLlmHook) { + this.preLlmHook = preLlmHook; + return this; + } + + public Builder postLlmHook(NodeAction postLlmHook) { + this.postLlmHook = postLlmHook; + return this; + } + + public Builder preToolHook(NodeAction preToolHook) { + this.preToolHook = preToolHook; + return this; + } + + public Builder postToolHook(NodeAction postToolHook) { + this.postToolHook = postToolHook; + return this; + } + + public Builder inputKey(String inputKey) { + this.inputKey = inputKey; + return this; + } + + public abstract ReactAgent build() throws GraphStateException; + +} \ No newline at end of file diff --git a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/DefaultBuilder.java b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/DefaultBuilder.java new file mode 100644 index 0000000000..e00d66dc6c --- /dev/null +++ b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/DefaultBuilder.java @@ -0,0 +1,55 @@ +package com.alibaba.cloud.ai.graph.agent; + +import com.alibaba.cloud.ai.graph.exception.GraphStateException; +import com.alibaba.cloud.ai.graph.node.LlmNode; +import com.alibaba.cloud.ai.graph.node.ToolNode; +import org.apache.commons.collections4.CollectionUtils; + +import org.springframework.ai.chat.client.ChatClient; + +public class DefaultBuilder extends Builder { + + @Override + public ReactAgent build() throws GraphStateException { + if (chatClient == null) { + if (model == null) { + throw new IllegalArgumentException("Either chatClient or model must be provided"); + } + ChatClient.Builder clientBuilder = ChatClient.builder(model); + if (chatOptions != null) { + clientBuilder.defaultOptions(chatOptions); + } + if (instruction != null) { + clientBuilder.defaultSystem(instruction); + } + chatClient = clientBuilder.build(); + } + + LlmNode.Builder llmNodeBuilder = LlmNode.builder() + .stream(true) + .chatClient(chatClient) + .messagesKey(this.inputKey); + if (outputKey != null && !outputKey.isEmpty()) { + llmNodeBuilder.outputKey(outputKey); + } + if (CollectionUtils.isNotEmpty(tools)) { + llmNodeBuilder.toolCallbacks(tools); + } + LlmNode llmNode = llmNodeBuilder.build(); + + ToolNode toolNode = null; + if (resolver != null) { + toolNode = ToolNode.builder().toolCallbackResolver(resolver).build(); + } + else if (tools != null) { + toolNode = ToolNode.builder().toolCallbacks(tools).build(); + } + else { + toolNode = ToolNode.builder().build(); + } + + return new ReactAgent(llmNode, toolNode, this); + } + +} + diff --git a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/ReactAgent.java b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/ReactAgent.java index b350022d77..d32552bb2c 100644 --- a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/ReactAgent.java +++ b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/ReactAgent.java @@ -15,17 +15,25 @@ */ package com.alibaba.cloud.ai.graph.agent; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + import com.alibaba.cloud.ai.graph.CompileConfig; import com.alibaba.cloud.ai.graph.CompiledGraph; -import com.alibaba.cloud.ai.graph.GraphResponse; import com.alibaba.cloud.ai.graph.KeyStrategy; import com.alibaba.cloud.ai.graph.KeyStrategyFactory; import com.alibaba.cloud.ai.graph.NodeOutput; import com.alibaba.cloud.ai.graph.OverAllState; -import com.alibaba.cloud.ai.graph.RunnableConfig; import com.alibaba.cloud.ai.graph.StateGraph; import com.alibaba.cloud.ai.graph.action.AsyncNodeAction; import com.alibaba.cloud.ai.graph.action.NodeAction; +import com.alibaba.cloud.ai.graph.agent.factory.AgentBuilderFactory; +import com.alibaba.cloud.ai.graph.agent.factory.DefaultAgentBuilderFactory; +import com.alibaba.cloud.ai.graph.async.AsyncGenerator; +import com.alibaba.cloud.ai.graph.exception.GraphRunnerException; import com.alibaba.cloud.ai.graph.exception.GraphStateException; import com.alibaba.cloud.ai.graph.node.LlmNode; import com.alibaba.cloud.ai.graph.node.ToolNode; @@ -34,22 +42,9 @@ import com.alibaba.cloud.ai.graph.state.strategy.AppendStrategy; import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy; -import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatModel; -import org.springframework.ai.chat.prompt.ChatOptions; -import org.springframework.ai.tool.ToolCallback; -import org.springframework.ai.tool.resolution.ToolCallbackResolver; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Function; - -import org.apache.commons.collections4.CollectionUtils; -import reactor.core.publisher.Flux; import static com.alibaba.cloud.ai.graph.StateGraph.END; import static com.alibaba.cloud.ai.graph.StateGraph.START; @@ -62,6 +57,8 @@ public class ReactAgent extends BaseAgent { private final ToolNode toolNode; + private final StateGraph graph; + private CompiledGraph compiledGraph; private NodeAction preLlmHook; @@ -88,9 +85,11 @@ public class ReactAgent extends BaseAgent { private String inputKey; - protected ReactAgent(LlmNode llmNode, ToolNode toolNode, Builder builder) throws GraphStateException { - super(builder.name, builder.description, builder.outputKey); + public ReactAgent(LlmNode llmNode, ToolNode toolNode, Builder builder) throws GraphStateException { + this.name = builder.name; + this.description = builder.description; this.instruction = builder.instruction; + this.outputKey = builder.outputKey; this.llmNode = llmNode; this.toolNode = toolNode; this.keyStrategyFactory = builder.keyStrategyFactory; @@ -101,10 +100,25 @@ protected ReactAgent(LlmNode llmNode, ToolNode toolNode, Builder builder) throws this.preToolHook = builder.preToolHook; this.postToolHook = builder.postToolHook; this.inputKey = builder.inputKey; + + // 初始化graph + this.graph = initGraph(); } - public static Builder builder() { - return new Builder(); + public Optional invoke(Map input) throws GraphStateException, GraphRunnerException { + if (this.compiledGraph == null) { + this.compiledGraph = getAndCompileGraph(); + } + return this.compiledGraph.invoke(input); + } + + @Override + public AsyncGenerator stream(Map input) + throws GraphStateException, GraphRunnerException { + if (this.compiledGraph == null) { + this.compiledGraph = getAndCompileGraph(); + } + return this.compiledGraph.stream(input); } @Override @@ -121,6 +135,27 @@ public CompiledGraph getCompiledGraph() throws GraphStateException { return compiledGraph; } + public CompiledGraph getAndCompileGraph(CompileConfig compileConfig) throws GraphStateException { + if (this.compileConfig == null) { + this.compiledGraph = getStateGraph().compile(); + } + else { + this.compiledGraph = getStateGraph().compile(compileConfig); + } + this.compiledGraph = getStateGraph().compile(compileConfig); + return this.compiledGraph; + } + + public CompiledGraph getAndCompileGraph() throws GraphStateException { + if (this.compileConfig == null) { + this.compiledGraph = getStateGraph().compile(); + } + else { + this.compiledGraph = getStateGraph().compile(this.compileConfig); + } + return this.compiledGraph; + } + public NodeAction asNodeAction(String inputKeyFromParent, String outputKeyToParent) throws GraphStateException { if (this.compiledGraph == null) { this.compiledGraph = getAndCompileGraph(); @@ -136,8 +171,7 @@ public AsyncNodeAction asAsyncNodeAction(String inputKeyFromParent, String outpu return node_async(new SubGraphStreamingNodeAdapter(inputKeyFromParent, outputKeyToParent, this.compiledGraph)); } - @Override - protected StateGraph initGraph() throws GraphStateException { + private StateGraph initGraph() throws GraphStateException { if (keyStrategyFactory == null) { this.keyStrategyFactory = () -> { HashMap keyStrategyHashMap = new HashMap<>(); @@ -190,8 +224,8 @@ protected StateGraph initGraph() throws GraphStateException { if (postLlmHook != null) { graph.addEdge("llm", "postLlm") - .addConditionalEdges("postLlm", edge_async(this::think), - Map.of("continue", preToolHook != null ? "preTool" : "tool", "end", END)); + .addConditionalEdges("postLlm", edge_async(this::think), + Map.of("continue", preToolHook != null ? "preTool" : "tool", "end", END)); } else { graph.addConditionalEdges("llm", edge_async(this::think), @@ -298,177 +332,12 @@ void setShouldContinueFunc(Function shouldContinueFunc) { this.shouldContinueFunc = shouldContinueFunc; } - public static class Builder { - - private String name; - - private String description; - - private String instruction; - - private String outputKey; - - private ChatModel model; - - private ChatOptions chatOptions; - - private ChatClient chatClient; - - private List tools; - - private ToolCallbackResolver resolver; - - private int maxIterations = 10; - - private CompileConfig compileConfig; - - private KeyStrategyFactory keyStrategyFactory; - - private Function shouldContinueFunc; - - private NodeAction preLlmHook; - - private NodeAction postLlmHook; - - private NodeAction preToolHook; - - private NodeAction postToolHook; - - private String inputKey = "messages"; - - public Builder name(String name) { - this.name = name; - return this; - } - - public Builder chatClient(ChatClient chatClient) { - this.chatClient = chatClient; - return this; - } - - public Builder model(ChatModel model) { - this.model = model; - return this; - } - - public Builder chatOptions(ChatOptions chatOptions) { - this.chatOptions = chatOptions; - return this; - } - - public Builder tools(List tools) { - this.tools = tools; - return this; - } - - public Builder resolver(ToolCallbackResolver resolver) { - this.resolver = resolver; - return this; - } - - public Builder maxIterations(int maxIterations) { - this.maxIterations = maxIterations; - return this; - } - - public Builder state(KeyStrategyFactory keyStrategyFactory) { - this.keyStrategyFactory = keyStrategyFactory; - return this; - } - - public Builder compileConfig(CompileConfig compileConfig) { - this.compileConfig = compileConfig; - return this; - } - - public Builder shouldContinueFunction(Function shouldContinueFunc) { - this.shouldContinueFunc = shouldContinueFunc; - return this; - } - - public Builder description(String description) { - this.description = description; - return this; - } - - public Builder instruction(String instruction) { - this.instruction = instruction; - return this; - } - - public Builder outputKey(String outputKey) { - this.outputKey = outputKey; - return this; - } - - public Builder preLlmHook(NodeAction preLlmHook) { - this.preLlmHook = preLlmHook; - return this; - } - - public Builder postLlmHook(NodeAction postLlmHook) { - this.postLlmHook = postLlmHook; - return this; - } - - public Builder preToolHook(NodeAction preToolHook) { - this.preToolHook = preToolHook; - return this; - } - - public Builder postToolHook(NodeAction postToolHook) { - this.postToolHook = postToolHook; - return this; - } - - public Builder inputKey(String inputKey) { - this.inputKey = inputKey; - return this; - } - - public ReactAgent build() throws GraphStateException { - if (chatClient == null) { - if (model == null) { - throw new IllegalArgumentException("Either chatClient or model must be provided"); - } - ChatClient.Builder clientBuilder = ChatClient.builder(model); - if (chatOptions != null) { - clientBuilder.defaultOptions(chatOptions); - } - if (instruction != null) { - clientBuilder.defaultSystem(instruction); - } - chatClient = clientBuilder.build(); - } - - LlmNode.Builder llmNodeBuilder = LlmNode.builder() - .stream(true) - .chatClient(chatClient) - .messagesKey(this.inputKey); - // For graph built from ReactAgent, the only legal key used inside must be - // messages. - // if (outputKey != null && !outputKey.isEmpty()) { - // llmNodeBuilder.outputKey(outputKey); - // } - if (CollectionUtils.isNotEmpty(tools)) { - llmNodeBuilder.toolCallbacks(tools); - } - LlmNode llmNode = llmNodeBuilder.build(); - - ToolNode toolNode = null; - if (resolver != null) { - toolNode = ToolNode.builder().toolCallbackResolver(resolver).build(); - } - else if (tools != null) { - toolNode = ToolNode.builder().toolCallbacks(tools).build(); - } - else { - toolNode = ToolNode.builder().build(); - } - - return new ReactAgent(llmNode, toolNode, this); - } + public static Builder builder() { + return new DefaultAgentBuilderFactory().builder(); + } + public static Builder builder(AgentBuilderFactory agentBuilderFactory) { + return agentBuilderFactory.builder(); } public static class SubGraphNodeAdapter implements NodeAction { @@ -494,7 +363,7 @@ public Map apply(OverAllState parentState) throws Exception { List messages = List.of(message); // invoke child graph - OverAllState childState = childGraph.call(Map.of("messages", messages)).get(); + OverAllState childState = childGraph.invoke(Map.of("messages", messages)).get(); // extract output from child graph List reactMessages = (List) childState.value("messages").orElseThrow(); @@ -528,10 +397,52 @@ public Map apply(OverAllState parentState) throws Exception { Message message = new UserMessage(input); List messages = List.of(message); - Flux> subGraphFlux = childGraph.fluxDataStream(Map.of("messages", messages), - RunnableConfig.builder().build()); + AsyncGenerator child = childGraph.stream(Map.of("messages", messages)); + + AsyncGenerator wrapped = new AsyncGenerator() { + private volatile Map lastStateData; + + @Override + public Data next() { + Data data = child.next(); + if (data.isDone()) { + String result = extractAssistantText(lastStateData); + return Data.done(Map.of(outputKeyToParent, result)); + } + if (data.isError()) { + return data; + } + return Data.of(data.getData().thenApply(n -> { + try { + lastStateData = n.state().data(); + } + catch (Exception ignored) { + } + return n; + })); + } + }; - return Map.of(outputKeyToParent, subGraphFlux); + return Map.of(outputKeyToParent, wrapped); + } + + private String extractAssistantText(Map stateData) { + if (stateData == null) { + return ""; + } + Object msgs = stateData.get("messages"); + if (!(msgs instanceof List)) { + return ""; + } + List list = (List) msgs; + if (list.isEmpty()) { + return ""; + } + Object last = list.get(list.size() - 1); + if (last instanceof AssistantMessage assistant) { + return assistant.getText(); + } + return ""; } } diff --git a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/factory/AgentBuilderFactory.java b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/factory/AgentBuilderFactory.java new file mode 100644 index 0000000000..aeeb8b217b --- /dev/null +++ b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/factory/AgentBuilderFactory.java @@ -0,0 +1,8 @@ +package com.alibaba.cloud.ai.graph.agent.factory; + +import com.alibaba.cloud.ai.graph.agent.Builder; + +public interface AgentBuilderFactory { + + Builder builder(); +} diff --git a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/factory/DefaultAgentBuilderFactory.java b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/factory/DefaultAgentBuilderFactory.java new file mode 100644 index 0000000000..944a2a5464 --- /dev/null +++ b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/factory/DefaultAgentBuilderFactory.java @@ -0,0 +1,12 @@ +package com.alibaba.cloud.ai.graph.agent.factory; + +import com.alibaba.cloud.ai.graph.agent.Builder; +import com.alibaba.cloud.ai.graph.agent.DefaultBuilder; + +public class DefaultAgentBuilderFactory implements AgentBuilderFactory { + + @Override + public Builder builder() { + return new DefaultBuilder(); + } +} diff --git a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/node/LlmNode.java b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/node/LlmNode.java index 38ef8cafbb..098fcdf5f7 100644 --- a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/node/LlmNode.java +++ b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/node/LlmNode.java @@ -15,8 +15,18 @@ */ package com.alibaba.cloud.ai.graph.node; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + import com.alibaba.cloud.ai.graph.OverAllState; import com.alibaba.cloud.ai.graph.action.NodeAction; +import com.alibaba.cloud.ai.graph.streaming.StreamingChatGenerator; +import reactor.core.publisher.Flux; + import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.api.Advisor; @@ -26,7 +36,6 @@ import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.PromptTemplate; -import org.springframework.ai.model.tool.ToolCallingChatOptions; import org.springframework.ai.tool.ToolCallback; import org.springframework.util.StringUtils; @@ -158,6 +167,10 @@ private void initNodeWithState(OverAllState state) { } } + public void setToolCallbacks(List toolCallbacks) { + this.toolCallbacks = toolCallbacks; + } + private String renderPromptTemplate(String prompt, Map params) { PromptTemplate promptTemplate = new PromptTemplate(prompt); return promptTemplate.render(params); @@ -173,12 +186,9 @@ public ChatResponse call() { private ChatClient.ChatClientRequestSpec buildChatClientRequestSpec() { ChatClient.ChatClientRequestSpec chatClientRequestSpec = chatClient.prompt() - .options(ToolCallingChatOptions.builder() .toolCallbacks(toolCallbacks) - .internalToolExecutionEnabled(false) - .build()) - .messages(messages) - .advisors(advisors); + .messages(messages) + .advisors(advisors); if (StringUtils.hasLength(systemPrompt)) { if (!params.isEmpty()) { diff --git a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/node/ToolNode.java b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/node/ToolNode.java index 1405f7d9bf..b73c2a2941 100644 --- a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/node/ToolNode.java +++ b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/node/ToolNode.java @@ -52,7 +52,7 @@ public ToolNode(List toolCallbacks, ToolCallbackResolver resolver) this.toolCallbackResolver = resolver; } - void setToolCallbacks(List toolCallbacks) { + public void setToolCallbacks(List toolCallbacks) { this.toolCallbacks = toolCallbacks; } From a5fe296992e5ce968a4d3580954d215ab6d4db62 Mon Sep 17 00:00:00 2001 From: shiyiyue1102 Date: Wed, 17 Sep 2025 18:07:48 +0800 Subject: [PATCH 2/6] add description,partener agents,memory vo --- .../ai/agent/nacos/NacosAgentInjector.java | 11 +++-- .../ai/agent/nacos/NacosMcpToolsInjector.java | 8 ++-- .../ai/agent/nacos/NacosModelInjector.java | 35 ++++++++------- .../nacos/NacosPartenerAgentsInjector.java | 45 +++++++++++++++++++ .../ai/agent/nacos/NacosPromptInjector.java | 4 +- .../agent/nacos/NacosReactAgentBuilder.java | 21 ++++++--- .../cloud/ai/agent/nacos/vo/AgentVO.java | 2 + .../cloud/ai/agent/nacos/vo/MemoryVO.java | 18 ++++++++ .../ai/agent/nacos/vo/PartnerAgentsVO.java | 23 ++++++++++ 9 files changed, 131 insertions(+), 36 deletions(-) create mode 100644 spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosPartenerAgentsInjector.java create mode 100644 spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/MemoryVO.java create mode 100644 spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/PartnerAgentsVO.java diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosAgentInjector.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosAgentInjector.java index f59c1e0e7f..2dccc8e09e 100644 --- a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosAgentInjector.java +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosAgentInjector.java @@ -32,12 +32,12 @@ public static void injectPrompt(NacosConfigService nacosConfigService, ChatClien * load prompt by agent id. * * @param nacosConfigService - * @param agentId + * @param agentName * @return */ - public static AgentVO loadAgentVO(NacosConfigService nacosConfigService, String agentId) { + public static AgentVO loadAgentVO(NacosConfigService nacosConfigService, String agentName) { try { - String config = nacosConfigService.getConfig(String.format("agent-%s.json", agentId), "nacos-ai-agent", + String config = nacosConfigService.getConfig("agent-base.json", "ai-agent-" + agentName, 3000L); return JSON.parseObject(config, AgentVO.class); } @@ -46,10 +46,9 @@ public static AgentVO loadAgentVO(NacosConfigService nacosConfigService, String } } - public static void injectPromptByAgentId(NacosConfigService nacosConfigService, ChatClient chatClient, String agentId) { + public static void injectPromptByAgentName(NacosConfigService nacosConfigService, ChatClient chatClient,String agentName,AgentVO agentVO) { try { - AgentVO agentVO = loadAgentVO(nacosConfigService, agentId); if (agentVO == null) { return; } @@ -57,7 +56,7 @@ public static void injectPromptByAgentId(NacosConfigService nacosConfigService, if (promptVO != null) { NacosPromptInjector.replacePrompt(chatClient, promptVO); } - NacosPromptInjector.registryPromptByAgentId(chatClient, nacosConfigService, agentId, promptVO); + NacosPromptInjector.registryPromptByAgentId(chatClient, nacosConfigService, agentName, promptVO); } catch (Exception e) { diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosMcpToolsInjector.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosMcpToolsInjector.java index bda3b1f144..23d9b4f663 100644 --- a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosMcpToolsInjector.java +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosMcpToolsInjector.java @@ -27,11 +27,11 @@ public static List loadMcpTools(NacosOptions nacosOptions, String return null; } - public static void registry(LlmNode llmNode, ToolNode toolNode, NacosOptions nacosOptions, String agentId) { + public static void registry(LlmNode llmNode, ToolNode toolNode, NacosOptions nacosOptions, String agentName) { try { nacosOptions.getNacosConfigService() - .addListener(String.format("mcp-servers-%s.json", agentId), "nacos-ai-agent", new AbstractListener() { + .addListener("mcp-servers.json", "ai-agent-" + agentName, new AbstractListener() { @Override public void receiveConfigInfo(String configInfo) { McpServersVO mcpServersVO = JSON.parseObject(configInfo, McpServersVO.class); @@ -50,10 +50,10 @@ public void receiveConfigInfo(String configInfo) { } - public static McpServersVO getMcpServersVO(NacosOptions nacosOptions, String agentId) { + public static McpServersVO getMcpServersVO(NacosOptions nacosOptions, String agentName) { try { String config = nacosOptions.getNacosConfigService() - .getConfig(String.format("mcp-servers-%s.json", agentId), "nacos-ai-agent", 3000L); + .getConfig("mcp-servers.json", "ai-agent-" + agentName, 3000L); return JSON.parseObject(config, McpServersVO.class); } catch (NacosException e) { diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosModelInjector.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosModelInjector.java index 11676d94d9..340f81a16f 100644 --- a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosModelInjector.java +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosModelInjector.java @@ -20,8 +20,8 @@ public class NacosModelInjector { public static ModelVO getModelByAgentId(NacosOptions nacosOptions, String agentId) { try { - String dataIdT = String.format(nacosOptions.isModelConfigEncrypted() ? "cipher-kms-aes-256-model-%s.json" : "model-%s.json", agentId); - String config = nacosOptions.getNacosConfigService().getConfig(dataIdT, "nacos-ai-agent", 3000L); + String dataIdT = String.format(nacosOptions.isModelConfigEncrypted() ? "cipher-kms-aes-256-model-%s.json" : "model.json"); + String config = nacosOptions.getNacosConfigService().getConfig(dataIdT, "ai-agent-" + agentId, 3000L); return JSON.parseObject(config, ModelVO.class); } catch (NacosException e) { @@ -74,21 +74,22 @@ public static void registerModelListener(ChatClient chatClient, NacosOptions nac return; } try { - String dataIdT = String.format(nacosOptions.isModelConfigEncrypted() ? "cipher-kms-aes-256-model-%s.json" : "model-%s.json", agentId); - - nacosOptions.getNacosConfigService().addListener(dataIdT, "nacos-ai-agent", new AbstractListener() { - @Override - public void receiveConfigInfo(String configInfo) { - ModelVO modelVO = JSON.parseObject(configInfo, ModelVO.class); - try { - ChatModel chatModelNew = initModel(nacosOptions, modelVO); - replaceModel(chatClient, chatModelNew); - } - catch (Exception e) { - throw new RuntimeException(e); - } - } - }); + String dataIdT = String.format(nacosOptions.isModelConfigEncrypted() ? "cipher-kms-aes-256-model.json" : "model.json", agentId); + + nacosOptions.getNacosConfigService() + .addListener(dataIdT, "ai-agent-" + agentId, new AbstractListener() { + @Override + public void receiveConfigInfo(String configInfo) { + ModelVO modelVO = JSON.parseObject(configInfo, ModelVO.class); + try { + ChatModel chatModelNew = initModel(nacosOptions, modelVO); + replaceModel(chatClient, chatModelNew); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + }); } catch (NacosException e) { throw new RuntimeException(e); diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosPartenerAgentsInjector.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosPartenerAgentsInjector.java new file mode 100644 index 0000000000..c449bf9ec6 --- /dev/null +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosPartenerAgentsInjector.java @@ -0,0 +1,45 @@ +package com.alibaba.cloud.ai.agent.nacos; + +import com.alibaba.cloud.ai.agent.nacos.vo.PartnerAgentsVO; +import com.alibaba.cloud.ai.graph.node.LlmNode; +import com.alibaba.cloud.ai.graph.node.ToolNode; +import com.alibaba.fastjson.JSON; +import com.alibaba.nacos.api.config.listener.AbstractListener; +import com.alibaba.nacos.api.exception.NacosException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class NacosPartenerAgentsInjector { + + private static final Logger logger = LoggerFactory.getLogger(NacosPartenerAgentsInjector.class); + + public static void registry(LlmNode llmNode, ToolNode toolNode, NacosOptions nacosOptions, String agentName) { + + try { + nacosOptions.getNacosConfigService() + .addListener("parterner-agents.json", "ai-agent-" + agentName, new AbstractListener() { + @Override + public void receiveConfigInfo(String configInfo) { + PartnerAgentsVO partnerAgentsVO = JSON.parseObject(configInfo, PartnerAgentsVO.class); + System.out.println(partnerAgentsVO); + } + }); + } + catch (NacosException e) { + throw new RuntimeException(e); + } + + } + + public static PartnerAgentsVO getPartenerVO(NacosOptions nacosOptions, String agentName) { + try { + String config = nacosOptions.getNacosConfigService() + .getConfig("parterner-agents.json", "ai-agent-" + agentName, 3000L); + return JSON.parseObject(config, PartnerAgentsVO.class); + } + catch (NacosException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosPromptInjector.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosPromptInjector.java index 4ad28fb5bd..c693bbf47a 100644 --- a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosPromptInjector.java +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosPromptInjector.java @@ -27,7 +27,7 @@ public class NacosPromptInjector { * load prompt by agent id. * * @param nacosConfigService - * @param agentId + * @param AgentVO * @return */ public static PromptVO loadPromptByAgentId(NacosConfigService nacosConfigService, AgentVO agentVO) { @@ -80,7 +80,7 @@ public static void registryPromptByAgentId(ChatClient chatClient, NacosConfigSer try { // register agent prompt listener - nacosConfigService.addListener(String.format("prompt-%s.json", agentId), "nacos-ai-agent", + nacosConfigService.addListener("agent-base.json", "ai-agent-" + agentId, new AbstractListener() { String currentPromptKey; diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosReactAgentBuilder.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosReactAgentBuilder.java index da7101082c..10d2945940 100644 --- a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosReactAgentBuilder.java +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosReactAgentBuilder.java @@ -2,6 +2,7 @@ import java.util.List; +import com.alibaba.cloud.ai.agent.nacos.vo.AgentVO; import com.alibaba.cloud.ai.graph.agent.Builder; import com.alibaba.cloud.ai.graph.agent.DefaultBuilder; import com.alibaba.cloud.ai.graph.agent.ReactAgent; @@ -55,19 +56,22 @@ public ReactAgent build() throws GraphStateException { clientBuilder.defaultOptions(chatOptions); } if (instruction != null) { clientBuilder.defaultSystem(instruction); - } chatClient = clientBuilder.build(); + } + chatClient = clientBuilder.build(); } if (!nacosOptions.modelSpecified) { NacosAgentInjector.injectModel(nacosOptions, chatClient, this.name); - } if (!nacosOptions.promptSpecified) { + } + + if (!nacosOptions.promptSpecified) { if (nacosOptions.promptKey != null) { NacosAgentInjector.injectPrompt(nacosOptions.getNacosConfigService(), chatClient, nacosOptions.promptKey); - } else { - NacosAgentInjector.injectPromptByAgentId(nacosOptions.getNacosConfigService(), chatClient, nacosOptions.getAgentName()); - + AgentVO agentVO = NacosAgentInjector.loadAgentVO(nacosOptions.getNacosConfigService(), this.name); + this.description = agentVO.getDescription(); + NacosAgentInjector.injectPromptByAgentName(nacosOptions.getNacosConfigService(), chatClient, nacosOptions.getAgentName(), agentVO); } } @@ -84,7 +88,8 @@ public ReactAgent build() throws GraphStateException { llmNodeBuilder.toolCallbacks(tools); } LlmNode llmNode = llmNodeBuilder.build(); - ToolNode toolNode = null; if (resolver != null) { + ToolNode toolNode = null; + if (resolver != null) { toolNode = ToolNode.builder().toolCallbackResolver(resolver).build(); } else if (tools != null) { @@ -92,7 +97,9 @@ else if (tools != null) { } else { toolNode = ToolNode.builder().build(); - } NacosMcpToolsInjector.registry(llmNode, toolNode, nacosOptions, this.name); + } + NacosMcpToolsInjector.registry(llmNode, toolNode, nacosOptions, this.name); + return new ReactAgent(llmNode, toolNode, this); } } diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/AgentVO.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/AgentVO.java index 41d68dc79e..a9de72a3b7 100644 --- a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/AgentVO.java +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/AgentVO.java @@ -8,4 +8,6 @@ public class AgentVO { String promptKey; String description; + + int maxIterations; } diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/MemoryVO.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/MemoryVO.java new file mode 100644 index 0000000000..10389f9b57 --- /dev/null +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/MemoryVO.java @@ -0,0 +1,18 @@ +package com.alibaba.cloud.ai.agent.nacos.vo; + +import lombok.Data; + +@Data +public class MemoryVO { + + String storageType; + + String address; + + String credential; + + String compressionStrategy; + + String searchStrategy; + +} diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/PartnerAgentsVO.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/PartnerAgentsVO.java new file mode 100644 index 0000000000..2b7240c73e --- /dev/null +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/PartnerAgentsVO.java @@ -0,0 +1,23 @@ +package com.alibaba.cloud.ai.agent.nacos.vo; + +import java.util.List; +import java.util.Map; + +import lombok.Data; + +@Data +public class PartnerAgentsVO { + + List agents; + + @Data + public static class PartnerAgentVO { + + String agentName; + + Map headers; + + Map queryPrams; + + } +} From 43d2c3b1a946bc4d2154cb9965315e38258f906e Mon Sep 17 00:00:00 2001 From: shiyiyue1102 Date: Wed, 17 Sep 2025 19:19:43 +0800 Subject: [PATCH 3/6] fix merge error --- .../tools/NacosMcpGatewayToolCallback.java | 663 +++++++----------- .../alibaba/cloud/ai/graph/agent/Builder.java | 29 +- .../cloud/ai/graph/agent/DefaultBuilder.java | 9 +- .../cloud/ai/graph/agent/ReactAgent.java | 109 +-- .../alibaba/cloud/ai/graph/node/LlmNode.java | 2 - 5 files changed, 294 insertions(+), 518 deletions(-) diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/tools/NacosMcpGatewayToolCallback.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/tools/NacosMcpGatewayToolCallback.java index 8dcbae2e1f..db47c69127 100644 --- a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/tools/NacosMcpGatewayToolCallback.java +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/tools/NacosMcpGatewayToolCallback.java @@ -25,18 +25,14 @@ import com.alibaba.cloud.ai.agent.nacos.vo.McpServersVO; import com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayToolDefinition; -import com.alibaba.cloud.ai.mcp.gateway.core.jsontemplate.RequestTemplateInfo; -import com.alibaba.cloud.ai.mcp.gateway.core.jsontemplate.RequestTemplateParser; -import com.alibaba.cloud.ai.mcp.gateway.core.jsontemplate.ResponseTemplateParser; import com.alibaba.cloud.ai.mcp.gateway.nacos.definition.NacosMcpGatewayToolDefinition; import com.alibaba.cloud.ai.mcp.nacos.service.NacosMcpOperationService; import com.alibaba.nacos.api.ai.model.mcp.McpEndpointInfo; import com.alibaba.nacos.api.ai.model.mcp.McpServerRemoteServiceConfig; import com.alibaba.nacos.api.ai.model.mcp.McpServiceRef; -import com.alibaba.nacos.api.ai.model.mcp.McpToolMeta; +import com.alibaba.nacos.api.config.listener.AbstractListener; import com.alibaba.nacos.api.exception.NacosException; import com.alibaba.nacos.common.utils.JacksonUtils; -import com.alibaba.nacos.common.utils.StringUtils; import com.alibaba.nacos.shaded.com.google.common.collect.Maps; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.core.JsonProcessingException; @@ -50,31 +46,30 @@ import io.modelcontextprotocol.spec.McpSchema.CallToolResult; import io.modelcontextprotocol.spec.McpSchema.InitializeResult; import io.modelcontextprotocol.spec.McpSchema.TextContent; +import org.apache.poi.util.StringUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import reactor.core.publisher.Mono; import org.springframework.ai.chat.model.ToolContext; import org.springframework.ai.tool.ToolCallback; import org.springframework.ai.tool.definition.ToolDefinition; -import org.springframework.http.HttpMethod; import org.springframework.lang.NonNull; import org.springframework.util.CollectionUtils; -import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.util.StringUtils; public class NacosMcpGatewayToolCallback implements ToolCallback { - private static final Logger logger = LoggerFactory.getLogger(NacosMcpGatewayToolCallback.class); + private static final Logger logger = LoggerFactory.getLogger(com.alibaba.cloud.ai.mcp.gateway.nacos.callback.NacosMcpGatewayToolCallback.class); - private final NacosMcpGatewayToolDefinition toolDefinition; - - private NacosMcpOperationService nacosMcpOperationService; - - private static final Pattern TEMPLATE_PATTERN = Pattern.compile("\\{\\{\\s*\\.([\\w]*)\\s*\\}\\}"); + private static final Pattern TEMPLATE_PATTERN = Pattern.compile("\\{\\{\\s*(\\.[\\w]+(?:\\.[\\w]+)*)\\s*\\}\\}"); - private final WebClient.Builder webClientBuilder; + // 匹配 {{ ${nacos.dataId/group} }} 或 {{ ${nacos.dataId/group}.key1.key2 }} + private static final Pattern NACOS_TEMPLATE_PATTERN = Pattern + .compile("\\{\\{\\s*\\$\\{nacos\\.([^}]+)\\}(\\.[\\w]+(?:\\.[\\w]+)*)?\\s*}}"); - private McpServersVO.McpServerVO mcpServerVO; + /** + * The Object mapper. + */ static ObjectMapper objectMapper = new ObjectMapper(); static { @@ -82,257 +77,299 @@ public class NacosMcpGatewayToolCallback implements ToolCallback { objectMapper.setSerializationInclusion(Include.NON_NULL); } - public NacosMcpGatewayToolCallback(final McpGatewayToolDefinition toolDefinition, NacosMcpOperationService nacosMcpOperationService, McpServersVO.McpServerVO mcpServerVO) { - this.webClientBuilder = null; + private final NacosMcpGatewayToolDefinition toolDefinition; + + private final NacosMcpOperationService nacosMcpOperationService; + + private final HashMap nacosConfigListeners = new HashMap<>(); + + private final HashMap nacosConfigContent = new HashMap<>(); + + McpServersVO.McpServerVO mcpServerVO; + + /** + * Instantiates a new Nacos mcp gateway tool callback. + * @param toolDefinition the tool definition + */ + public NacosMcpGatewayToolCallback(final McpGatewayToolDefinition toolDefinition, NacosMcpOperationService nacosMcpOperationService, McpServersVO.McpServerVO mcpServersVO) { this.toolDefinition = (NacosMcpGatewayToolDefinition) toolDefinition; this.nacosMcpOperationService = nacosMcpOperationService; - this.mcpServerVO = mcpServerVO; - // 尝试获取配置属性 - // try { - // NacosMcpGatewayProperties properties = SpringBeanUtils.getInstance() - // .getBean(NacosMcpGatewayProperties.class); - // if (properties != null) { - // logger.info("Loaded gateway properties: maxConnections={}, - // connectionTimeout={}, readTimeout={}", - // properties.getMaxConnections(), properties.getConnectionTimeout(), - // properties.getReadTimeout()); - // } - // } - // catch (Exception e) { - // logger.debug("Failed to load gateway properties, using defaults", e); - // } + this.mcpServerVO = mcpServersVO; } /** - * 处理工具请求 + * Process nacos config ref template string. + * @param template the template + * @return the string */ - private Mono processToolRequest(String configJson, Map args, String baseUrl) { - try { - JsonNode toolConfig = objectMapper.readTree(configJson); - logger.info("[processToolRequest] toolConfig: {} args: {} baseUrl: {}", toolConfig, args, baseUrl); + public String processNacosConfigRefTemplate(String template) { + if (!org.springframework.util.StringUtils.hasText(template)) { + return template; + } - // 验证配置完整性 - if (toolConfig == null || toolConfig.isEmpty()) { - return Mono.error(new IllegalArgumentException("Tool configuration is empty or invalid")); - } + StringBuffer result = new StringBuffer(); + Matcher matcher = NACOS_TEMPLATE_PATTERN.matcher(template); - JsonNode argsNode = toolConfig.path("args"); - Map processedArgs; - if (!argsNode.isMissingNode() && argsNode.isArray() && argsNode.size() > 0) { - processedArgs = processArguments(argsNode, args); - logger.info("[processToolRequest] processedArgs from args: {}", processedArgs); - } - else if (!toolConfig.path("inputSchema").isMissingNode() && toolConfig.path("inputSchema").isObject()) { - // 从 inputSchema.properties 解析参数 - JsonNode properties = toolConfig.path("inputSchema").path("properties"); - if (properties.isObject()) { - processedArgs = new HashMap<>(); - properties.fieldNames().forEachRemaining(field -> { - if (args.containsKey(field)) { - processedArgs.put(field, args.get(field)); - } - }); - logger.info("[processToolRequest] processedArgs from inputSchema: {}", processedArgs); - } - else { - processedArgs = args; - logger.info("[processToolRequest] inputSchema.properties missing, use original args: {}", - processedArgs); - } - } - else { - processedArgs = args; - logger.info("[processToolRequest] no args or inputSchema, use original args: {}", processedArgs); - } + while (matcher.find()) { + String nacosRef = matcher.group(1); + String dotNotation = matcher.group(2); + String replacement = resolveNacosReference(nacosRef, dotNotation); + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement != null ? replacement : "")); + } + matcher.appendTail(result); - JsonNode requestTemplate = toolConfig.path("requestTemplate"); - String url = requestTemplate.path("url").asText(); - String method = requestTemplate.path("method").asText(); - logger.info("[processToolRequest] requestTemplate: {} url: {} method: {}", requestTemplate, url, method); + return result.toString(); + } + + /** + * 解析Nacos引用 + * @param nacosRef 引用字符串,格式为 dataId/group + * @param dotNotation 点语法部分,格式为 .key1.key2(可能为null) + * @return 解析后的值 + */ + private String resolveNacosReference(String nacosRef, String dotNotation) { + if (!org.springframework.util.StringUtils.hasText(nacosRef)) { + return null; + } - // 检查URL和方法 - if (url.isEmpty() || method.isEmpty()) { - return Mono.error(new IllegalArgumentException("URL and method are required in requestTemplate")); + try { + // 解析dataId和group + String[] configParts = nacosRef.split("/"); + if (configParts.length != 2) { + throw new IllegalArgumentException( + "Invalid Nacos config reference format: " + nacosRef + ". Expected format: dataId/group"); } - // 验证HTTP方法 - try { - HttpMethod.valueOf(method.toUpperCase()); + String dataId = configParts[0]; + String group = configParts[1]; + + // 获取配置内容 + String configContent = getConfigContent(dataId, group); + if (!org.springframework.util.StringUtils.hasText(configContent)) { + logger.warn("[resolveNacosReference] No content found for dataId: {}, group: {}", dataId, group); + return null; } - catch (IllegalArgumentException e) { - return Mono.error(new IllegalArgumentException("Invalid HTTP method: " + method)); + + // 如果没有点语法,直接返回配置内容 + if (!org.springframework.util.StringUtils.hasText(dotNotation)) { + return configContent; } - // 创建WebClient - baseUrl = baseUrl != null ? baseUrl : "http://localhost"; - WebClient client = webClientBuilder.baseUrl(baseUrl).build(); - - // 构建并执行请求 - return buildAndExecuteRequest(client, requestTemplate, toolConfig.path("responseTemplate"), processedArgs, - baseUrl) - .onErrorResume(e -> { - logger.error("Failed to execute tool request: {}", e.getMessage(), e); - return Mono.error(new RuntimeException("Tool execution failed: " + e.getMessage(), e)); - }); + // 如果有点语法,去掉开头的点号,然后解析JSON并提取指定字段 + String jsonPath = dotNotation.startsWith(".") ? dotNotation.substring(1) : dotNotation; + return extractJsonValueFromNacos(configContent, jsonPath); + } catch (Exception e) { - logger.error("Failed to process tool request", e); - return Mono.error(new RuntimeException("Failed to process tool request: " + e.getMessage(), e)); + // 记录日志但不中断处理 + logger.error("[resolveNacosReference] Failed to resolve Nacos reference: {}", e.getMessage(), e); + throw new RuntimeException("Failed to resolve Nacos reference: " + e.getMessage(), e); } } /** - * 处理参数定义和值 + * 获取Nacos配置内容 + * @param dataId 配置ID + * @param group 分组 + * @return 配置内容 + * @throws NacosException Nacos异常 */ - private Map processArguments(JsonNode argsDefinition, Map providedArgs) { - Map processedArgs = new HashMap<>(); - - if (argsDefinition.isArray()) { - for (JsonNode argDef : argsDefinition) { - String name = argDef.path("name").asText(); - boolean required = argDef.path("required").asBoolean(false); - Object defaultValue = argDef.has("default") - ? objectMapper.convertValue(argDef.path("default"), Object.class) : null; - - // 检查参数 - if (providedArgs.containsKey(name)) { - processedArgs.put(name, providedArgs.get(name)); + private String getConfigContent(String dataId, String group) throws NacosException { + String cacheKey = dataId + "@@" + group; + if (nacosConfigContent.containsKey(cacheKey)) { + return nacosConfigContent.get(cacheKey); + } + else { + AbstractListener listener = new AbstractListener() { + @Override + public void receiveConfigInfo(String configInfo) { + nacosConfigContent.put(cacheKey, configInfo); } - else if (defaultValue != null) { - processedArgs.put(name, defaultValue); + }; + AbstractListener oldListener = nacosConfigListeners.putIfAbsent(cacheKey, listener); + if (oldListener == null) { + try { + nacosMcpOperationService.getConfigService().addListener(dataId, group, listener); } - else if (required) { - throw new IllegalArgumentException("Required argument missing: " + name); + catch (Exception e) { + nacosConfigListeners.remove(cacheKey); + logger.error("Failed to add listener for Nacos config: {}", e.getMessage(), e); } } + return nacosMcpOperationService.getConfigService().getConfig(dataId, group, 3000); } - - return processedArgs; } /** - * 构建并执行WebClient请求 + * 从JSON字符串中提取指定路径的值 + * @param jsonString JSON字符串 + * @param jsonPath JSON路径,如 key1.key2 + * @return 提取的值 */ - private Mono buildAndExecuteRequest(WebClient client, JsonNode requestTemplate, JsonNode responseTemplate, - Map args, String baseUrl) { - - RequestTemplateInfo info = RequestTemplateParser.parseRequestTemplate(requestTemplate); - String url = info.url; - String method = info.method; - HttpMethod httpMethod = HttpMethod.valueOf(method.toUpperCase()); - - // 处理URL中的路径参数 - String processedUrl = processTemplateString(url, args); - logger.info("[buildAndExecuteRequest] original url template: {} processed url: {}", url, processedUrl); - - // 构建请求 - WebClient.RequestBodySpec requestBodySpec = client.method(httpMethod) - .uri(builder -> RequestTemplateParser.buildUri(builder, processedUrl, info, args)); - - // 添加请求头 - RequestTemplateParser.addHeaders(requestBodySpec, info.headers, args, this::processTemplateString); - - // 处理请求体 - WebClient.RequestHeadersSpec headersSpec = RequestTemplateParser.addRequestBody(requestBodySpec, info, args, - this::processTemplateString, objectMapper, logger); - - // 输出最终请求信息 - String fullUrl = baseUrl.endsWith("/") && processedUrl.startsWith("/") ? baseUrl + processedUrl.substring(1) - : baseUrl + processedUrl; - logger.info("[buildAndExecuteRequest] final request: method={} url={} args={}", method, fullUrl, args); - - return headersSpec.retrieve() - .onStatus(status -> status.is4xxClientError(), - response -> Mono.error(new RuntimeException("Client error: " + response.statusCode()))) - .onStatus(status -> status.is5xxServerError(), - response -> Mono.error(new RuntimeException("Server error: " + response.statusCode()))) - .bodyToMono(String.class) - .timeout(getTimeoutDuration()) // 使用配置的超时时间 - .doOnNext(responseBody -> logger.info("[buildAndExecuteRequest] received responseBody: {}", responseBody)) - .map(responseBody -> processResponse(responseBody, responseTemplate, args)) - .onErrorResume(e -> { - logger.error("[buildAndExecuteRequest] Request failed: {}", e.getMessage(), e); - return Mono.error(new RuntimeException("HTTP request failed: " + e.getMessage(), e)); - }); - } + private String extractJsonValueFromNacos(String jsonString, String jsonPath) throws JsonProcessingException { - /** - * 处理响应 - */ - private String processResponse(String responseBody, JsonNode responseTemplate, Map args) { - logger.info("[processResponse] received responseBody: {}", responseBody); - String result = null; - if (!responseTemplate.isEmpty()) { - if (responseTemplate.has("body") && !responseTemplate.path("body").asText().isEmpty()) { - String bodyTemplate = responseTemplate.path("body").asText(); - // 统一交给 ResponseTemplateParser 处理 - result = ResponseTemplateParser.parse(responseBody, bodyTemplate); - logger.info("[processResponse] ResponseTemplateParser result: {}", result); - return result; + try { + JsonNode rootNode = objectMapper.readTree(jsonString); + String[] pathParts = jsonPath.split("\\."); + + JsonNode currentNode = rootNode; + for (String part : pathParts) { + if (currentNode == null || currentNode.isMissingNode()) { + logger.warn("[extractJsonValueFromNacos] Path '{}' not found in JSON", jsonPath); + return null; + } + currentNode = currentNode.get(part); } - else if (responseTemplate.has("prependBody") || responseTemplate.has("appendBody")) { - String prependText = responseTemplate.path("prependBody").asText(""); - String appendText = responseTemplate.path("appendBody").asText(""); - result = processTemplateString(prependText, args) + responseBody - + processTemplateString(appendText, args); - logger.info("[processResponse] prepend/append result: {}", result); - return result; + + if (currentNode == null || currentNode.isMissingNode()) { + logger.warn("[extractJsonValueFromNacos] Final path '{}' not found in JSON", jsonPath); + return null; + } + + // 根据节点类型返回合适的值 + if (currentNode.isTextual()) { + return currentNode.asText(); + } + else if (currentNode.isNumber()) { + return currentNode.asText(); + } + else if (currentNode.isBoolean()) { + return String.valueOf(currentNode.asBoolean()); + } + else { + // 对于复杂对象,返回JSON字符串 + return currentNode.toString(); } } - result = responseBody; - logger.info("[processResponse] default result: {}", result); - return result; + catch (JsonProcessingException e) { + logger.error("[extractJsonValueFromNacos] Failed to parse JSON from Nacos config. Content: {}, Error: {}", + jsonString, e.getMessage()); + throw new RuntimeException( + "Nacos config content is not valid JSON, but dot notation was used. Please ensure the config is in JSON format or remove the dot notation. Content: " + + jsonString, + e); + } + catch (Exception e) { + logger.error("[extractJsonValueFromNacos] Failed to extract JSON value from Nacos config: {}", + e.getMessage(), e); + throw e; + } } - /** - * 处理模板字符串中的变量 - */ - private String processTemplateString(String template, Map data) { - logger.debug("[processTemplateString] template: {} data: {}", template, data); + private String processTemplateString(String template, Map params) { + Map args = (Map) params.get("args"); + String extendedData = (String) params.get("extendedData"); + logger.debug("[processTemplateString] template: {} args: {} extendedData: {}", template, args, extendedData); if (template == null || template.isEmpty()) { return ""; } - Matcher matcher = TEMPLATE_PATTERN.matcher(template); StringBuilder result = new StringBuilder(); while (matcher.find()) { - String variable = matcher.group(1); - String replacement; - if ("".equals(variable) || ".".equals(variable)) { - // 特殊处理{{.}},输出data唯一值或整个data - if (data != null && data.size() == 1) { - replacement = String.valueOf(data.values().iterator().next()); + // 获取完整路径,如 .args.name 或 .data.key1.key2 + String fullPath = matcher.group(1); + String replacement = resolvePathValue(fullPath, args, extendedData); + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } + matcher.appendTail(result); + String finalResult = result.toString(); + finalResult = processNacosConfigRefTemplate(finalResult); + logger.debug("[processTemplateString] final result: {}", finalResult); + + return finalResult; + } + + /** + * 根据路径解析值 + * @param fullPath 完整路径,如 .args.name 或 .data.key1.key2 + * @param args 参数数据映射 + * @param extendedData 扩展数据(JSON字符串) + * @return 解析后的值 + */ + private String resolvePathValue(String fullPath, Map args, String extendedData) { + if (fullPath == null || fullPath.isEmpty()) { + return ""; + } + // 移除开头的点号 + if (fullPath.startsWith(".")) { + fullPath = fullPath.substring(1); + } + + String[] pathParts = fullPath.split("\\."); + if (pathParts.length == 0) { + return ""; + } + + // 确定数据源 + Object dataSource; + if (pathParts[0].equals("args")) { + // 从args中取值 + dataSource = args; + // 如果只有args,没有具体字段名 + if (pathParts.length == 1) { + if (args != null && args.size() == 1) { + return String.valueOf(args.values().iterator().next()); } - else if (data != null && !data.isEmpty()) { - replacement = data.toString(); + else if (args != null && !args.isEmpty()) { + return args.toString(); } else { - replacement = ""; + return ""; } } - else { - Object value = data != null ? data.get(variable) : null; - if (value == null) { - logger.warn("[processTemplateString] Variable '{}' not found in data, using empty string", - variable); - replacement = ""; + } + else { + // 从extendedData中取值 + // 首先将extendedData字符串解析为JSON对象 + try { + if (StringUtils.hasText(extendedData)) { + dataSource = objectMapper.readValue(extendedData, Map.class); } else { - replacement = value.toString(); + dataSource = null; } } - matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + catch (Exception e) { + logger.warn("[resolvePathValue] Failed to parse extendedData as JSON: {}", e.getMessage()); + // 如果解析失败,将extendedData作为普通字符串处理 + if (pathParts.length == 1 && fullPath.equals("extendedData")) { + return extendedData != null ? extendedData : ""; + } + return ""; + } + + // 特殊处理直接访问extendedData的情况 + if (pathParts.length == 1 && fullPath.equals("extendedData")) { + return extendedData != null ? extendedData : ""; + } } - matcher.appendTail(result); - String finalResult = result.toString(); - logger.debug("[processTemplateString] final result: {}", finalResult); - // 验证是否还存在未被替换的{{.}},如有则输出警告 - if (finalResult.contains("{{.}}")) { - logger.warn("[processTemplateString] WARNING: {{.}} was not replaced in result: {}", finalResult); + // 如果数据源为空 + if (dataSource == null) { + return ""; } + // 处理嵌套路径 + Object currentValue = dataSource; + int startIndex = pathParts[0].equals("args") ? 1 : 0; + // 如果是args,从索引1开始;否则从索引0开始 + + for (int i = startIndex; i < pathParts.length; i++) { + String key = pathParts[i]; + if (currentValue instanceof Map) { + Map currentMap = (Map) currentValue; + currentValue = currentMap.get(key); + } + else { + logger.warn("[resolvePathValue] Cannot access key '{}' from non-map value", key); + return ""; + } - return finalResult; + if (currentValue == null) { + logger.warn("[resolvePathValue] Key '{}' not found in nested path", key); + return ""; + } + } + return currentValue.toString(); } @Override @@ -376,16 +413,7 @@ public String call(@NonNull final String input, final ToolContext toolContext) { throw new IllegalStateException("Protocol is null"); } - // 根据协议类型分发到不同的处理方法 - if ("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) { - McpServerRemoteServiceConfig remoteServerConfig = this.toolDefinition.getRemoteServerConfig(); - if (remoteServerConfig == null) { - throw new IllegalStateException("Remote server config is null"); - } - - return handleHttpHttpsProtocol(args, remoteServerConfig, protocol); - } - else if ("mcp-sse".equalsIgnoreCase(protocol)) { + if ("mcp-sse".equalsIgnoreCase(protocol)) { McpServerRemoteServiceConfig remoteServerConfig = this.toolDefinition.getRemoteServerConfig(); if (remoteServerConfig == null) { throw new IllegalStateException("Remote server config is null"); @@ -414,61 +442,6 @@ else if ("mcp-streamable".equalsIgnoreCase(protocol)) { } } - /** - * 处理HTTP/HTTPS协议的工具调用 - */ - private String handleHttpHttpsProtocol(Map args, McpServerRemoteServiceConfig remoteServerConfig, - String protocol) throws NacosException { - McpServiceRef serviceRef = remoteServerConfig.getServiceRef(); - if (serviceRef != null) { - McpEndpointInfo mcpEndpointInfo = nacosMcpOperationService.selectEndpoint(serviceRef); - if (mcpEndpointInfo == null) { - throw new RuntimeException("No available endpoint found for service: " + serviceRef.getServiceName()); - } - - logger.info("Tool callback instance: {}", JacksonUtils.toJson(mcpEndpointInfo)); - McpToolMeta toolMeta = this.toolDefinition.getToolMeta(); - String baseUrl = protocol + "://" + mcpEndpointInfo.getAddress() + ":" + mcpEndpointInfo.getPort(); - - if (toolMeta != null && toolMeta.getTemplates() != null) { - Map templates = toolMeta.getTemplates(); - if (templates != null && templates.containsKey("json-go-template")) { - Object jsonGoTemplate = templates.get("json-go-template"); - try { - logger.info("[handleHttpHttpsProtocol] json-go-template: {}", - objectMapper.writeValueAsString(jsonGoTemplate)); - } - catch (JsonProcessingException e) { - logger.error("[handleHttpHttpsProtocol] Failed to serialize json-go-template", e); - } - try { - // 调用executeToolRequest - String configJson = objectMapper.writeValueAsString(jsonGoTemplate); - logger.info("[handleHttpHttpsProtocol] configJson: {} args: {} baseUrl: {}", configJson, args, - baseUrl); - return processToolRequest(configJson, args, baseUrl).block(); - } - catch (Exception e) { - logger.error("Failed to execute tool request", e); - return "Error: " + e.getMessage(); - } - } - else { - logger.warn("[handleHttpHttpsProtocol] json-go-template not found in templates"); - return "Error: json-go-template not found in tool configuration"; - } - } - else { - logger.warn("[handleHttpHttpsProtocol] templates not found in toolsMeta"); - return "Error: templates not found in tool metadata"; - } - } - else { - logger.error("[handleHttpHttpsProtocol] serviceRef is null"); - return "Error: service reference is null"; - } - } - /** * 处理MCP流式协议的工具调用 (mcp-sse, mcp-streamable) */ @@ -485,7 +458,7 @@ private String handleMcpStreamProtocol(Map args, McpServerRemote String exportPath = remoteServerConfig.getExportPath(); // 构建基础URL,根据协议类型调整 - String transportProtocol = StringUtils.isNotBlank(serviceRef.getTransportProtocol()) ? serviceRef.getTransportProtocol() : "http"; + String transportProtocol = StringUtil.isNotBlank(serviceRef.getTransportProtocol()) ? serviceRef.getTransportProtocol() : "http"; StringBuilder baseUrl; if ("mcp-sse".equalsIgnoreCase(protocol)) { baseUrl = new StringBuilder(transportProtocol + "://" + mcpEndpointInfo.getAddress() + ":" + mcpEndpointInfo.getPort()); @@ -581,9 +554,6 @@ else if (first instanceof Map map && map.containsKey("text")) { return content != null ? content.toString() : "No content returned"; } } - catch (Exception e) { - throw new RuntimeException(e); - } finally { // 清理资源 try { @@ -607,128 +577,17 @@ else if (first instanceof Map map && map.containsKey("text")) { } } - // /** - // * 处理MCP Streamable HTTP协议的工具调用 - // */ - // private String handleMcpStreamableProtocol(Map args, - // McpServerRemoteServiceConfig remoteServerConfig, String protocol) throws - // NacosException { - // McpServiceRef serviceRef = remoteServerConfig.getServiceRef(); - // if (serviceRef != null) { - // McpEndpointInfo mcpEndpointInfo = - // nacosMcpOperationService.selectEndpoint(serviceRef); - // if (mcpEndpointInfo == null) { - // throw new RuntimeException("No available endpoint found for service: " + - // serviceRef.getServiceName()); - // } - // - // logger.info("[handleMcpStreamableProtocol] Tool callback instance: {}", - // JacksonUtils.toJson(mcpEndpointInfo)); - // String exportPath = remoteServerConfig.getExportPath(); - // - // // 构建基础URL - // String baseUrl = "http://" + mcpEndpointInfo.getAddress() + ":" + - // mcpEndpointInfo.getPort(); - // - // // 构建streamable endpoint - // String streamableEndpoint = "/streamable"; - // if (exportPath != null && !exportPath.isEmpty()) { - // streamableEndpoint = exportPath; - // } - // - // logger.info( - // "[handleMcpStreamableProtocol] Processing {} protocol with args: {} and baseUrl: {} - // endpoint: {}", - // protocol, args, baseUrl, streamableEndpoint); - // - // try { - // // 获取工具名称 - // String toolDefinitionName = this.toolDefinition.name(); - // if (toolDefinitionName == null || toolDefinitionName.isEmpty()) { - // throw new RuntimeException("Tool definition name is not available"); - // } - // - // String toolName; - // if (toolDefinitionName.contains("_tools_")) { - // toolName = toolDefinitionName.substring(toolDefinitionName.lastIndexOf("_tools_") + - // 7); - // } - // else { - // toolName = toolDefinitionName; - // } - // - // if (toolName.isEmpty()) { - // throw new RuntimeException("Extracted tool name is empty"); - // } - // - // // HTTP协议版本 - // - // // 创建MCP同步客户端,使用Streamable HTTP传输 - // HttpClientStreamableHttpTransport.Builder transportBuilder = - // HttpClientStreamableHttpTransport - // .builder(baseUrl) - // .endpoint(streamableEndpoint); - // - // HttpClientStreamableHttpTransport transport = transportBuilder.build(); - // McpSyncClient client = McpClient.sync(transport).build(); - // - // try { - // // 初始化客户端 - // InitializeResult initializeResult = client.initialize(); - // logger.info("[handleMcpStreamableProtocol] MCP Client initialized: {}", - // initializeResult); - // - // // 调用工具 - // McpSchema.CallToolRequest request = new McpSchema.CallToolRequest(toolName, args); - // logger.info("[handleMcpStreamableProtocol] CallToolRequest: {}", request); - // - // CallToolResult result = client.callTool(request); - // logger.info("[handleMcpStreamableProtocol] tool call result: {}", result); - // - // // 处理结果 - // Object content = result.content(); - // if (content instanceof List list && !CollectionUtils.isEmpty(list)) { - // Object first = list.get(0); - // if (first instanceof TextContent textContent) { - // return textContent.text(); - // } - // else if (first instanceof Map map && map.containsKey("text")) { - // return map.get("text").toString(); - // } - // else { - // return first.toString(); - // } - // } - // else { - // return content != null ? content.toString() : "No content returned"; - // } - // } - // finally { - // // 清理资源 - // try { - // if (client != null) { - // client.close(); - // } - // } - // catch (Exception e) { - // logger.warn("[handleMcpStreamableProtocol] Failed to close MCP client", e); - // } - // } - // } - // catch (Exception e) { - // logger.error("[handleMcpStreamableProtocol] MCP streamable call failed:", e); - // return "Error: MCP streamable call failed - " + e.getMessage(); - // } - // } - // else { - // logger.error("[handleMcpStreamableProtocol] serviceRef is null"); - // return "Error: service reference is null"; - // } - // } - - private java.time.Duration getTimeoutDuration() { - - return java.time.Duration.ofSeconds(30); // 默认超时时间 + /** + * Close. + */ + public void close() { + + for (Map.Entry entry : nacosConfigListeners.entrySet()) { + String cacheKey = entry.getKey(); + String dataId = cacheKey.split("@@")[0]; + String group = cacheKey.split("@@")[1]; + nacosMcpOperationService.getConfigService().removeListener(dataId, group, entry.getValue()); + } } } diff --git a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/Builder.java b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/Builder.java index fc53004fe1..37a99fc63c 100644 --- a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/Builder.java +++ b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/Builder.java @@ -19,6 +19,7 @@ public abstract class Builder { + protected String name; protected String description; @@ -55,20 +56,6 @@ public abstract class Builder { protected String inputKey = "messages"; - protected ObservationRegistry observationRegistry; - - protected ChatClientObservationConvention customObservationConvention; - - public Builder observationRegistry(ObservationRegistry observationRegistry) { - this.observationRegistry = observationRegistry; - return this; - } - - public Builder customObservationConvention(ChatClientObservationConvention customObservationConvention) { - this.customObservationConvention = customObservationConvention; - return this; - } - public Builder name(String name) { this.name = name; return this; @@ -159,6 +146,20 @@ public Builder inputKey(String inputKey) { return this; } + protected ObservationRegistry observationRegistry; + + protected ChatClientObservationConvention customObservationConvention; + + public Builder observationRegistry(ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + return this; + } + + public Builder customObservationConvention(ChatClientObservationConvention customObservationConvention) { + this.customObservationConvention = customObservationConvention; + return this; + } + public abstract ReactAgent build() throws GraphStateException; } \ No newline at end of file diff --git a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/DefaultBuilder.java b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/DefaultBuilder.java index e00d66dc6c..5bbee5d399 100644 --- a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/DefaultBuilder.java +++ b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/DefaultBuilder.java @@ -11,6 +11,7 @@ public class DefaultBuilder extends Builder { @Override public ReactAgent build() throws GraphStateException { + if (chatClient == null) { if (model == null) { throw new IllegalArgumentException("Either chatClient or model must be provided"); @@ -29,9 +30,11 @@ public ReactAgent build() throws GraphStateException { .stream(true) .chatClient(chatClient) .messagesKey(this.inputKey); - if (outputKey != null && !outputKey.isEmpty()) { - llmNodeBuilder.outputKey(outputKey); - } + // For graph built from ReactAgent, the only legal key used inside must be + // messages. + // if (outputKey != null && !outputKey.isEmpty()) { + // llmNodeBuilder.outputKey(outputKey); + // } if (CollectionUtils.isNotEmpty(tools)) { llmNodeBuilder.toolCallbacks(tools); } diff --git a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/ReactAgent.java b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/ReactAgent.java index d32552bb2c..dca4cdd6c3 100644 --- a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/ReactAgent.java +++ b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/ReactAgent.java @@ -18,22 +18,21 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.function.Function; import com.alibaba.cloud.ai.graph.CompileConfig; import com.alibaba.cloud.ai.graph.CompiledGraph; +import com.alibaba.cloud.ai.graph.GraphResponse; import com.alibaba.cloud.ai.graph.KeyStrategy; import com.alibaba.cloud.ai.graph.KeyStrategyFactory; import com.alibaba.cloud.ai.graph.NodeOutput; import com.alibaba.cloud.ai.graph.OverAllState; +import com.alibaba.cloud.ai.graph.RunnableConfig; import com.alibaba.cloud.ai.graph.StateGraph; import com.alibaba.cloud.ai.graph.action.AsyncNodeAction; import com.alibaba.cloud.ai.graph.action.NodeAction; import com.alibaba.cloud.ai.graph.agent.factory.AgentBuilderFactory; import com.alibaba.cloud.ai.graph.agent.factory.DefaultAgentBuilderFactory; -import com.alibaba.cloud.ai.graph.async.AsyncGenerator; -import com.alibaba.cloud.ai.graph.exception.GraphRunnerException; import com.alibaba.cloud.ai.graph.exception.GraphStateException; import com.alibaba.cloud.ai.graph.node.LlmNode; import com.alibaba.cloud.ai.graph.node.ToolNode; @@ -41,6 +40,7 @@ import com.alibaba.cloud.ai.graph.scheduling.ScheduledAgentTask; import com.alibaba.cloud.ai.graph.state.strategy.AppendStrategy; import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy; +import reactor.core.publisher.Flux; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.Message; @@ -57,8 +57,6 @@ public class ReactAgent extends BaseAgent { private final ToolNode toolNode; - private final StateGraph graph; - private CompiledGraph compiledGraph; private NodeAction preLlmHook; @@ -86,10 +84,8 @@ public class ReactAgent extends BaseAgent { private String inputKey; public ReactAgent(LlmNode llmNode, ToolNode toolNode, Builder builder) throws GraphStateException { - this.name = builder.name; - this.description = builder.description; + super(builder.name, builder.description, builder.outputKey); this.instruction = builder.instruction; - this.outputKey = builder.outputKey; this.llmNode = llmNode; this.toolNode = toolNode; this.keyStrategyFactory = builder.keyStrategyFactory; @@ -100,25 +96,6 @@ public ReactAgent(LlmNode llmNode, ToolNode toolNode, Builder builder) throws Gr this.preToolHook = builder.preToolHook; this.postToolHook = builder.postToolHook; this.inputKey = builder.inputKey; - - // 初始化graph - this.graph = initGraph(); - } - - public Optional invoke(Map input) throws GraphStateException, GraphRunnerException { - if (this.compiledGraph == null) { - this.compiledGraph = getAndCompileGraph(); - } - return this.compiledGraph.invoke(input); - } - - @Override - public AsyncGenerator stream(Map input) - throws GraphStateException, GraphRunnerException { - if (this.compiledGraph == null) { - this.compiledGraph = getAndCompileGraph(); - } - return this.compiledGraph.stream(input); } @Override @@ -135,27 +112,6 @@ public CompiledGraph getCompiledGraph() throws GraphStateException { return compiledGraph; } - public CompiledGraph getAndCompileGraph(CompileConfig compileConfig) throws GraphStateException { - if (this.compileConfig == null) { - this.compiledGraph = getStateGraph().compile(); - } - else { - this.compiledGraph = getStateGraph().compile(compileConfig); - } - this.compiledGraph = getStateGraph().compile(compileConfig); - return this.compiledGraph; - } - - public CompiledGraph getAndCompileGraph() throws GraphStateException { - if (this.compileConfig == null) { - this.compiledGraph = getStateGraph().compile(); - } - else { - this.compiledGraph = getStateGraph().compile(this.compileConfig); - } - return this.compiledGraph; - } - public NodeAction asNodeAction(String inputKeyFromParent, String outputKeyToParent) throws GraphStateException { if (this.compiledGraph == null) { this.compiledGraph = getAndCompileGraph(); @@ -171,7 +127,8 @@ public AsyncNodeAction asAsyncNodeAction(String inputKeyFromParent, String outpu return node_async(new SubGraphStreamingNodeAdapter(inputKeyFromParent, outputKeyToParent, this.compiledGraph)); } - private StateGraph initGraph() throws GraphStateException { + @Override + protected StateGraph initGraph() throws GraphStateException { if (keyStrategyFactory == null) { this.keyStrategyFactory = () -> { HashMap keyStrategyHashMap = new HashMap<>(); @@ -224,8 +181,8 @@ private StateGraph initGraph() throws GraphStateException { if (postLlmHook != null) { graph.addEdge("llm", "postLlm") - .addConditionalEdges("postLlm", edge_async(this::think), - Map.of("continue", preToolHook != null ? "preTool" : "tool", "end", END)); + .addConditionalEdges("postLlm", edge_async(this::think), + Map.of("continue", preToolHook != null ? "preTool" : "tool", "end", END)); } else { graph.addConditionalEdges("llm", edge_async(this::think), @@ -363,7 +320,7 @@ public Map apply(OverAllState parentState) throws Exception { List messages = List.of(message); // invoke child graph - OverAllState childState = childGraph.invoke(Map.of("messages", messages)).get(); + OverAllState childState = childGraph.call(Map.of("messages", messages)).get(); // extract output from child graph List reactMessages = (List) childState.value("messages").orElseThrow(); @@ -397,52 +354,10 @@ public Map apply(OverAllState parentState) throws Exception { Message message = new UserMessage(input); List messages = List.of(message); - AsyncGenerator child = childGraph.stream(Map.of("messages", messages)); - - AsyncGenerator wrapped = new AsyncGenerator() { - private volatile Map lastStateData; - - @Override - public Data next() { - Data data = child.next(); - if (data.isDone()) { - String result = extractAssistantText(lastStateData); - return Data.done(Map.of(outputKeyToParent, result)); - } - if (data.isError()) { - return data; - } - return Data.of(data.getData().thenApply(n -> { - try { - lastStateData = n.state().data(); - } - catch (Exception ignored) { - } - return n; - })); - } - }; - - return Map.of(outputKeyToParent, wrapped); - } + Flux> subGraphFlux = childGraph.fluxDataStream(Map.of("messages", messages), + RunnableConfig.builder().build()); - private String extractAssistantText(Map stateData) { - if (stateData == null) { - return ""; - } - Object msgs = stateData.get("messages"); - if (!(msgs instanceof List)) { - return ""; - } - List list = (List) msgs; - if (list.isEmpty()) { - return ""; - } - Object last = list.get(list.size() - 1); - if (last instanceof AssistantMessage assistant) { - return assistant.getText(); - } - return ""; + return Map.of(outputKeyToParent, subGraphFlux); } } diff --git a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/node/LlmNode.java b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/node/LlmNode.java index 098fcdf5f7..d72623a2b7 100644 --- a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/node/LlmNode.java +++ b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/node/LlmNode.java @@ -19,12 +19,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import com.alibaba.cloud.ai.graph.OverAllState; import com.alibaba.cloud.ai.graph.action.NodeAction; -import com.alibaba.cloud.ai.graph.streaming.StreamingChatGenerator; import reactor.core.publisher.Flux; From d88b6dd11a10887482294db379cb44572c8356f1 Mon Sep 17 00:00:00 2001 From: shiyiyue1102 Date: Wed, 17 Sep 2025 19:27:10 +0800 Subject: [PATCH 4/6] checkstyle --- .../com/alibaba/cloud/ai/graph/agent/Builder.java | 3 +-- .../com/alibaba/cloud/ai/graph/node/LlmNode.java | 14 ++------------ 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/Builder.java b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/Builder.java index 37a99fc63c..141d40c97a 100644 --- a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/Builder.java +++ b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/Builder.java @@ -19,7 +19,6 @@ public abstract class Builder { - protected String name; protected String description; @@ -162,4 +161,4 @@ public Builder customObservationConvention(ChatClientObservationConvention custo public abstract ReactAgent build() throws GraphStateException; -} \ No newline at end of file +} diff --git a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/node/LlmNode.java b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/node/LlmNode.java index d72623a2b7..9005a16c40 100644 --- a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/node/LlmNode.java +++ b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/node/LlmNode.java @@ -23,9 +23,10 @@ import com.alibaba.cloud.ai.graph.OverAllState; import com.alibaba.cloud.ai.graph.action.NodeAction; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import reactor.core.publisher.Flux; - import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.api.Advisor; import org.springframework.ai.chat.messages.AssistantMessage; @@ -35,19 +36,8 @@ import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.PromptTemplate; import org.springframework.ai.tool.ToolCallback; - import org.springframework.util.StringUtils; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import reactor.core.publisher.Flux; - public class LlmNode implements NodeAction { public static final String LLM_RESPONSE_KEY = "llm_response"; From ed188cdb99f9f3e753cdaac0cf3fe9a444ed1581 Mon Sep 17 00:00:00 2001 From: shiyiyue1102 Date: Wed, 17 Sep 2025 20:45:26 +0800 Subject: [PATCH 5/6] license --- .../ai/agent/nacos/NacosAgentBuilderFactory.java | 16 ++++++++++++++++ .../cloud/ai/agent/nacos/NacosAgentInjector.java | 16 ++++++++++++++++ .../ai/agent/nacos/NacosMcpToolsInjector.java | 16 ++++++++++++++++ .../cloud/ai/agent/nacos/NacosModelInjector.java | 16 ++++++++++++++++ .../cloud/ai/agent/nacos/NacosOptions.java | 16 ++++++++++++++++ .../agent/nacos/NacosPartenerAgentsInjector.java | 16 ++++++++++++++++ .../ai/agent/nacos/NacosPromptInjector.java | 16 ++++++++++++++++ .../ai/agent/nacos/NacosReactAgentBuilder.java | 16 ++++++++++++++++ .../ai/agent/nacos/ObservationConfigration.java | 16 ++++++++++++++++ .../alibaba/cloud/ai/agent/nacos/vo/AgentVO.java | 16 ++++++++++++++++ .../cloud/ai/agent/nacos/vo/McpServersVO.java | 16 ++++++++++++++++ .../cloud/ai/agent/nacos/vo/MemoryVO.java | 16 ++++++++++++++++ .../alibaba/cloud/ai/agent/nacos/vo/ModelVO.java | 16 ++++++++++++++++ .../cloud/ai/agent/nacos/vo/PartnerAgentsVO.java | 16 ++++++++++++++++ .../cloud/ai/agent/nacos/vo/PromptVO.java | 16 ++++++++++++++++ 15 files changed, 240 insertions(+) diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosAgentBuilderFactory.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosAgentBuilderFactory.java index 2691f40662..6404344c35 100644 --- a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosAgentBuilderFactory.java +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosAgentBuilderFactory.java @@ -1,3 +1,19 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.alibaba.cloud.ai.agent.nacos; import com.alibaba.cloud.ai.graph.agent.Builder; diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosAgentInjector.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosAgentInjector.java index 2dccc8e09e..640d74b331 100644 --- a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosAgentInjector.java +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosAgentInjector.java @@ -1,3 +1,19 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.alibaba.cloud.ai.agent.nacos; import com.alibaba.cloud.ai.agent.nacos.vo.AgentVO; diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosMcpToolsInjector.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosMcpToolsInjector.java index 23d9b4f663..b2b4860497 100644 --- a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosMcpToolsInjector.java +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosMcpToolsInjector.java @@ -1,3 +1,19 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.alibaba.cloud.ai.agent.nacos; import java.util.List; diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosModelInjector.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosModelInjector.java index 340f81a16f..4228e906fe 100644 --- a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosModelInjector.java +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosModelInjector.java @@ -1,3 +1,19 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.alibaba.cloud.ai.agent.nacos; import java.lang.reflect.Field; diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosOptions.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosOptions.java index 957e8b7ce7..549d879c45 100644 --- a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosOptions.java +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosOptions.java @@ -1,3 +1,19 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.alibaba.cloud.ai.agent.nacos; import java.util.Properties; diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosPartenerAgentsInjector.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosPartenerAgentsInjector.java index c449bf9ec6..51caf23f0c 100644 --- a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosPartenerAgentsInjector.java +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosPartenerAgentsInjector.java @@ -1,3 +1,19 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.alibaba.cloud.ai.agent.nacos; import com.alibaba.cloud.ai.agent.nacos.vo.PartnerAgentsVO; diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosPromptInjector.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosPromptInjector.java index c693bbf47a..8bea066ae2 100644 --- a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosPromptInjector.java +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosPromptInjector.java @@ -1,3 +1,19 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.alibaba.cloud.ai.agent.nacos; import java.lang.reflect.Field; diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosReactAgentBuilder.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosReactAgentBuilder.java index 10d2945940..5af967299b 100644 --- a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosReactAgentBuilder.java +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosReactAgentBuilder.java @@ -1,3 +1,19 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.alibaba.cloud.ai.agent.nacos; import java.util.List; diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/ObservationConfigration.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/ObservationConfigration.java index d5f22d55c4..f786f708df 100644 --- a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/ObservationConfigration.java +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/ObservationConfigration.java @@ -1,3 +1,19 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.alibaba.cloud.ai.agent.nacos; import io.micrometer.observation.ObservationRegistry; diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/AgentVO.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/AgentVO.java index a9de72a3b7..bbc23858f6 100644 --- a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/AgentVO.java +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/AgentVO.java @@ -1,3 +1,19 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.alibaba.cloud.ai.agent.nacos.vo; import lombok.Data; diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/McpServersVO.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/McpServersVO.java index e067b544a9..47539319ff 100644 --- a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/McpServersVO.java +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/McpServersVO.java @@ -1,3 +1,19 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.alibaba.cloud.ai.agent.nacos.vo; import java.util.List; diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/MemoryVO.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/MemoryVO.java index 10389f9b57..a830db4316 100644 --- a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/MemoryVO.java +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/MemoryVO.java @@ -1,3 +1,19 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.alibaba.cloud.ai.agent.nacos.vo; import lombok.Data; diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/ModelVO.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/ModelVO.java index 23fadc1ae3..37f54ac3bf 100644 --- a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/ModelVO.java +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/ModelVO.java @@ -1,3 +1,19 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.alibaba.cloud.ai.agent.nacos.vo; import lombok.Data; diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/PartnerAgentsVO.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/PartnerAgentsVO.java index 2b7240c73e..4b5e0af3f0 100644 --- a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/PartnerAgentsVO.java +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/PartnerAgentsVO.java @@ -1,3 +1,19 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.alibaba.cloud.ai.agent.nacos.vo; import java.util.List; diff --git a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/PromptVO.java b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/PromptVO.java index 7963b37950..2238bfa477 100644 --- a/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/PromptVO.java +++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/PromptVO.java @@ -1,3 +1,19 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.alibaba.cloud.ai.agent.nacos.vo; import java.util.List; From 07c13d9344afafab697dcfb08d9c7db4ddf37f8a Mon Sep 17 00:00:00 2001 From: shiyiyue1102 Date: Wed, 17 Sep 2025 20:48:27 +0800 Subject: [PATCH 6/6] license --- .../com/alibaba/cloud/ai/graph/agent/Builder.java | 15 +++++++++++++++ .../cloud/ai/graph/agent/DefaultBuilder.java | 15 +++++++++++++++ .../graph/agent/factory/AgentBuilderFactory.java | 15 +++++++++++++++ .../agent/factory/DefaultAgentBuilderFactory.java | 15 +++++++++++++++ 4 files changed, 60 insertions(+) diff --git a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/Builder.java b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/Builder.java index 141d40c97a..863e35f5c8 100644 --- a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/Builder.java +++ b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/Builder.java @@ -1,3 +1,18 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.alibaba.cloud.ai.graph.agent; import java.util.List; diff --git a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/DefaultBuilder.java b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/DefaultBuilder.java index 5bbee5d399..83d206a543 100644 --- a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/DefaultBuilder.java +++ b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/DefaultBuilder.java @@ -1,3 +1,18 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.alibaba.cloud.ai.graph.agent; import com.alibaba.cloud.ai.graph.exception.GraphStateException; diff --git a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/factory/AgentBuilderFactory.java b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/factory/AgentBuilderFactory.java index aeeb8b217b..4c89256915 100644 --- a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/factory/AgentBuilderFactory.java +++ b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/factory/AgentBuilderFactory.java @@ -1,3 +1,18 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.alibaba.cloud.ai.graph.agent.factory; import com.alibaba.cloud.ai.graph.agent.Builder; diff --git a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/factory/DefaultAgentBuilderFactory.java b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/factory/DefaultAgentBuilderFactory.java index 944a2a5464..df94cde017 100644 --- a/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/factory/DefaultAgentBuilderFactory.java +++ b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/factory/DefaultAgentBuilderFactory.java @@ -1,3 +1,18 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.alibaba.cloud.ai.graph.agent.factory; import com.alibaba.cloud.ai.graph.agent.Builder;