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..6404344c35
--- /dev/null
+++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosAgentBuilderFactory.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+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..640d74b331
--- /dev/null
+++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosAgentInjector.java
@@ -0,0 +1,108 @@
+/*
+ * 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;
+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 agentName
+ * @return
+ */
+ public static AgentVO loadAgentVO(NacosConfigService nacosConfigService, String agentName) {
+ try {
+ String config = nacosConfigService.getConfig("agent-base.json", "ai-agent-" + agentName,
+ 3000L);
+ return JSON.parseObject(config, AgentVO.class);
+ }
+ catch (NacosException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static void injectPromptByAgentName(NacosConfigService nacosConfigService, ChatClient chatClient,String agentName,AgentVO agentVO) {
+
+ try {
+ if (agentVO == null) {
+ return;
+ }
+ PromptVO promptVO = NacosPromptInjector.loadPromptByAgentId(nacosConfigService, agentVO);
+ if (promptVO != null) {
+ NacosPromptInjector.replacePrompt(chatClient, promptVO);
+ }
+ NacosPromptInjector.registryPromptByAgentId(chatClient, nacosConfigService, agentName, 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..b2b4860497
--- /dev/null
+++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosMcpToolsInjector.java
@@ -0,0 +1,92 @@
+/*
+ * 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;
+
+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 agentName) {
+
+ try {
+ nacosOptions.getNacosConfigService()
+ .addListener("mcp-servers.json", "ai-agent-" + agentName, 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 agentName) {
+ try {
+ String config = nacosOptions.getNacosConfigService()
+ .getConfig("mcp-servers.json", "ai-agent-" + agentName, 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..4228e906fe
--- /dev/null
+++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosModelInjector.java
@@ -0,0 +1,178 @@
+/*
+ * 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;
+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.json");
+ String config = nacosOptions.getNacosConfigService().getConfig(dataIdT, "ai-agent-" + agentId, 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.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);
+ }
+ }
+
+ 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..549d879c45
--- /dev/null
+++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosOptions.java
@@ -0,0 +1,60 @@
+/*
+ * 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;
+
+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/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..51caf23f0c
--- /dev/null
+++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosPartenerAgentsInjector.java
@@ -0,0 +1,61 @@
+/*
+ * 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;
+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
new file mode 100644
index 0000000000..8bea066ae2
--- /dev/null
+++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosPromptInjector.java
@@ -0,0 +1,195 @@
+/*
+ * 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;
+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 AgentVO
+ * @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("agent-base.json", "ai-agent-" + agentId,
+ 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..5af967299b
--- /dev/null
+++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/NacosReactAgentBuilder.java
@@ -0,0 +1,121 @@
+/*
+ * 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;
+
+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;
+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 {
+ AgentVO agentVO = NacosAgentInjector.loadAgentVO(nacosOptions.getNacosConfigService(), this.name);
+ this.description = agentVO.getDescription();
+ NacosAgentInjector.injectPromptByAgentName(nacosOptions.getNacosConfigService(), chatClient, nacosOptions.getAgentName(), agentVO);
+ }
+ }
+
+ 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..f786f708df
--- /dev/null
+++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/ObservationConfigration.java
@@ -0,0 +1,45 @@
+/*
+ * 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;
+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..db47c69127
--- /dev/null
+++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/tools/NacosMcpGatewayToolCallback.java
@@ -0,0 +1,593 @@
+/*
+ * 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.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.config.listener.AbstractListener;
+import com.alibaba.nacos.api.exception.NacosException;
+import com.alibaba.nacos.common.utils.JacksonUtils;
+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.apache.poi.util.StringUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.ai.chat.model.ToolContext;
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.ai.tool.definition.ToolDefinition;
+import org.springframework.lang.NonNull;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+public class NacosMcpGatewayToolCallback implements ToolCallback {
+
+ private static final Logger logger = LoggerFactory.getLogger(com.alibaba.cloud.ai.mcp.gateway.nacos.callback.NacosMcpGatewayToolCallback.class);
+
+ private static final Pattern TEMPLATE_PATTERN = Pattern.compile("\\{\\{\\s*(\\.[\\w]+(?:\\.[\\w]+)*)\\s*\\}\\}");
+
+ // 匹配 {{ ${nacos.dataId/group} }} 或 {{ ${nacos.dataId/group}.key1.key2 }}
+ private static final Pattern NACOS_TEMPLATE_PATTERN = Pattern
+ .compile("\\{\\{\\s*\\$\\{nacos\\.([^}]+)\\}(\\.[\\w]+(?:\\.[\\w]+)*)?\\s*}}");
+
+ /**
+ * The Object mapper.
+ */
+ static ObjectMapper objectMapper = new ObjectMapper();
+
+ static {
+ objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+ objectMapper.setSerializationInclusion(Include.NON_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 = mcpServersVO;
+ }
+
+ /**
+ * Process nacos config ref template string.
+ * @param template the template
+ * @return the string
+ */
+ public String processNacosConfigRefTemplate(String template) {
+ if (!org.springframework.util.StringUtils.hasText(template)) {
+ return template;
+ }
+
+ StringBuffer result = new StringBuffer();
+ Matcher matcher = NACOS_TEMPLATE_PATTERN.matcher(template);
+
+ 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);
+
+ 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;
+ }
+
+ try {
+ // 解析dataId和group
+ String[] configParts = nacosRef.split("/");
+ if (configParts.length != 2) {
+ throw new IllegalArgumentException(
+ "Invalid Nacos config reference format: " + nacosRef + ". Expected format: dataId/group");
+ }
+
+ 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;
+ }
+
+ // 如果没有点语法,直接返回配置内容
+ if (!org.springframework.util.StringUtils.hasText(dotNotation)) {
+ return configContent;
+ }
+
+ // 如果有点语法,去掉开头的点号,然后解析JSON并提取指定字段
+ String jsonPath = dotNotation.startsWith(".") ? dotNotation.substring(1) : dotNotation;
+ return extractJsonValueFromNacos(configContent, jsonPath);
+
+ }
+ catch (Exception 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 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);
+ }
+ };
+ AbstractListener oldListener = nacosConfigListeners.putIfAbsent(cacheKey, listener);
+ if (oldListener == null) {
+ try {
+ nacosMcpOperationService.getConfigService().addListener(dataId, group, listener);
+ }
+ 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);
+ }
+ }
+
+ /**
+ * 从JSON字符串中提取指定路径的值
+ * @param jsonString JSON字符串
+ * @param jsonPath JSON路径,如 key1.key2
+ * @return 提取的值
+ */
+ private String extractJsonValueFromNacos(String jsonString, String jsonPath) throws JsonProcessingException {
+
+ 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);
+ }
+
+ 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();
+ }
+ }
+ 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 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()) {
+ // 获取完整路径,如 .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 (args != null && !args.isEmpty()) {
+ return args.toString();
+ }
+ else {
+ return "";
+ }
+ }
+ }
+ else {
+ // 从extendedData中取值
+ // 首先将extendedData字符串解析为JSON对象
+ try {
+ if (StringUtils.hasText(extendedData)) {
+ dataSource = objectMapper.readValue(extendedData, Map.class);
+ }
+ else {
+ dataSource = null;
+ }
+ }
+ 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 : "";
+ }
+ }
+
+ // 如果数据源为空
+ 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 "";
+ }
+
+ if (currentValue == null) {
+ logger.warn("[resolvePathValue] Key '{}' not found in nested path", key);
+ return "";
+ }
+ }
+ return currentValue.toString();
+ }
+
+ @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 ("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();
+ }
+ }
+
+ /**
+ * 处理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 = StringUtil.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";
+ }
+ }
+ 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";
+ }
+ }
+
+ /**
+ * 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-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..bbc23858f6
--- /dev/null
+++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/AgentVO.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+@Data
+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/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..47539319ff
--- /dev/null
+++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/McpServersVO.java
@@ -0,0 +1,47 @@
+/*
+ * 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;
+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/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..a830db4316
--- /dev/null
+++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/MemoryVO.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+@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/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..37f54ac3bf
--- /dev/null
+++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/ModelVO.java
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+@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/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..4b5e0af3f0
--- /dev/null
+++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/PartnerAgentsVO.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+import java.util.Map;
+
+import lombok.Data;
+
+@Data
+public class PartnerAgentsVO {
+
+ List agents;
+
+ @Data
+ public static class PartnerAgentVO {
+
+ String agentName;
+
+ Map headers;
+
+ Map queryPrams;
+
+ }
+}
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..2238bfa477
--- /dev/null
+++ b/spring-ai-alibaba-agent-nacos/src/main/java/com/alibaba/cloud/ai/agent/nacos/vo/PromptVO.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+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..863e35f5c8
--- /dev/null
+++ b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/Builder.java
@@ -0,0 +1,179 @@
+/*
+ * 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;
+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";
+
+ 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;
+ }
+
+ 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;
+
+}
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..83d206a543
--- /dev/null
+++ b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/DefaultBuilder.java
@@ -0,0 +1,73 @@
+/*
+ * 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;
+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);
+ // 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);
+ }
+
+}
+
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..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
@@ -15,6 +15,11 @@
*/
package com.alibaba.cloud.ai.graph.agent;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+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;
@@ -26,6 +31,8 @@
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.exception.GraphStateException;
import com.alibaba.cloud.ai.graph.node.LlmNode;
import com.alibaba.cloud.ai.graph.node.ToolNode;
@@ -33,23 +40,11 @@
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.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;
@@ -88,7 +83,7 @@ public class ReactAgent extends BaseAgent {
private String inputKey;
- protected ReactAgent(LlmNode llmNode, ToolNode toolNode, Builder builder) throws GraphStateException {
+ public ReactAgent(LlmNode llmNode, ToolNode toolNode, Builder builder) throws GraphStateException {
super(builder.name, builder.description, builder.outputKey);
this.instruction = builder.instruction;
this.llmNode = llmNode;
@@ -103,10 +98,6 @@ protected ReactAgent(LlmNode llmNode, ToolNode toolNode, Builder builder) throws
this.inputKey = builder.inputKey;
}
- public static Builder builder() {
- return new Builder();
- }
-
@Override
public ScheduledAgentTask schedule(ScheduleConfig scheduleConfig) throws GraphStateException {
CompiledGraph compiledGraph = getAndCompileGraph();
@@ -298,177 +289,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 {
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..4c89256915
--- /dev/null
+++ b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/factory/AgentBuilderFactory.java
@@ -0,0 +1,23 @@
+/*
+ * 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;
+
+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..df94cde017
--- /dev/null
+++ b/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/agent/factory/DefaultAgentBuilderFactory.java
@@ -0,0 +1,27 @@
+/*
+ * 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;
+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..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
@@ -15,8 +15,17 @@
*/
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.Optional;
+
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;
@@ -26,21 +35,9 @@
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;
-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";
@@ -158,6 +155,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 +174,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;
}