From 8ca2d0a3de86f680d0b99ef0cff964a339b97cae Mon Sep 17 00:00:00 2001
From: lhpqaq <657407891@qq.com>
Date: Thu, 26 Mar 2026 20:18:56 +0800
Subject: [PATCH 1/7] Refactor: Remove unused function classes and tests
- Deleted ClusterFunctions, HostFunctions, StackFunctions, AIServiceToolsProvider, and InfoToolsProvider classes as they are no longer needed.
- Removed associated test classes: ClusterFunctionsTest, HostFunctionsTest, StackFunctionsTest, and AIServiceToolsProviderTest.
- Updated application.yml to include new client configuration.
- Added new API methods in llm-config for fetching platform models.
- Enhanced UI components to support model fetching with loading states and error handling.
- Updated localization files for new UI strings related to model fetching.
- Added repository for Spring milestones in pom.xml.
---
bigtop-manager-ai/pom.xml | 4 +
.../ai/assistant/GeneralAssistantFactory.java | 46 ++++-
.../ai/config/McpAsyncClientManager.java | 109 +++++++++++
.../manager/ai/core/AbstractAIAssistant.java | 93 +++++++++
.../manager/ai/core/factory/AIAssistant.java | 6 +
.../ai/core/factory/AIAssistantFactory.java | 12 +-
.../ai/platform/DashScopeAssistant.java | 37 +++-
.../ai/platform/DeepSeekAssistant.java | 44 ++++-
.../manager/ai/platform/OpenAIAssistant.java | 45 ++++-
.../manager/ai/platform/QianFanAssistant.java | 81 +++++++-
.../GeneralAssistantFactoryTest.java | 4 +-
.../ai/config/McpAsyncClientManagerTest.java | 60 ++++++
bigtop-manager-bom/pom.xml | 6 +
bigtop-manager-server/pom.xml | 4 -
.../controller/LLMConfigController.java | 10 +
.../manager/server/mcp/tool/StackMcpTool.java | 4 +-
.../converter/AuthPlatformConverter.java | 1 +
.../server/service/LLMConfigService.java | 3 +
.../service/impl/ChatbotServiceImpl.java | 9 +-
.../service/impl/LLMConfigServiceImpl.java | 106 ++++++-----
.../tools/functions/ClusterFunctions.java | 107 -----------
.../server/tools/functions/HostFunctions.java | 93 ---------
.../tools/functions/StackFunctions.java | 108 -----------
.../provider/AIServiceToolsProvider.java | 40 ----
.../tools/provider/InfoToolsProvider.java | 54 ------
.../src/main/resources/application.yml | 6 +-
.../tools/functions/ClusterFunctionsTest.java | 180 ------------------
.../tools/functions/HostFunctionsTest.java | 153 ---------------
.../tools/functions/StackFunctionsTest.java | 171 -----------------
.../provider/AIServiceToolsProviderTest.java | 62 ------
bigtop-manager-ui/src/api/llm-config/index.ts | 16 +-
bigtop-manager-ui/src/api/llm-config/types.ts | 4 +
.../src/locales/en_US/llm-config.ts | 5 +-
.../src/locales/zh_CN/llm-config.ts | 5 +-
.../llm-config/components/add-llm-item.vue | 31 ++-
.../src/store/llm-config/index.ts | 25 +++
pom.xml | 11 ++
37 files changed, 678 insertions(+), 1077 deletions(-)
create mode 100644 bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/config/McpAsyncClientManager.java
create mode 100644 bigtop-manager-ai/src/test/java/org/apache/bigtop/manager/ai/config/McpAsyncClientManagerTest.java
delete mode 100644 bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/functions/ClusterFunctions.java
delete mode 100644 bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/functions/HostFunctions.java
delete mode 100644 bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/functions/StackFunctions.java
delete mode 100644 bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/provider/AIServiceToolsProvider.java
delete mode 100644 bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/provider/InfoToolsProvider.java
delete mode 100644 bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/tools/functions/ClusterFunctionsTest.java
delete mode 100644 bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/tools/functions/HostFunctionsTest.java
delete mode 100644 bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/tools/functions/StackFunctionsTest.java
delete mode 100644 bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/tools/provider/AIServiceToolsProviderTest.java
diff --git a/bigtop-manager-ai/pom.xml b/bigtop-manager-ai/pom.xml
index 6eb68ca83..6c1fbbd73 100644
--- a/bigtop-manager-ai/pom.xml
+++ b/bigtop-manager-ai/pom.xml
@@ -67,6 +67,10 @@
org.apache.commons
commons-lang3
+
+ org.springframework.ai
+ spring-ai-starter-mcp-client-webflux
+
diff --git a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/assistant/GeneralAssistantFactory.java b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/assistant/GeneralAssistantFactory.java
index 38951c8ec..a59a94b33 100644
--- a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/assistant/GeneralAssistantFactory.java
+++ b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/assistant/GeneralAssistantFactory.java
@@ -20,6 +20,7 @@
import org.apache.bigtop.manager.ai.assistant.config.GeneralAssistantConfig;
import org.apache.bigtop.manager.ai.assistant.provider.ChatMemoryStoreProvider;
+import org.apache.bigtop.manager.ai.config.McpAsyncClientManager;
import org.apache.bigtop.manager.ai.core.AbstractAIAssistantFactory;
import org.apache.bigtop.manager.ai.core.config.AIAssistantConfig;
import org.apache.bigtop.manager.ai.core.enums.PlatformType;
@@ -33,12 +34,14 @@
import org.apache.bigtop.manager.ai.platform.QianFanAssistant;
import org.springframework.stereotype.Component;
+import lombok.extern.slf4j.Slf4j;
import jakarta.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
@Component
+@Slf4j
public class GeneralAssistantFactory extends AbstractAIAssistantFactory {
@Resource
@@ -47,6 +50,9 @@ public class GeneralAssistantFactory extends AbstractAIAssistantFactory {
@Resource
private ChatMemoryStoreProvider chatMemoryStoreProvider;
+ @Resource
+ private McpAsyncClientManager mcpAsyncClientManager;
+
private void configureSystemPrompt(AIAssistant.Builder builder, SystemPrompt systemPrompt, String locale) {
List systemPrompts = new ArrayList<>();
if (systemPrompt != null) {
@@ -68,7 +74,7 @@ private AIAssistant.Builder initializeBuilder(PlatformType platformType) {
}
@Override
- public AIAssistant createWithPrompt(AIAssistantConfig config, Object toolProvider, SystemPrompt systemPrompt) {
+ public AIAssistant createWithPrompt(AIAssistantConfig config, SystemPrompt systemPrompt) {
GeneralAssistantConfig generalAssistantConfig = (GeneralAssistantConfig) config;
PlatformType platformType = generalAssistantConfig.getPlatformType();
Object id = generalAssistantConfig.getId();
@@ -81,13 +87,21 @@ public AIAssistant createWithPrompt(AIAssistantConfig config, Object toolProvide
.memoryStore(chatMemoryStoreProvider.createPersistentChatMemoryStore(id))
.withConfig(generalAssistantConfig);
+ io.modelcontextprotocol.client.McpAsyncClient mcpAsyncClient = mcpAsyncClientManager.getClient();
+ if (mcpAsyncClient != null) {
+ log.info("MCP client available for platform {} (chat)", platformType);
+ builder.withMcpClient(mcpAsyncClient);
+ } else {
+ log.info("MCP client unavailable for platform {} (chat)", platformType);
+ }
+
configureSystemPrompt(builder, systemPrompt, generalAssistantConfig.getLanguage());
return builder.build();
}
@Override
- public AIAssistant createForTest(AIAssistantConfig config, Object toolProvider) {
+ public AIAssistant createForTest(AIAssistantConfig config) {
GeneralAssistantConfig generalAssistantConfig = (GeneralAssistantConfig) config;
PlatformType platformType = generalAssistantConfig.getPlatformType();
AIAssistant.Builder builder = initializeBuilder(platformType);
@@ -96,6 +110,34 @@ public AIAssistant createForTest(AIAssistantConfig config, Object toolProvider)
.memoryStore(chatMemoryStoreProvider.createInMemoryChatMemoryStore())
.withConfig(generalAssistantConfig);
+ io.modelcontextprotocol.client.McpAsyncClient mcpAsyncClient = mcpAsyncClientManager.getClient();
+ if (mcpAsyncClient != null) {
+ log.info("MCP client available for platform {} (test)", platformType);
+ builder.withMcpClient(mcpAsyncClient);
+ } else {
+ log.info("MCP client unavailable for platform {} (test)", platformType);
+ }
+
return builder.build();
}
+
+ @Override
+ public List getModels(AIAssistantConfig config) {
+ GeneralAssistantConfig generalAssistantConfig = (GeneralAssistantConfig) config;
+ PlatformType platformType = generalAssistantConfig.getPlatformType();
+ try {
+ AIAssistant.Builder builder = initializeBuilder(platformType);
+ builder.withConfig(generalAssistantConfig);
+ List models = builder.getModels();
+ if (models != null && !models.isEmpty()) {
+ log.info("Fetched {} dynamic models for platform {}.", models.size(), platformType);
+ return models;
+ }
+ } catch (Exception e) {
+ log.warn("Failed to fetch dynamic models from platform {}: {}", platformType, e.getMessage());
+ }
+
+ log.info("No dynamic models for platform {}, fallback to default models.", platformType);
+ return java.util.Collections.emptyList();
+ }
}
diff --git a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/config/McpAsyncClientManager.java b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/config/McpAsyncClientManager.java
new file mode 100644
index 000000000..c9c5af8ec
--- /dev/null
+++ b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/config/McpAsyncClientManager.java
@@ -0,0 +1,109 @@
+package org.apache.bigtop.manager.ai.config;
+
+import io.modelcontextprotocol.client.McpAsyncClient;
+import io.modelcontextprotocol.client.McpClient;
+import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.web.reactive.function.client.WebClient;
+import lombok.extern.slf4j.Slf4j;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+
+@Component
+@Slf4j
+public class McpAsyncClientManager {
+
+ @Value("${server.port:8080}")
+ private String serverPort;
+
+ @Value("${spring.ai.mcp.client.enabled:false}")
+ private boolean enabled;
+
+ @Value("${spring.ai.mcp.client.base-url:http://localhost:${server.port:8080}}")
+ private String clientBaseUrl;
+
+ @Value("${spring.ai.mcp.client.sse-endpoint:/mcp/sse}")
+ private String sseEndpoint;
+
+ private McpAsyncClient mcpAsyncClient;
+ private boolean initialized = false;
+ private boolean disabledLogged = false;
+
+ public synchronized McpAsyncClient getClient() {
+ if (!enabled) {
+ if (!disabledLogged) {
+ log.info("MCP Async Client is disabled by config (spring.ai.mcp.client.enabled=false)");
+ disabledLogged = true;
+ }
+ return null;
+ }
+
+ if (!initialized) {
+ try {
+ String baseUrl = resolveBaseUrl();
+ log.info("Initializing MCP Async Client with baseUrl={}, sseEndpoint={}", baseUrl, sseEndpoint);
+ WebClient.Builder webClientBuilder = WebClient.builder().baseUrl(baseUrl);
+ WebFluxSseClientTransport transport =
+ WebFluxSseClientTransport.builder(webClientBuilder).sseEndpoint(sseEndpoint).build();
+
+ McpAsyncClient client = McpClient.async(transport)
+ .requestTimeout(Duration.ofSeconds(30))
+ .build();
+
+ client.initialize().block(Duration.ofSeconds(10));
+ client.ping().block(Duration.ofSeconds(10));
+
+ mcpAsyncClient = client;
+ initialized = true;
+ log.info("MCP Async Client successfully initialized to {}{}", baseUrl, sseEndpoint);
+ logRegisteredTools(client);
+ } catch (Exception e) {
+ log.warn("Failed to initialize MCP Async Client: {}", e.getMessage());
+ // Leave initialized = false to retry on next getClient() call
+ }
+ }
+ return mcpAsyncClient;
+ }
+
+ private void logRegisteredTools(McpAsyncClient client) {
+ try {
+ List toolNames = new ArrayList<>();
+ String cursor = null;
+ while (true) {
+ McpSchema.ListToolsResult listToolsResult = cursor == null || cursor.isBlank()
+ ? client.listTools().block(Duration.ofSeconds(10))
+ : client.listTools(cursor).block(Duration.ofSeconds(10));
+ if (listToolsResult == null || listToolsResult.tools() == null || listToolsResult.tools().isEmpty()) {
+ break;
+ }
+
+ for (McpSchema.Tool tool : listToolsResult.tools()) {
+ if (tool != null && tool.name() != null) {
+ toolNames.add(tool.name());
+ }
+ }
+
+ String nextCursor = listToolsResult.nextCursor();
+ if (nextCursor == null || nextCursor.isBlank() || nextCursor.equals(cursor)) {
+ break;
+ }
+ cursor = nextCursor;
+ }
+
+ log.info("MCP tools discovered: count={}, names={}", toolNames.size(), toolNames);
+ } catch (Exception e) {
+ log.warn("Failed to list MCP tools after initialization: {}", e.getMessage());
+ }
+ }
+
+ private String resolveBaseUrl() {
+ if (clientBaseUrl != null && !clientBaseUrl.isBlank()) {
+ return clientBaseUrl;
+ }
+ return "http://localhost:" + serverPort;
+ }
+}
diff --git a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/AbstractAIAssistant.java b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/AbstractAIAssistant.java
index 30ec7ab62..b40a7a4ee 100644
--- a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/AbstractAIAssistant.java
+++ b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/AbstractAIAssistant.java
@@ -24,9 +24,18 @@
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemoryRepository;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
+import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
+import com.fasterxml.jackson.databind.JsonNode;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
public abstract class AbstractAIAssistant implements AIAssistant {
protected final AIAssistant.Service aiServices;
protected static final Integer MEMORY_LEN = 10;
@@ -67,6 +76,10 @@ public abstract static class Builder implements AIAssistant.Builder {
protected String systemPrompt;
+ protected io.modelcontextprotocol.client.McpAsyncClient mcpAsyncClient;
+
+ private static final String AUTHORIZATION_HEADER = "Authorization";
+
public Builder() {}
public Builder withSystemPrompt(String systemPrompt) {
@@ -89,6 +102,11 @@ public Builder memoryStore(ChatMemory chatMemory) {
return this;
}
+ public Builder withMcpClient(io.modelcontextprotocol.client.McpAsyncClient mcpAsyncClient) {
+ this.mcpAsyncClient = mcpAsyncClient;
+ return this;
+ }
+
public ChatMemory getChatMemory() {
if (chatMemory == null) {
chatMemory = MessageWindowChatMemory.builder()
@@ -97,5 +115,80 @@ public ChatMemory getChatMemory() {
}
return chatMemory;
}
+
+ protected String resolveModelsBaseUrl() {
+ return null;
+ }
+
+ protected String resolveModelsPath() {
+ return "/v1/models";
+ }
+
+ protected String resolveApiKey(Map credentials) {
+ if (credentials == null) {
+ return null;
+ }
+ String apiKey = credentials.get("apiKey");
+ if (apiKey == null) {
+ return null;
+ }
+ apiKey = apiKey.trim();
+ if (apiKey.startsWith("Bearer ")) {
+ apiKey = apiKey.substring("Bearer ".length()).trim();
+ }
+ return apiKey;
+ }
+
+ protected void applyModelRequestAuth(WebClient.RequestHeadersSpec> requestSpec, String apiKey) {
+ if (apiKey != null && !apiKey.isBlank()) {
+ requestSpec.header(AUTHORIZATION_HEADER, "Bearer " + apiKey);
+ }
+ }
+
+ protected List parseModelsResponse(JsonNode response) {
+ if (response == null || !response.has("data")) {
+ return Collections.emptyList();
+ }
+ List models = new ArrayList<>();
+ for (JsonNode node : response.get("data")) {
+ JsonNode idNode = node.get("id");
+ if (idNode != null && !idNode.isNull()) {
+ models.add(idNode.asText());
+ }
+ }
+ return models;
+ }
+
+ @Override
+ public List getModels() {
+ String baseUrl = resolveModelsBaseUrl();
+ if (baseUrl == null || baseUrl.isBlank()) {
+ return Collections.emptyList();
+ }
+
+ String path = resolveModelsPath();
+ if (path == null || path.isBlank()) {
+ path = "/v1/models";
+ }
+
+ Map credentials = config == null ? Collections.emptyMap() : config.getCredentials();
+ String apiKey = resolveApiKey(credentials);
+
+ try {
+ WebClient webClient = WebClient.builder().baseUrl(baseUrl.trim()).build();
+ WebClient.RequestHeadersSpec> requestSpec = webClient.get().uri(path);
+ applyModelRequestAuth(requestSpec, apiKey);
+
+ JsonNode response = requestSpec
+ .retrieve()
+ .bodyToMono(JsonNode.class)
+ .timeout(Duration.ofSeconds(10))
+ .block();
+
+ return parseModelsResponse(response);
+ } catch (Exception ignored) {
+ return Collections.emptyList();
+ }
+ }
}
}
diff --git a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/factory/AIAssistant.java b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/factory/AIAssistant.java
index ff2b96f5c..cf76ec1ab 100644
--- a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/factory/AIAssistant.java
+++ b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/factory/AIAssistant.java
@@ -27,6 +27,8 @@
import reactor.core.publisher.Flux;
+import java.util.List;
+
public interface AIAssistant {
/**
@@ -75,6 +77,8 @@ interface Builder {
Builder withConfig(AIAssistantConfig configProvider);
+ Builder withMcpClient(io.modelcontextprotocol.client.McpAsyncClient mcpAsyncClient);
+
Builder withSystemPrompt(String systemPrompt);
AIAssistant build();
@@ -84,5 +88,7 @@ interface Builder {
StreamingChatModel getStreamingChatModel();
ChatMemory getChatMemory();
+
+ List getModels();
}
}
diff --git a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/factory/AIAssistantFactory.java b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/factory/AIAssistantFactory.java
index 06e1dafef..c36ccc4fc 100644
--- a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/factory/AIAssistantFactory.java
+++ b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/factory/AIAssistantFactory.java
@@ -21,13 +21,17 @@
import org.apache.bigtop.manager.ai.core.config.AIAssistantConfig;
import org.apache.bigtop.manager.ai.core.enums.SystemPrompt;
+import java.util.List;
+
public interface AIAssistantFactory {
- AIAssistant createWithPrompt(AIAssistantConfig config, Object toolProvider, SystemPrompt systemPrompt);
+ AIAssistant createWithPrompt(AIAssistantConfig config, SystemPrompt systemPrompt);
- AIAssistant createForTest(AIAssistantConfig config, Object toolProvider);
+ AIAssistant createForTest(AIAssistantConfig config);
- default AIAssistant createAIService(AIAssistantConfig config, Object toolProvider) {
- return createWithPrompt(config, toolProvider, SystemPrompt.DEFAULT_PROMPT);
+ default AIAssistant createAIService(AIAssistantConfig config) {
+ return createWithPrompt(config, SystemPrompt.DEFAULT_PROMPT);
}
+
+ List getModels(AIAssistantConfig config);
}
diff --git a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DashScopeAssistant.java b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DashScopeAssistant.java
index 6661e6e02..cd8e205b8 100644
--- a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DashScopeAssistant.java
+++ b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DashScopeAssistant.java
@@ -41,6 +41,7 @@
public class DashScopeAssistant extends AbstractAIAssistant {
+ private static final String BASE_URL_ENV_KEY = "BIGTOP_MANAGER_AI_DASHSCOPE_BASE_URL";
private static final String BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode";
public DashScopeAssistant(Object memoryId, ChatMemory chatMemory, AIAssistant.Service aiServices) {
@@ -58,6 +59,19 @@ public static Builder builder() {
public static class Builder extends AbstractAIAssistant.Builder {
+ @Override
+ protected String resolveModelsBaseUrl() {
+ return resolveDefaultBaseUrl();
+ }
+
+ private String resolveDefaultBaseUrl() {
+ String envBaseUrl = System.getenv(BASE_URL_ENV_KEY);
+ if (envBaseUrl != null && !envBaseUrl.isBlank()) {
+ return envBaseUrl;
+ }
+ return BASE_URL;
+ }
+
@Override
public ChatModel getChatModel() {
String model = config.getModel();
@@ -66,8 +80,13 @@ public ChatModel getChatModel() {
Assert.notNull(apiKey, "apiKey must not be null");
OpenAiApi openAiApi =
- OpenAiApi.builder().baseUrl(BASE_URL).apiKey(apiKey).build();
- OpenAiChatOptions options = OpenAiChatOptions.builder().model(model).build();
+ OpenAiApi.builder().baseUrl(resolveDefaultBaseUrl()).apiKey(apiKey).build();
+ OpenAiChatOptions.Builder optionsBuilder = OpenAiChatOptions.builder().model(model);
+ if (mcpAsyncClient != null) {
+ optionsBuilder.toolCallbacks(
+ new org.springframework.ai.mcp.AsyncMcpToolCallbackProvider(mcpAsyncClient).getToolCallbacks());
+ }
+ OpenAiChatOptions options = optionsBuilder.build();
return OpenAiChatModel.builder()
.openAiApi(openAiApi)
.defaultOptions(options)
@@ -132,13 +151,17 @@ public Flux streamChat(String userMessage) {
StringBuilder responseBuilder = new StringBuilder();
return streamingChatModel.stream(prompt)
- .map(chatResponse -> {
- String content =
- chatResponse.getResult().getOutput().getText();
- if (content != null) {
+ .concatMap(chatResponse -> {
+ String content = null;
+ if (chatResponse.getResult() != null
+ && chatResponse.getResult().getOutput() != null) {
+ content = chatResponse.getResult().getOutput().getText();
+ }
+ if (content != null && !content.isEmpty()) {
responseBuilder.append(content);
+ return Flux.just(content);
}
- return content;
+ return Flux.empty();
})
.doOnComplete(() -> {
// Save to memory when streaming completes
diff --git a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DeepSeekAssistant.java b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DeepSeekAssistant.java
index 962ad8258..e52bac1fc 100644
--- a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DeepSeekAssistant.java
+++ b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DeepSeekAssistant.java
@@ -42,6 +42,9 @@
public class DeepSeekAssistant extends AbstractAIAssistant {
+ private static final String BASE_URL_ENV_KEY = "BIGTOP_MANAGER_AI_DEEPSEEK_BASE_URL";
+ private static final String BASE_URL = "https://api.deepseek.com";
+
public DeepSeekAssistant(Object memoryId, ChatMemory chatMemory, AIAssistant.Service aiServices) {
super(memoryId, chatMemory, aiServices);
}
@@ -57,6 +60,19 @@ public static Builder builder() {
public static class Builder extends AbstractAIAssistant.Builder {
+ @Override
+ protected String resolveModelsBaseUrl() {
+ return resolveDefaultBaseUrl();
+ }
+
+ private String resolveDefaultBaseUrl() {
+ String envBaseUrl = System.getenv(BASE_URL_ENV_KEY);
+ if (envBaseUrl != null && !envBaseUrl.isBlank()) {
+ return envBaseUrl;
+ }
+ return BASE_URL;
+ }
+
@Override
public ChatModel getChatModel() {
String model = config.getModel();
@@ -64,9 +80,17 @@ public ChatModel getChatModel() {
String apiKey = config.getCredentials().get("apiKey");
Assert.notNull(apiKey, "apiKey must not be null");
- DeepSeekApi deepSeekApi = DeepSeekApi.builder().apiKey(apiKey).build();
- DeepSeekChatOptions options =
- DeepSeekChatOptions.builder().model(model).build();
+ DeepSeekApi deepSeekApi = DeepSeekApi.builder()
+ .baseUrl(resolveDefaultBaseUrl())
+ .apiKey(apiKey)
+ .build();
+ DeepSeekChatOptions.Builder optionsBuilder =
+ DeepSeekChatOptions.builder().model(model);
+ if (mcpAsyncClient != null) {
+ optionsBuilder.toolCallbacks(
+ new org.springframework.ai.mcp.AsyncMcpToolCallbackProvider(mcpAsyncClient).getToolCallbacks());
+ }
+ DeepSeekChatOptions options = optionsBuilder.build();
return DeepSeekChatModel.builder()
.deepSeekApi(deepSeekApi)
.defaultOptions(options)
@@ -131,13 +155,17 @@ public Flux streamChat(String userMessage) {
StringBuilder responseBuilder = new StringBuilder();
return streamingChatModel.stream(prompt)
- .map(chatResponse -> {
- String content =
- chatResponse.getResult().getOutput().getText();
- if (content != null) {
+ .concatMap(chatResponse -> {
+ String content = null;
+ if (chatResponse.getResult() != null
+ && chatResponse.getResult().getOutput() != null) {
+ content = chatResponse.getResult().getOutput().getText();
+ }
+ if (content != null && !content.isEmpty()) {
responseBuilder.append(content);
+ return Flux.just(content);
}
- return content;
+ return Flux.empty();
})
.doOnComplete(() -> {
// Save to memory when streaming completes
diff --git a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/OpenAIAssistant.java b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/OpenAIAssistant.java
index b51fa75ef..2fe30ae53 100644
--- a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/OpenAIAssistant.java
+++ b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/OpenAIAssistant.java
@@ -38,9 +38,11 @@
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
public class OpenAIAssistant extends AbstractAIAssistant {
+ private static final String BASE_URL_ENV_KEY = "BIGTOP_MANAGER_AI_OPENAI_BASE_URL";
private static final String BASE_URL = "https://api.openai.com";
public OpenAIAssistant(Object memoryId, ChatMemory chatMemory, AIAssistant.Service aiServices) {
@@ -58,6 +60,26 @@ public static Builder builder() {
public static class Builder extends AbstractAIAssistant.Builder {
+ @Override
+ protected String resolveModelsBaseUrl() {
+ Map credentials = config == null ? null : config.getCredentials();
+ if (credentials != null) {
+ String baseUrl = credentials.get("baseUrl");
+ if (baseUrl != null && !baseUrl.isBlank()) {
+ return baseUrl;
+ }
+ }
+ return resolveDefaultBaseUrl();
+ }
+
+ private String resolveDefaultBaseUrl() {
+ String envBaseUrl = System.getenv(BASE_URL_ENV_KEY);
+ if (envBaseUrl != null && !envBaseUrl.isBlank()) {
+ return envBaseUrl;
+ }
+ return BASE_URL;
+ }
+
@Override
public ChatModel getChatModel() {
String model = config.getModel();
@@ -66,8 +88,13 @@ public ChatModel getChatModel() {
Assert.notNull(apiKey, "apiKey must not be null");
OpenAiApi openAiApi =
- OpenAiApi.builder().baseUrl(BASE_URL).apiKey(apiKey).build();
- OpenAiChatOptions options = OpenAiChatOptions.builder().model(model).build();
+ OpenAiApi.builder().baseUrl(resolveDefaultBaseUrl()).apiKey(apiKey).build();
+ OpenAiChatOptions.Builder optionsBuilder = OpenAiChatOptions.builder().model(model);
+ if (mcpAsyncClient != null) {
+ optionsBuilder.toolCallbacks(
+ new org.springframework.ai.mcp.AsyncMcpToolCallbackProvider(mcpAsyncClient).getToolCallbacks());
+ }
+ OpenAiChatOptions options = optionsBuilder.build();
return OpenAiChatModel.builder()
.openAiApi(openAiApi)
.defaultOptions(options)
@@ -132,13 +159,17 @@ public Flux streamChat(String userMessage) {
StringBuilder responseBuilder = new StringBuilder();
return streamingChatModel.stream(prompt)
- .map(chatResponse -> {
- String content =
- chatResponse.getResult().getOutput().getText();
- if (content != null) {
+ .concatMap(chatResponse -> {
+ String content = null;
+ if (chatResponse.getResult() != null
+ && chatResponse.getResult().getOutput() != null) {
+ content = chatResponse.getResult().getOutput().getText();
+ }
+ if (content != null && !content.isEmpty()) {
responseBuilder.append(content);
+ return Flux.just(content);
}
- return content;
+ return Flux.empty();
})
.doOnComplete(() -> {
// Save to memory when streaming completes
diff --git a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/QianFanAssistant.java b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/QianFanAssistant.java
index 468020eb1..f073ba96a 100644
--- a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/QianFanAssistant.java
+++ b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/QianFanAssistant.java
@@ -32,15 +32,20 @@
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
+import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.util.Assert;
import reactor.core.publisher.Flux;
+import com.fasterxml.jackson.databind.JsonNode;
+
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
public class QianFanAssistant extends AbstractAIAssistant {
+ private static final String BASE_URL_ENV_KEY = "BIGTOP_MANAGER_AI_QIANFAN_BASE_URL";
private static final String BASE_URL = "https://qianfan.baidubce.com";
public QianFanAssistant(Object memoryId, ChatMemory chatMemory, AIAssistant.Service aiServices) {
@@ -58,6 +63,59 @@ public static Builder builder() {
public static class Builder extends AbstractAIAssistant.Builder {
+ @Override
+ protected String resolveModelsBaseUrl() {
+ return resolveDefaultBaseUrl();
+ }
+
+ private String resolveDefaultBaseUrl() {
+ String envBaseUrl = System.getenv(BASE_URL_ENV_KEY);
+ if (envBaseUrl != null && !envBaseUrl.isBlank()) {
+ return envBaseUrl;
+ }
+ return BASE_URL;
+ }
+
+ @Override
+ protected String resolveModelsPath() {
+ return "/v2/chat/models";
+ }
+
+ @Override
+ public List getModels() {
+ String apiKey = resolveApiKey(config == null ? null : config.getCredentials());
+ if (apiKey == null || apiKey.isBlank()) {
+ return Collections.emptyList();
+ }
+
+ try {
+ WebClient webClient = WebClient.builder().baseUrl(resolveModelsBaseUrl()).build();
+ JsonNode response = webClient
+ .get()
+ .uri(resolveModelsPath())
+ .header("Authorization", "Bearer " + apiKey)
+ .retrieve()
+ .bodyToMono(JsonNode.class)
+ .timeout(java.time.Duration.ofSeconds(10))
+ .block();
+
+ if (response == null || !response.has("result")) {
+ return Collections.emptyList();
+ }
+
+ List models = new ArrayList<>();
+ for (JsonNode modelNode : response.get("result")) {
+ JsonNode modelId = modelNode.get("model");
+ if (modelId != null && !modelId.isNull()) {
+ models.add(modelId.asText());
+ }
+ }
+ return models;
+ } catch (Exception ignored) {
+ return Collections.emptyList();
+ }
+ }
+
@Override
public ChatModel getChatModel() {
String model = config.getModel();
@@ -66,11 +124,16 @@ public ChatModel getChatModel() {
Assert.notNull(apiKey, "apiKey must not be null");
OpenAiApi openAiApi = OpenAiApi.builder()
- .baseUrl(BASE_URL)
+ .baseUrl(resolveDefaultBaseUrl())
.completionsPath("/v2/chat/completions")
.apiKey(apiKey)
.build();
- OpenAiChatOptions options = OpenAiChatOptions.builder().model(model).build();
+ OpenAiChatOptions.Builder optionsBuilder = OpenAiChatOptions.builder().model(model);
+ if (mcpAsyncClient != null) {
+ optionsBuilder.toolCallbacks(
+ new org.springframework.ai.mcp.AsyncMcpToolCallbackProvider(mcpAsyncClient).getToolCallbacks());
+ }
+ OpenAiChatOptions options = optionsBuilder.build();
return OpenAiChatModel.builder()
.openAiApi(openAiApi)
.defaultOptions(options)
@@ -134,13 +197,17 @@ public Flux streamChat(String userMessage) {
StringBuilder responseBuilder = new StringBuilder();
return streamingChatModel.stream(prompt)
- .map(chatResponse -> {
- String content =
- chatResponse.getResult().getOutput().getText();
- if (content != null) {
+ .concatMap(chatResponse -> {
+ String content = null;
+ if (chatResponse.getResult() != null
+ && chatResponse.getResult().getOutput() != null) {
+ content = chatResponse.getResult().getOutput().getText();
+ }
+ if (content != null && !content.isEmpty()) {
responseBuilder.append(content);
+ return Flux.just(content);
}
- return content;
+ return Flux.empty();
})
.doOnComplete(() -> {
// Save to memory when streaming completes
diff --git a/bigtop-manager-ai/src/test/java/assistant/GeneralAssistantFactoryTest.java b/bigtop-manager-ai/src/test/java/assistant/GeneralAssistantFactoryTest.java
index e87d2c418..a780a65d5 100644
--- a/bigtop-manager-ai/src/test/java/assistant/GeneralAssistantFactoryTest.java
+++ b/bigtop-manager-ai/src/test/java/assistant/GeneralAssistantFactoryTest.java
@@ -64,8 +64,8 @@ void testCreateAIAssistant() {
try (MockedStatic openAIAssistantMockedStatic = mockStatic(OpenAIAssistant.class)) {
openAIAssistantMockedStatic.when(OpenAIAssistant::builder).thenReturn(mockBuilder);
- generalAssistantFactory.createAIService(assistantConfigProvider, null);
- generalAssistantFactory.createForTest(assistantConfigProvider, null);
+ generalAssistantFactory.createAIService(assistantConfigProvider);
+ generalAssistantFactory.createForTest(assistantConfigProvider);
}
}
}
diff --git a/bigtop-manager-ai/src/test/java/org/apache/bigtop/manager/ai/config/McpAsyncClientManagerTest.java b/bigtop-manager-ai/src/test/java/org/apache/bigtop/manager/ai/config/McpAsyncClientManagerTest.java
new file mode 100644
index 000000000..cccc83039
--- /dev/null
+++ b/bigtop-manager-ai/src/test/java/org/apache/bigtop/manager/ai/config/McpAsyncClientManagerTest.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.bigtop.manager.ai.config;
+
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Method;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class McpAsyncClientManagerTest {
+
+ @Test
+ void resolveBaseUrlShouldPreferConfiguredValue() throws Exception {
+ McpAsyncClientManager manager = new McpAsyncClientManager();
+ setField(manager, "clientBaseUrl", "http://127.0.0.1:9000");
+ setField(manager, "serverPort", "8080");
+
+ String baseUrl = invokeResolveBaseUrl(manager);
+ assertEquals("http://127.0.0.1:9000", baseUrl);
+ }
+
+ @Test
+ void resolveBaseUrlShouldFallbackToServerPort() throws Exception {
+ McpAsyncClientManager manager = new McpAsyncClientManager();
+ setField(manager, "clientBaseUrl", " ");
+ setField(manager, "serverPort", "18080");
+
+ String baseUrl = invokeResolveBaseUrl(manager);
+ assertEquals("http://localhost:18080", baseUrl);
+ }
+
+ private String invokeResolveBaseUrl(McpAsyncClientManager manager) throws Exception {
+ Method method = McpAsyncClientManager.class.getDeclaredMethod("resolveBaseUrl");
+ method.setAccessible(true);
+ return (String) method.invoke(manager);
+ }
+
+ private void setField(McpAsyncClientManager manager, String fieldName, String value) throws Exception {
+ var field = McpAsyncClientManager.class.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ field.set(manager, value);
+ }
+}
diff --git a/bigtop-manager-bom/pom.xml b/bigtop-manager-bom/pom.xml
index 878ae561b..8ffc841fc 100644
--- a/bigtop-manager-bom/pom.xml
+++ b/bigtop-manager-bom/pom.xml
@@ -286,6 +286,12 @@
${spring-ai.version}
+
+ org.springframework.ai
+ spring-ai-starter-mcp-client-webflux
+ ${spring-ai.version}
+
+
dev.langchain4j
diff --git a/bigtop-manager-server/pom.xml b/bigtop-manager-server/pom.xml
index e6d3ff384..17cbc5ab1 100644
--- a/bigtop-manager-server/pom.xml
+++ b/bigtop-manager-server/pom.xml
@@ -63,10 +63,6 @@
org.apache.bigtop
bigtop-manager-ai
-
- dev.langchain4j
- langchain4j
-
org.jetbrains
annotations
diff --git a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/controller/LLMConfigController.java b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/controller/LLMConfigController.java
index 141d5341a..f2daf7f0d 100644
--- a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/controller/LLMConfigController.java
+++ b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/controller/LLMConfigController.java
@@ -41,6 +41,7 @@
import jakarta.annotation.Resource;
import java.util.List;
+import java.util.Map;
@Tag(name = "LLM Config Controller")
@RestController
@@ -69,6 +70,15 @@ public ResponseEntity> platformsAuthCredential(
return ResponseEntity.success(llmConfigService.platformsAuthCredentials(platformId));
}
+ @Operation(summary = "list platform models", description = "List models from /v1/models")
+ @PostMapping("/platforms/{platformId}/models")
+ public ResponseEntity> platformModels(
+ @PathVariable(name = "platformId") Long platformId, @RequestBody AuthPlatformReq authPlatformReq) {
+ AuthPlatformDTO authPlatformDTO = AuthPlatformConverter.INSTANCE.fromReq2DTO(authPlatformReq);
+ Map authCredentials = authPlatformDTO.getAuthCredentials();
+ return ResponseEntity.success(llmConfigService.platformModels(platformId, authCredentials));
+ }
+
@Operation(summary = "list auth platforms", description = "List authorized platforms")
@GetMapping("/auth-platforms")
public ResponseEntity> authorizedPlatforms() {
diff --git a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/mcp/tool/StackMcpTool.java b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/mcp/tool/StackMcpTool.java
index f27bf40b5..a90e0a1af 100644
--- a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/mcp/tool/StackMcpTool.java
+++ b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/mcp/tool/StackMcpTool.java
@@ -18,6 +18,7 @@
*/
package org.apache.bigtop.manager.server.mcp.tool;
+import lombok.extern.slf4j.Slf4j;
import org.apache.bigtop.manager.server.mcp.converter.JsonToolCallResultConverter;
import org.apache.bigtop.manager.server.model.converter.ServiceConverter;
import org.apache.bigtop.manager.server.model.converter.StackConverter;
@@ -34,6 +35,7 @@
import java.util.Map;
@Component
+@Slf4j
public class StackMcpTool implements McpTool {
@Tool(
@@ -51,7 +53,7 @@ public List listStacks() {
stackVO.setServices(ServiceConverter.INSTANCE.fromDTO2VO(serviceDTOList));
stackVOList.add(stackVO);
}
-
+ log.info("ListStacks tool called, total stacks: {}", stackVOList.size());
return stackVOList;
}
}
diff --git a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/model/converter/AuthPlatformConverter.java b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/model/converter/AuthPlatformConverter.java
index 329823f36..5b30d61c3 100644
--- a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/model/converter/AuthPlatformConverter.java
+++ b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/model/converter/AuthPlatformConverter.java
@@ -54,6 +54,7 @@ default Map mapAuthCredentials(List authCrede
return null;
}
return authCredentials.stream()
+ .filter(item -> item != null && item.getKey() != null)
.collect(Collectors.toMap(AuthCredentialReq::getKey, AuthCredentialReq::getValue));
}
diff --git a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/LLMConfigService.java b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/LLMConfigService.java
index 223b5723c..3a3dd1d33 100644
--- a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/LLMConfigService.java
+++ b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/LLMConfigService.java
@@ -24,6 +24,7 @@
import org.apache.bigtop.manager.server.model.vo.PlatformVO;
import java.util.List;
+import java.util.Map;
public interface LLMConfigService {
@@ -48,4 +49,6 @@ public interface LLMConfigService {
AuthPlatformVO getAuthorizedPlatform(Long authId);
PlatformVO getPlatform(Long id);
+
+ List platformModels(Long platformId, Map authCredentials);
}
diff --git a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/ChatbotServiceImpl.java b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/ChatbotServiceImpl.java
index 45ae57aec..c592bcd28 100644
--- a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/ChatbotServiceImpl.java
+++ b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/ChatbotServiceImpl.java
@@ -45,7 +45,6 @@
import org.apache.bigtop.manager.server.model.vo.ChatThreadVO;
import org.apache.bigtop.manager.server.model.vo.TalkVO;
import org.apache.bigtop.manager.server.service.ChatbotService;
-import org.apache.bigtop.manager.server.tools.provider.AIServiceToolsProvider;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Service;
@@ -74,9 +73,6 @@ public class ChatbotServiceImpl implements ChatbotService {
@Resource
private ChatMessageDao chatMessageDao;
- @Resource
- private AIServiceToolsProvider aiServiceToolsProvider;
-
@Resource
private AIAssistantFactory aiAssistantFactory;
@@ -235,8 +231,7 @@ private PlatformType getPlatformType(String platformName) {
private AIAssistant buildAIAssistant(
String platformName, String model, Map credentials, Long threadId, ChatbotCommand command) {
return aiAssistantFactory.createAIService(
- getAIAssistantConfig(platformName, model, credentials, threadId),
- aiServiceToolsProvider.getToolsProvide(command));
+ getAIAssistantConfig(platformName, model, credentials, threadId));
}
private AIAssistant prepareTalk(Long threadId, ChatbotCommand command) {
@@ -273,7 +268,7 @@ private void sendTalkVO(SseEmitter emitter, String content, String finishReason)
private void handleError(SseEmitter emitter, Throwable throwable) {
log.error("Error during SSE streaming: {}", throwable.getMessage(), throwable);
sendTalkVO(emitter, null, "Error: " + throwable.getMessage());
- emitter.completeWithError(throwable);
+ emitter.complete();
}
private void completeEmitter(SseEmitter emitter) {
diff --git a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/LLMConfigServiceImpl.java b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/LLMConfigServiceImpl.java
index eaf46eae4..8c8b4ab38 100644
--- a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/LLMConfigServiceImpl.java
+++ b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/LLMConfigServiceImpl.java
@@ -47,15 +47,11 @@
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Service;
-import dev.langchain4j.agent.tool.ToolSpecification;
-import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
-import dev.langchain4j.service.tool.ToolExecutor;
-import dev.langchain4j.service.tool.ToolProvider;
-import dev.langchain4j.service.tool.ToolProviderResult;
import lombok.extern.slf4j.Slf4j;
import jakarta.annotation.Resource;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -78,15 +74,60 @@ public class LLMConfigServiceImpl implements LLMConfigService {
@Resource
private AIAssistantFactory aiAssistantFactory;
- private static final String TEST_FLAG = "ZmxhZw==";
- private static final String TEST_KEY = "bm";
-
@Override
public List platforms() {
List platformPOs = platformDao.findAll();
return PlatformConverter.INSTANCE.fromPO2VO(platformPOs);
}
+ private List getDynamicModels(PlatformPO platformPO, Map explicitCreds) {
+ PlatformType platformType = PlatformType.getPlatformType(platformPO.getName().toLowerCase());
+ if (platformType == null) {
+ return getDefaultModels(platformPO);
+ }
+
+ Map creds = new HashMap<>();
+
+ if (explicitCreds != null && !explicitCreds.isEmpty()) {
+ creds.putAll(explicitCreds);
+ } else {
+ List authPlatformPOs = authPlatformDao.findAll();
+ for (AuthPlatformPO auth : authPlatformPOs) {
+ if (auth.getPlatformId().equals(platformPO.getId()) && !auth.getIsDeleted()) {
+ creds = AuthPlatformConverter.INSTANCE.fromPO2DTO(auth).getAuthCredentials();
+ break;
+ }
+ }
+ }
+
+ GeneralAssistantConfig config = GeneralAssistantConfig.builder()
+ .setPlatformType(platformType)
+ .setModel("dummy")
+ .addCredentials(creds)
+ .build();
+
+ List models = aiAssistantFactory.getModels(config);
+ if (models != null && !models.isEmpty()) {
+ return models;
+ }
+
+ return getDefaultModels(platformPO);
+ }
+
+ private List getDefaultModels(PlatformPO platformPO) {
+ String supportModels = platformPO.getSupportModels();
+ if (supportModels == null || supportModels.isBlank()) {
+ return Collections.emptyList();
+ }
+ return List.of(supportModels.split(","));
+ }
+
+ @Override
+ public List platformModels(Long platformId, Map authCredentials) {
+ PlatformPO platformPO = validateAndGetPlatform(platformId);
+ return getDynamicModels(platformPO, authCredentials);
+ }
+
@Override
public List platformsAuthCredentials(Long platformId) {
PlatformPO platformPO = platformDao.findById(platformId);
@@ -164,11 +205,6 @@ public boolean testAuthorizedPlatform(AuthPlatformDTO authPlatformDTO) {
PlatformPO platformPO = validateAndGetPlatform(authPlatformDTO.getPlatformId());
- List supportModels = List.of(platformPO.getSupportModels().split(","));
- if (supportModels.isEmpty() || !supportModels.contains(authPlatformDTO.getModel())) {
- throw new ApiException(ApiExceptionEnum.MODEL_NOT_SUPPORTED);
- }
-
if (authPlatformDTO.getId() != null) {
AuthPlatformPO authPlatformPO = validateAndGetAuthPlatform(authPlatformDTO.getId());
@@ -180,6 +216,11 @@ public boolean testAuthorizedPlatform(AuthPlatformDTO authPlatformDTO) {
Map credentialSet =
getStringMap(authPlatformDTO, PlatformConverter.INSTANCE.fromPO2DTO(platformPO));
+ List dynamicModels = getDynamicModels(platformPO, credentialSet);
+ if (dynamicModels.isEmpty() || !dynamicModels.contains(authPlatformDTO.getModel())) {
+ throw new ApiException(ApiExceptionEnum.MODEL_NOT_SUPPORTED);
+ }
+
if (!testAuthorization(platformPO.getName(), authPlatformDTO.getModel(), credentialSet)) {
throw new ApiException(ApiExceptionEnum.CREDIT_INCORRECT);
}
@@ -260,7 +301,6 @@ public AuthPlatformVO getAuthorizedPlatform(Long authId) {
@Override
public PlatformVO getPlatform(Long id) {
PlatformPO platformPO = validateAndGetPlatform(id);
-
return PlatformConverter.INSTANCE.fromPO2VO(platformPO);
}
@@ -302,10 +342,8 @@ private PlatformType getPlatformType(String platformName) {
}
private Boolean testAuthorization(String platformName, String model, Map credentials) {
- Boolean result = testFuncCalling(platformName, model, credentials);
- log.info("Test func calling result: {}", result);
GeneralAssistantConfig generalAssistantConfig = getAIAssistantConfig(platformName, model, credentials);
- AIAssistant aiAssistant = aiAssistantFactory.createForTest(generalAssistantConfig, null);
+ AIAssistant aiAssistant = aiAssistantFactory.createForTest(generalAssistantConfig);
try {
return aiAssistant.test();
} catch (Exception e) {
@@ -313,40 +351,6 @@ private Boolean testAuthorization(String platformName, String model, Map credentials) {
- ToolProvider toolProvider = (toolProviderRequest) -> {
- ToolSpecification toolSpecification = ToolSpecification.builder()
- .name("getFlag")
- .description("Get flag based on key")
- .parameters(JsonObjectSchema.builder()
- .addStringProperty("key")
- .description("Lowercase key to get flag")
- .build())
- .build();
- ToolExecutor toolExecutor = (toolExecutionRequest, memoryId) -> {
- Map arguments = JsonUtils.readFromString(toolExecutionRequest.arguments());
- String key = arguments.get("key").toString();
- if (key.equals(TEST_KEY)) {
- return TEST_FLAG;
- }
- return null;
- };
-
- return ToolProviderResult.builder()
- .add(toolSpecification, toolExecutor)
- .build();
- };
-
- GeneralAssistantConfig generalAssistantConfig = getAIAssistantConfig(platformName, model, credentials);
- AIAssistant aiAssistant = aiAssistantFactory.createForTest(generalAssistantConfig, toolProvider);
- try {
- return aiAssistant.ask("What is the flag of " + TEST_KEY).contains(TEST_FLAG);
- } catch (Exception e) {
- log.error("Test function calling failed", e);
- return false;
- }
- }
-
private void switchActivePlatform(Long id) {
List authPlatformPOS = authPlatformDao.findAll();
for (AuthPlatformPO authPlatformPO : authPlatformPOS) {
diff --git a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/functions/ClusterFunctions.java b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/functions/ClusterFunctions.java
deleted file mode 100644
index 0978164da..000000000
--- a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/functions/ClusterFunctions.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you 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 org.apache.bigtop.manager.server.tools.functions;
-
-import org.apache.bigtop.manager.common.utils.JsonUtils;
-import org.apache.bigtop.manager.server.model.vo.ClusterVO;
-import org.apache.bigtop.manager.server.service.ClusterService;
-
-import org.springframework.stereotype.Component;
-
-import dev.langchain4j.agent.tool.ToolSpecification;
-import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
-import dev.langchain4j.service.tool.ToolExecutor;
-import lombok.extern.slf4j.Slf4j;
-
-import jakarta.annotation.Resource;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-@Component
-@Slf4j
-public class ClusterFunctions {
- @Resource
- private ClusterService clusterService;
-
- public Map listCluster() {
- ToolSpecification toolSpecification = ToolSpecification.builder()
- .name("listCluster")
- .description("Get cluster list")
- .build();
- ToolExecutor toolExecutor =
- (toolExecutionRequest, memoryId) -> JsonUtils.indentWriteAsString(clusterService.list());
-
- return Map.of(toolSpecification, toolExecutor);
- }
-
- public Map getClusterById() {
- ToolSpecification toolSpecification = ToolSpecification.builder()
- .name("getClusterById")
- .description("Get cluster information based on ID")
- .parameters(JsonObjectSchema.builder()
- .description("Cluster ID")
- .addNumberProperty("clusterId")
- .build())
- .build();
- ToolExecutor toolExecutor = (toolExecutionRequest, memoryId) -> {
- Map arguments = JsonUtils.readFromString(toolExecutionRequest.arguments());
- Long clusterId = Long.valueOf(arguments.get("clusterId").toString());
- ClusterVO clusterVO = clusterService.get(clusterId);
- if (clusterVO == null) {
- return "Cluster not found";
- }
- return JsonUtils.indentWriteAsString(clusterVO);
- };
-
- return Map.of(toolSpecification, toolExecutor);
- }
-
- public Map getClusterByName() {
- ToolSpecification toolSpecification = ToolSpecification.builder()
- .name("getClusterByName")
- .description("Get cluster information based on cluster name")
- .parameters(JsonObjectSchema.builder()
- .description("Cluster name")
- .addStringProperty("clusterName")
- .build())
- .build();
- ToolExecutor toolExecutor = (toolExecutionRequest, memoryId) -> {
- Map arguments = JsonUtils.readFromString(toolExecutionRequest.arguments());
- String clusterName = arguments.get("clusterName").toString();
- List clusterVOS = clusterService.list();
- for (ClusterVO clusterVO : clusterVOS) {
- if (clusterVO.getName().equals(clusterName)) {
- return JsonUtils.indentWriteAsString(clusterVO);
- }
- }
- return "Cluster not found";
- };
-
- return Map.of(toolSpecification, toolExecutor);
- }
-
- public Map getAllFunctions() {
- Map functions = new HashMap<>();
- functions.putAll(listCluster());
- functions.putAll(getClusterById());
- functions.putAll(getClusterByName());
- return functions;
- }
-}
diff --git a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/functions/HostFunctions.java b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/functions/HostFunctions.java
deleted file mode 100644
index b14b7327b..000000000
--- a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/functions/HostFunctions.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you 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 org.apache.bigtop.manager.server.tools.functions;
-
-import org.apache.bigtop.manager.common.utils.JsonUtils;
-import org.apache.bigtop.manager.dao.query.HostQuery;
-import org.apache.bigtop.manager.server.model.vo.HostVO;
-import org.apache.bigtop.manager.server.model.vo.PageVO;
-import org.apache.bigtop.manager.server.service.HostService;
-
-import org.springframework.stereotype.Component;
-
-import dev.langchain4j.agent.tool.ToolSpecification;
-import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
-import dev.langchain4j.service.tool.ToolExecutor;
-import lombok.extern.slf4j.Slf4j;
-
-import jakarta.annotation.Resource;
-import java.util.HashMap;
-import java.util.Map;
-
-@Component
-@Slf4j
-public class HostFunctions {
- @Resource
- private HostService hostService;
-
- public Map getHostById() {
- ToolSpecification toolSpecification = ToolSpecification.builder()
- .name("getHostById")
- .description("Get host information based on ID")
- .parameters(JsonObjectSchema.builder()
- .description("Host ID")
- .addNumberProperty("hostId")
- .build())
- .build();
- ToolExecutor toolExecutor = (toolExecutionRequest, memoryId) -> {
- Map arguments = JsonUtils.readFromString(toolExecutionRequest.arguments());
- Long hostId = Long.valueOf(arguments.get("hostId").toString());
- HostVO hostVO = hostService.get(hostId);
- if (hostVO == null) {
- return "Host not found";
- }
- return JsonUtils.indentWriteAsString(hostVO);
- };
-
- return Map.of(toolSpecification, toolExecutor);
- }
-
- public Map getHostByName() {
- ToolSpecification toolSpecification = ToolSpecification.builder()
- .name("getHostByName")
- .description("Get host information based on cluster name")
- .parameters(JsonObjectSchema.builder()
- .description("Host name")
- .addStringProperty("hostName")
- .build())
- .build();
- ToolExecutor toolExecutor = (toolExecutionRequest, memoryId) -> {
- Map arguments = JsonUtils.readFromString(toolExecutionRequest.arguments());
- String hostName = arguments.get("hostName").toString();
- HostQuery hostQuery = new HostQuery();
- hostQuery.setHostname(hostName);
- PageVO hostVO = hostService.list(hostQuery);
- return JsonUtils.indentWriteAsString(hostVO);
- };
-
- return Map.of(toolSpecification, toolExecutor);
- }
-
- public Map getAllFunctions() {
- Map functions = new HashMap<>();
- functions.putAll(getHostById());
- functions.putAll(getHostByName());
- return functions;
- }
-}
diff --git a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/functions/StackFunctions.java b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/functions/StackFunctions.java
deleted file mode 100644
index 93215f99b..000000000
--- a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/functions/StackFunctions.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you 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 org.apache.bigtop.manager.server.tools.functions;
-
-import org.apache.bigtop.manager.common.utils.JsonUtils;
-import org.apache.bigtop.manager.server.model.vo.PropertyVO;
-import org.apache.bigtop.manager.server.model.vo.ServiceConfigVO;
-import org.apache.bigtop.manager.server.model.vo.ServiceVO;
-import org.apache.bigtop.manager.server.model.vo.StackVO;
-import org.apache.bigtop.manager.server.service.StackService;
-
-import org.springframework.stereotype.Component;
-
-import dev.langchain4j.agent.tool.ToolSpecification;
-import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
-import dev.langchain4j.service.tool.ToolExecutor;
-import lombok.extern.slf4j.Slf4j;
-
-import jakarta.annotation.Resource;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-@Component
-@Slf4j
-public class StackFunctions {
- @Resource
- private StackService stackService;
-
- public Map listStackAndService() {
- ToolSpecification toolSpecification = ToolSpecification.builder()
- .name("listStackAndService")
- .description("Retrieve the list of services in each stack")
- .build();
- ToolExecutor toolExecutor = (toolExecutionRequest, memoryId) -> {
- Map> stackInfo = new HashMap<>();
- for (StackVO stackVO : stackService.list()) {
- List services = new ArrayList<>();
- for (ServiceVO serviceVO : stackVO.getServices()) {
- services.add(serviceVO.getName());
- }
- stackInfo.put(stackVO.getStackName(), services);
- }
- return JsonUtils.indentWriteAsString(stackInfo);
- };
- return Map.of(toolSpecification, toolExecutor);
- }
-
- public Map getServiceByName() {
- ToolSpecification toolSpecification = ToolSpecification.builder()
- .name("getServiceByName")
- .description("Get service information and configs based on service name")
- .parameters(JsonObjectSchema.builder()
- .addStringProperty("serviceName")
- .description("Service name")
- .build())
- .build();
- ToolExecutor toolExecutor = (toolExecutionRequest, memoryId) -> {
- Map arguments = JsonUtils.readFromString(toolExecutionRequest.arguments());
- String serviceName = arguments.get("serviceName").toString();
- for (StackVO stackVO : stackService.list()) {
- for (ServiceVO serviceVO : stackVO.getServices()) {
- if (serviceVO.getName().equals(serviceName)) {
- for (ServiceConfigVO serviceConfigVO : serviceVO.getConfigs()) {
- for (PropertyVO propertyVO : serviceConfigVO.getProperties()) {
- if (propertyVO.getName().equals("content")) {
- propertyVO.setValue(null);
- }
- if (propertyVO.getAttrs() != null
- && propertyVO.getAttrs().getType().equals("longtext")) {
- propertyVO.setValue(null);
- }
- }
- }
- return JsonUtils.indentWriteAsString(serviceVO);
- }
- }
- }
- return "Service not found";
- };
-
- return Map.of(toolSpecification, toolExecutor);
- }
-
- public Map getAllFunctions() {
- Map functions = new HashMap<>();
- functions.putAll(listStackAndService());
- functions.putAll(getServiceByName());
- return functions;
- }
-}
diff --git a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/provider/AIServiceToolsProvider.java b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/provider/AIServiceToolsProvider.java
deleted file mode 100644
index 72632ba3d..000000000
--- a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/provider/AIServiceToolsProvider.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you 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 org.apache.bigtop.manager.server.tools.provider;
-
-import org.apache.bigtop.manager.server.enums.ChatbotCommand;
-
-import org.springframework.stereotype.Component;
-
-import dev.langchain4j.service.tool.ToolProvider;
-
-import jakarta.annotation.Resource;
-
-@Component
-public class AIServiceToolsProvider {
- @Resource
- private InfoToolsProvider infoToolsProvider;
-
- public ToolProvider getToolsProvide(ChatbotCommand chatbotCommand) {
- if (ChatbotCommand.INFO.equals(chatbotCommand)) {
- return infoToolsProvider;
- }
- return null;
- }
-}
diff --git a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/provider/InfoToolsProvider.java b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/provider/InfoToolsProvider.java
deleted file mode 100644
index 36ec1f92b..000000000
--- a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/tools/provider/InfoToolsProvider.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you 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 org.apache.bigtop.manager.server.tools.provider;
-
-import org.apache.bigtop.manager.server.tools.functions.ClusterFunctions;
-import org.apache.bigtop.manager.server.tools.functions.HostFunctions;
-import org.apache.bigtop.manager.server.tools.functions.StackFunctions;
-
-import org.springframework.stereotype.Component;
-
-import dev.langchain4j.service.tool.ToolProvider;
-import dev.langchain4j.service.tool.ToolProviderRequest;
-import dev.langchain4j.service.tool.ToolProviderResult;
-import lombok.extern.slf4j.Slf4j;
-
-import jakarta.annotation.Resource;
-
-@Component
-@Slf4j
-public class InfoToolsProvider implements ToolProvider {
- @Resource
- private ClusterFunctions clusterFunctions;
-
- @Resource
- private HostFunctions hostFunctions;
-
- @Resource
- private StackFunctions stackFunctions;
-
- @Override
- public ToolProviderResult provideTools(ToolProviderRequest toolProviderRequest) {
- return ToolProviderResult.builder()
- .addAll(clusterFunctions.getAllFunctions())
- .addAll(hostFunctions.getAllFunctions())
- .addAll(stackFunctions.getAllFunctions())
- .build();
- }
-}
diff --git a/bigtop-manager-server/src/main/resources/application.yml b/bigtop-manager-server/src/main/resources/application.yml
index 860573b97..53934fa5d 100644
--- a/bigtop-manager-server/src/main/resources/application.yml
+++ b/bigtop-manager-server/src/main/resources/application.yml
@@ -25,6 +25,10 @@ spring:
type: ASYNC
sse-endpoint: /mcp/sse
sse-message-endpoint: /mcp/messages
+ client:
+ enabled: true
+ base-url: http://localhost:8080
+ sse-endpoint: /mcp/sse
banner:
charset: utf-8
application:
@@ -67,4 +71,4 @@ springdoc:
pagehelper:
reasonable: false
params: count=countSql
- support-methods-arguments: true
\ No newline at end of file
+ support-methods-arguments: true
diff --git a/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/tools/functions/ClusterFunctionsTest.java b/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/tools/functions/ClusterFunctionsTest.java
deleted file mode 100644
index 6a522da71..000000000
--- a/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/tools/functions/ClusterFunctionsTest.java
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you 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 org.apache.bigtop.manager.server.tools.functions;
-
-import org.apache.bigtop.manager.server.model.vo.ClusterVO;
-import org.apache.bigtop.manager.server.service.ClusterService;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-import dev.langchain4j.agent.tool.ToolExecutionRequest;
-import dev.langchain4j.agent.tool.ToolSpecification;
-import dev.langchain4j.service.tool.ToolExecutor;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-@ExtendWith(MockitoExtension.class)
-class ClusterFunctionsTest {
-
- @Mock
- private ClusterService clusterService;
-
- @InjectMocks
- private ClusterFunctions clusterFunctions;
-
- private ClusterVO testCluster;
-
- @BeforeEach
- void setUp() {
- testCluster = new ClusterVO();
- testCluster.setId(1L);
- testCluster.setName("test-cluster");
- }
-
- @Test
- void testListCluster() {
- // Mock clusterService response
- when(clusterService.list()).thenReturn(Collections.singletonList(testCluster));
-
- // Get the tool specification and executor
- Map tools = clusterFunctions.listCluster();
- assertEquals(1, tools.size());
-
- ToolSpecification spec = tools.keySet().iterator().next();
- ToolExecutor executor = tools.get(spec);
-
- // Execute the tool
- String result = executor.execute(ToolExecutionRequest.builder().build(), "memoryId");
-
- // Verify results
- assertTrue(result.contains("test-cluster"));
- verify(clusterService, times(1)).list();
- }
-
- @Test
- void testGetClusterById() {
- // Mock clusterService response
- when(clusterService.get(1L)).thenReturn(testCluster);
-
- // Get the tool specification and executor
- Map tools = clusterFunctions.getClusterById();
- assertEquals(1, tools.size());
-
- ToolSpecification spec = tools.keySet().iterator().next();
- ToolExecutor executor = tools.get(spec);
-
- // Build request with arguments
- String arguments = "{\"clusterId\": 1}";
- ToolExecutionRequest request =
- ToolExecutionRequest.builder().arguments(arguments).build();
-
- // Execute the tool
- String result = executor.execute(request, "memoryId");
-
- // Verify results
- assertTrue(result.contains("test-cluster"));
- verify(clusterService, times(1)).get(1L);
- }
-
- @Test
- void testGetClusterByIdWhenNotExists() {
- // Mock clusterService response
- when(clusterService.get(999L)).thenReturn(null);
-
- // Get the tool specification and executor
- Map tools = clusterFunctions.getClusterById();
- ToolExecutor executor = tools.values().iterator().next();
-
- // Build request with arguments
- String arguments = "{\"clusterId\": 999}";
- ToolExecutionRequest request =
- ToolExecutionRequest.builder().arguments(arguments).build();
-
- // Execute the tool
- String result = executor.execute(request, "memoryId");
-
- // Verify results
- assertEquals("Cluster not found", result);
- }
-
- @Test
- void testGetClusterByName() {
- // Mock clusterService response
- when(clusterService.list()).thenReturn(Collections.singletonList(testCluster));
-
- // Get the tool specification and executor
- Map tools = clusterFunctions.getClusterByName();
- ToolExecutor executor = tools.values().iterator().next();
-
- // Build request with arguments
- String arguments = "{\"clusterName\": \"test-cluster\"}";
- ToolExecutionRequest request =
- ToolExecutionRequest.builder().arguments(arguments).build();
-
- // Execute the tool
- String result = executor.execute(request, "memoryId");
-
- // Verify results
- assertTrue(result.contains("test-cluster"));
- verify(clusterService, times(1)).list();
- }
-
- @Test
- void testGetClusterByNameWhenNotExists() {
- // Mock clusterService response
- when(clusterService.list()).thenReturn(Collections.singletonList(testCluster));
-
- // Get the tool specification and executor
- Map tools = clusterFunctions.getClusterByName();
- ToolExecutor executor = tools.values().iterator().next();
-
- // Build request with arguments
- String arguments = "{\"clusterName\": \"non-existent\"}";
- ToolExecutionRequest request =
- ToolExecutionRequest.builder().arguments(arguments).build();
-
- // Execute the tool
- String result = executor.execute(request, "memoryId");
-
- // Verify results
- assertEquals("Cluster not found", result);
- }
-
- @Test
- void testGetAllFunctions() {
- Map functions = clusterFunctions.getAllFunctions();
- assertEquals(3, functions.size());
-
- List expectedToolNames = List.of("listCluster", "getClusterById", "getClusterByName");
- assertTrue(functions.keySet().stream().map(ToolSpecification::name).allMatch(expectedToolNames::contains));
- }
-}
diff --git a/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/tools/functions/HostFunctionsTest.java b/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/tools/functions/HostFunctionsTest.java
deleted file mode 100644
index b73996203..000000000
--- a/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/tools/functions/HostFunctionsTest.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you 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 org.apache.bigtop.manager.server.tools.functions;
-
-import org.apache.bigtop.manager.dao.query.HostQuery;
-import org.apache.bigtop.manager.server.model.vo.HostVO;
-import org.apache.bigtop.manager.server.model.vo.PageVO;
-import org.apache.bigtop.manager.server.service.HostService;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-import dev.langchain4j.agent.tool.ToolExecutionRequest;
-import dev.langchain4j.agent.tool.ToolSpecification;
-import dev.langchain4j.model.chat.request.json.JsonSchemaElement;
-import dev.langchain4j.service.tool.ToolExecutor;
-
-import java.util.List;
-import java.util.Map;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.Mockito.when;
-
-@ExtendWith(MockitoExtension.class)
-class HostFunctionsTest {
-
- @Mock
- private HostService hostService;
-
- @InjectMocks
- private HostFunctions hostFunctions;
-
- private HostVO testHost;
- private PageVO testPage;
-
- @BeforeEach
- void setUp() {
- testHost = new HostVO();
- testHost.setId(1L);
- testHost.setHostname("test-host");
-
- testPage = new PageVO<>();
- testPage.setContent(List.of(testHost));
- testPage.setTotal(1L);
- }
-
- @Test
- void testGetHostByIdToolSpecification() {
- Map tools = hostFunctions.getHostById();
- assertEquals(1, tools.size());
-
- ToolSpecification spec = tools.keySet().iterator().next();
- Map params = spec.parameters().properties();
-
- assertEquals(1, params.size());
- assertTrue(params.containsKey("hostId"));
- }
-
- @Test
- void testGetHostByIdExecutorFound() throws Exception {
- when(hostService.get(1L)).thenReturn(testHost);
-
- Map tools = hostFunctions.getHostById();
- ToolExecutor executor = tools.values().iterator().next();
-
- String arguments = "{\"hostId\": 1}";
- String result = executor.execute(
- ToolExecutionRequest.builder().arguments(arguments).build(), null);
-
- // Use system-independent newline character regex
- String expectedPattern = ".*\"hostname\"\\s*:\\s*\"test-host\".*";
- assertTrue(
- result.replaceAll("\\R", System.lineSeparator()).matches("(?s)" + expectedPattern),
- "Hostname should match with any line separators");
- }
-
- @Test
- void testGetHostByIdExecutorNotFound() {
- when(hostService.get(anyLong())).thenReturn(null);
-
- Map tools = hostFunctions.getHostById();
- ToolExecutor executor = tools.values().iterator().next();
-
- String arguments = "{\"hostId\": 999}";
- String result = executor.execute(
- ToolExecutionRequest.builder().arguments(arguments).build(), null);
-
- assertEquals("Host not found", result);
- }
-
- @Test
- void testGetHostByNameToolSpecification() {
- Map tools = hostFunctions.getHostByName();
- assertEquals(1, tools.size());
-
- ToolSpecification spec = tools.keySet().iterator().next();
- assertEquals("getHostByName", spec.name());
- assertEquals("Get host information based on cluster name", spec.description());
- Map params = spec.parameters().properties();
- assertEquals(1, params.size());
- assertTrue(params.containsKey("hostName"));
- }
-
- @Test
- void testGetHostByNameExecutor() {
- HostQuery query = new HostQuery();
- query.setHostname("test-host");
- when(hostService.list(query)).thenReturn(testPage);
-
- Map tools = hostFunctions.getHostByName();
- ToolExecutor executor = tools.values().iterator().next();
-
- String arguments = "{\"hostName\":\"test-host\"}";
- String result = executor.execute(
- ToolExecutionRequest.builder().arguments(arguments).build(), null);
-
- // System-independent matching pattern
- String totalPattern = "(?s).*\"total\"\\s*:\\s*1.*";
- String hostPattern = "(?s).*\"hostname\"\\s*:\\s*\"test-host\".*";
- assertTrue(result.matches(totalPattern), "Should contain total=1");
- assertTrue(result.matches(hostPattern), "Should contain hostname=test-host");
- }
-
- @Test
- void testGetAllFunctions() {
- Map functions = hostFunctions.getAllFunctions();
- assertEquals(2, functions.size());
- assertTrue(functions.keySet().stream().anyMatch(s -> s.name().equals("getHostById")));
- assertTrue(functions.keySet().stream().anyMatch(s -> s.name().equals("getHostByName")));
- }
-}
diff --git a/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/tools/functions/StackFunctionsTest.java b/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/tools/functions/StackFunctionsTest.java
deleted file mode 100644
index 2324ac309..000000000
--- a/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/tools/functions/StackFunctionsTest.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you 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 org.apache.bigtop.manager.server.tools.functions;
-
-import org.apache.bigtop.manager.server.model.vo.AttrsVO;
-import org.apache.bigtop.manager.server.model.vo.PropertyVO;
-import org.apache.bigtop.manager.server.model.vo.ServiceConfigVO;
-import org.apache.bigtop.manager.server.model.vo.ServiceVO;
-import org.apache.bigtop.manager.server.model.vo.StackVO;
-import org.apache.bigtop.manager.server.service.StackService;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-import dev.langchain4j.agent.tool.ToolExecutionRequest;
-import dev.langchain4j.agent.tool.ToolSpecification;
-import dev.langchain4j.model.chat.request.json.JsonSchemaElement;
-import dev.langchain4j.service.tool.ToolExecutor;
-
-import java.util.List;
-import java.util.Map;
-
-import static org.junit.jupiter.api.Assertions.assertAll;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.Mockito.when;
-
-@ExtendWith(MockitoExtension.class)
-class StackFunctionsTest {
-
- @Mock
- private StackService stackService;
-
- @InjectMocks
- private StackFunctions stackFunctions;
-
- private StackVO testStack;
-
- @BeforeEach
- void setUp() {
- // Initialize test data
- testStack = new StackVO();
- testStack.setStackName("test-stack");
-
- ServiceVO testService = new ServiceVO();
- testService.setName("test-service");
-
- ServiceConfigVO config = new ServiceConfigVO();
- PropertyVO normalProp = new PropertyVO();
- normalProp.setName("normal");
- normalProp.setValue("value");
- PropertyVO contentProp = new PropertyVO();
- contentProp.setName("content");
- contentProp.setValue("secret");
- PropertyVO longtextProp = new PropertyVO();
- longtextProp.setName("longtext");
- longtextProp.setValue("very long text");
- AttrsVO attrs = new AttrsVO();
- attrs.setType("longtext");
- longtextProp.setAttrs(attrs);
-
- config.setProperties(List.of(normalProp, contentProp, longtextProp));
- testService.setConfigs(List.of(config));
-
- testStack.setServices(List.of(testService));
- }
-
- @Test
- void testListStackAndService() {
- // Mock service layer return data
- when(stackService.list()).thenReturn(List.of(testStack));
-
- // Get tool
- Map tools = stackFunctions.listStackAndService();
- assertEquals(1, tools.size());
-
- // Validate tool specification
- ToolSpecification spec = tools.keySet().iterator().next();
- assertEquals("listStackAndService", spec.name());
- assertEquals("Retrieve the list of services in each stack", spec.description());
-
- // Execute tool
- ToolExecutor executor = tools.values().iterator().next();
- String result =
- executor.execute(ToolExecutionRequest.builder().arguments("{}").build(), null);
-
- // Validate result
- String expectedJson =
- """
- {
- "test-stack": ["test-service"]
- }""";
- assertEquals(expectedJson.replaceAll("\\s", ""), result.replaceAll("\\s", ""));
- }
-
- @Test
- void testGetServiceByNameFound() {
- // Mock service layer return data
- when(stackService.list()).thenReturn(List.of(testStack));
-
- // Get tool
- Map tools = stackFunctions.getServiceByName();
- ToolExecutor executor = tools.values().iterator().next();
-
- // Execute query
- String arguments = "{\"serviceName\" : \"test-service\"}";
- String result = executor.execute(
- ToolExecutionRequest.builder().arguments(arguments).build(), null);
-
- // Validate result
- assertAll(
- () -> assertTrue(result.contains("\"name\" : \"test-service\"")),
- () -> assertTrue(result.contains("\"name\" : \"normal\"")),
- () -> assertTrue(result.contains("\"name\" : \"content\"")),
- () -> assertTrue(result.contains("\"name\" : \"longtext\"")));
- }
-
- @Test
- void testGetServiceByNameNotFound() {
- when(stackService.list()).thenReturn(List.of(testStack));
-
- Map tools = stackFunctions.getServiceByName();
- ToolExecutor executor = tools.values().iterator().next();
-
- String arguments = "{\"serviceName\":\"non-existent\"}";
- String result = executor.execute(
- ToolExecutionRequest.builder().arguments(arguments).build(), null);
-
- assertEquals("Service not found", result);
- }
-
- @Test
- void testGetServiceByNameToolSpecification() {
- Map tools = stackFunctions.getServiceByName();
- ToolSpecification spec = tools.keySet().iterator().next();
- Map params = spec.parameters().properties();
- assertAll(
- () -> assertEquals("getServiceByName", spec.name()),
- () -> assertEquals("Get service information and configs based on service name", spec.description()),
- () -> assertEquals(1, params.size()),
- () -> assertTrue(params.containsKey("serviceName")));
- }
-
- @Test
- void testGetAllFunctions() {
- Map functions = stackFunctions.getAllFunctions();
- assertEquals(2, functions.size());
- assertTrue(functions.keySet().stream().anyMatch(s -> s.name().equals("listStackAndService")));
- assertTrue(functions.keySet().stream().anyMatch(s -> s.name().equals("getServiceByName")));
- }
-}
diff --git a/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/tools/provider/AIServiceToolsProviderTest.java b/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/tools/provider/AIServiceToolsProviderTest.java
deleted file mode 100644
index 475dfe3f6..000000000
--- a/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/tools/provider/AIServiceToolsProviderTest.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you 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 org.apache.bigtop.manager.server.tools.provider;
-
-import org.apache.bigtop.manager.server.enums.ChatbotCommand;
-
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-import dev.langchain4j.service.tool.ToolProvider;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-
-@ExtendWith(MockitoExtension.class)
-public class AIServiceToolsProviderTest {
-
- @Mock
- private InfoToolsProvider infoToolsProvider;
-
- @InjectMocks
- private AIServiceToolsProvider aiServiceToolsProvider;
-
- @Test
- public void testGetToolsProvideWithInfoCommand() {
- ToolProvider toolProvider = aiServiceToolsProvider.getToolsProvide(ChatbotCommand.INFO);
- assertNotNull(toolProvider);
- assertEquals(infoToolsProvider, toolProvider);
- }
-
- @Test
- public void testGetToolsProvideWithOtherCommand() {
- ToolProvider toolProvider = aiServiceToolsProvider.getToolsProvide(ChatbotCommand.HELP);
- assertNull(toolProvider);
- }
-
- @Test
- public void testGetToolsProvideWithNullCommand() {
- ToolProvider toolProvider = aiServiceToolsProvider.getToolsProvide(null);
- assertNull(toolProvider);
- }
-}
diff --git a/bigtop-manager-ui/src/api/llm-config/index.ts b/bigtop-manager-ui/src/api/llm-config/index.ts
index 4d0702bb5..234231746 100644
--- a/bigtop-manager-ui/src/api/llm-config/index.ts
+++ b/bigtop-manager-ui/src/api/llm-config/index.ts
@@ -18,7 +18,13 @@
*/
import request from '@/api/request.ts'
-import { Platform, PlatformCredential, AuthorizedPlatform, UpdateAuthorizedPlatformConfig } from './types'
+import {
+ Platform,
+ PlatformCredential,
+ AuthorizedPlatform,
+ UpdateAuthorizedPlatformConfig,
+ PlatformModelsReq
+} from './types'
export const getPlatforms = (): Promise => {
return request({
@@ -41,6 +47,14 @@ export const getPlatformCredentials = (platformId: number): Promise => {
+ return request({
+ method: 'post',
+ url: `/llm/config/platforms/${platformId}/models`,
+ data
+ })
+}
+
export const addAuthorizedPlatform = (data: UpdateAuthorizedPlatformConfig): Promise => {
return request({
method: 'post',
diff --git a/bigtop-manager-ui/src/api/llm-config/types.ts b/bigtop-manager-ui/src/api/llm-config/types.ts
index 0b458b561..c085734e5 100644
--- a/bigtop-manager-ui/src/api/llm-config/types.ts
+++ b/bigtop-manager-ui/src/api/llm-config/types.ts
@@ -68,3 +68,7 @@ export interface UpdateAuthorizedPlatformConfig extends AuthorizedPlatform {
authCredentials: AuthCredential[]
testPassed: boolean
}
+
+export interface PlatformModelsReq {
+ authCredentials: AuthCredential[]
+}
diff --git a/bigtop-manager-ui/src/locales/en_US/llm-config.ts b/bigtop-manager-ui/src/locales/en_US/llm-config.ts
index f128d1163..7a14dee15 100644
--- a/bigtop-manager-ui/src/locales/en_US/llm-config.ts
+++ b/bigtop-manager-ui/src/locales/en_US/llm-config.ts
@@ -26,5 +26,8 @@ export default {
name: 'Name',
platform_name: 'Platform',
model: 'Model',
- desc: 'Remark'
+ desc: 'Remark',
+ fetch_models: 'Fetch Models',
+ models_loaded: 'Models loaded',
+ models_load_failed: 'Failed to load models'
}
diff --git a/bigtop-manager-ui/src/locales/zh_CN/llm-config.ts b/bigtop-manager-ui/src/locales/zh_CN/llm-config.ts
index 04facdd05..760f84fea 100644
--- a/bigtop-manager-ui/src/locales/zh_CN/llm-config.ts
+++ b/bigtop-manager-ui/src/locales/zh_CN/llm-config.ts
@@ -26,5 +26,8 @@ export default {
name: '名字',
platform_name: '平台',
model: '模型',
- desc: '备注'
+ desc: '备注',
+ fetch_models: '拉取模型',
+ models_loaded: '模型列表已更新',
+ models_load_failed: '拉取模型失败'
}
diff --git a/bigtop-manager-ui/src/pages/system-manage/llm-config/components/add-llm-item.vue b/bigtop-manager-ui/src/pages/system-manage/llm-config/components/add-llm-item.vue
index 4dcb83d59..1e508f5f0 100644
--- a/bigtop-manager-ui/src/pages/system-manage/llm-config/components/add-llm-item.vue
+++ b/bigtop-manager-ui/src/pages/system-manage/llm-config/components/add-llm-item.vue
@@ -42,6 +42,7 @@
const mode = ref('ADD')
const formRef = ref(null)
const disabledFormKeys = shallowRef(['platformId'])
+ const loadingModels = ref(false)
const { loading, loadingTest, currPlatform, platforms, isDisabled, formKeys, formCredentials, supportModels } =
storeToRefs(llmConfigStore)
@@ -137,6 +138,20 @@
}
}
+ const handleRefreshModels = async () => {
+ loadingModels.value = true
+ const models = await llmConfigStore.refreshPlatformModels()
+ if (models.length > 0) {
+ if (!models.includes(currPlatform.value.model as string)) {
+ currPlatform.value.model = undefined
+ }
+ message.success(t('llmConfig.models_loaded'))
+ } else {
+ message.error(t('llmConfig.models_load_failed'))
+ }
+ loadingModels.value = false
+ }
+
const handleCancel = () => {
formRef.value?.resetForm()
open.value = false
@@ -181,11 +196,17 @@
>
-
+
+
+
+ {{ t('llmConfig.fetch_models') }}
+
+
diff --git a/bigtop-manager-ui/src/store/llm-config/index.ts b/bigtop-manager-ui/src/store/llm-config/index.ts
index e1d763c85..269f7100c 100644
--- a/bigtop-manager-ui/src/store/llm-config/index.ts
+++ b/bigtop-manager-ui/src/store/llm-config/index.ts
@@ -116,6 +116,30 @@ export const useLlmConfigStore = defineStore(
}
}
+ const refreshPlatformModels = async () => {
+ try {
+ const platformId = currPlatform.value.platformId
+ if (typeof platformId === 'undefined') {
+ return []
+ }
+
+ const credentials = authCredentials.value
+ const models = await llmServer.getPlatformModels(platformId, { authCredentials: credentials })
+
+ if (Array.isArray(models) && models.length > 0) {
+ const target = platforms.value.find((item) => item.id === platformId)
+ if (target) {
+ target.supportModels = models
+ }
+ }
+
+ return models
+ } catch (error) {
+ console.log('error :>> ', error)
+ return []
+ }
+ }
+
const getAuthPlatformDetail = async () => {
try {
const authId = currPlatform.value.id as number
@@ -201,6 +225,7 @@ export const useLlmConfigStore = defineStore(
getAuthPlatformDetail,
getAuthorizedPlatforms,
getPlatformCredentials,
+ refreshPlatformModels,
addAuthorizedPlatform,
updateAuthPlatform,
testAuthorizedPlatform,
diff --git a/pom.xml b/pom.xml
index 689864a6d..9509b90d5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -161,6 +161,17 @@
+
+
+ spring-milestones
+ Spring Milestones
+ https://repo.spring.io/milestone
+
+ false
+
+
+
+
${project.artifactId}-${project.version}
From e49396068b03238cba2c27b017a34abe2d31ace9 Mon Sep 17 00:00:00 2001
From: lhpqaq <657407891@qq.com>
Date: Thu, 26 Mar 2026 20:25:48 +0800
Subject: [PATCH 2/7] fix ci
---
.../ai/assistant/GeneralAssistantFactory.java | 1 +
.../ai/config/McpAsyncClientManager.java | 36 +++++++++++++++----
.../manager/ai/core/AbstractAIAssistant.java | 6 ++--
.../ai/platform/DashScopeAssistant.java | 12 ++++---
.../ai/platform/DeepSeekAssistant.java | 3 +-
.../manager/ai/platform/OpenAIAssistant.java | 12 ++++---
.../manager/ai/platform/QianFanAssistant.java | 14 ++++----
.../manager/server/mcp/tool/StackMcpTool.java | 3 +-
.../service/impl/ChatbotServiceImpl.java | 3 +-
.../service/impl/LLMConfigServiceImpl.java | 4 +--
pom.xml | 6 ++--
11 files changed, 67 insertions(+), 33 deletions(-)
diff --git a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/assistant/GeneralAssistantFactory.java b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/assistant/GeneralAssistantFactory.java
index a59a94b33..1970c47c9 100644
--- a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/assistant/GeneralAssistantFactory.java
+++ b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/assistant/GeneralAssistantFactory.java
@@ -34,6 +34,7 @@
import org.apache.bigtop.manager.ai.platform.QianFanAssistant;
import org.springframework.stereotype.Component;
+
import lombok.extern.slf4j.Slf4j;
import jakarta.annotation.Resource;
diff --git a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/config/McpAsyncClientManager.java b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/config/McpAsyncClientManager.java
index c9c5af8ec..afbdfe4ef 100644
--- a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/config/McpAsyncClientManager.java
+++ b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/config/McpAsyncClientManager.java
@@ -1,12 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.bigtop.manager.ai.config;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.web.reactive.function.client.WebClient;
+
import io.modelcontextprotocol.client.McpAsyncClient;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport;
import io.modelcontextprotocol.spec.McpSchema;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Component;
-import org.springframework.web.reactive.function.client.WebClient;
import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
@@ -47,13 +66,14 @@ public synchronized McpAsyncClient getClient() {
String baseUrl = resolveBaseUrl();
log.info("Initializing MCP Async Client with baseUrl={}, sseEndpoint={}", baseUrl, sseEndpoint);
WebClient.Builder webClientBuilder = WebClient.builder().baseUrl(baseUrl);
- WebFluxSseClientTransport transport =
- WebFluxSseClientTransport.builder(webClientBuilder).sseEndpoint(sseEndpoint).build();
+ WebFluxSseClientTransport transport = WebFluxSseClientTransport.builder(webClientBuilder)
+ .sseEndpoint(sseEndpoint)
+ .build();
McpAsyncClient client = McpClient.async(transport)
.requestTimeout(Duration.ofSeconds(30))
.build();
-
+
client.initialize().block(Duration.ofSeconds(10));
client.ping().block(Duration.ofSeconds(10));
@@ -77,7 +97,9 @@ private void logRegisteredTools(McpAsyncClient client) {
McpSchema.ListToolsResult listToolsResult = cursor == null || cursor.isBlank()
? client.listTools().block(Duration.ofSeconds(10))
: client.listTools(cursor).block(Duration.ofSeconds(10));
- if (listToolsResult == null || listToolsResult.tools() == null || listToolsResult.tools().isEmpty()) {
+ if (listToolsResult == null
+ || listToolsResult.tools() == null
+ || listToolsResult.tools().isEmpty()) {
break;
}
diff --git a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/AbstractAIAssistant.java b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/AbstractAIAssistant.java
index b40a7a4ee..457846e6e 100644
--- a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/AbstractAIAssistant.java
+++ b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/AbstractAIAssistant.java
@@ -26,9 +26,8 @@
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.web.reactive.function.client.WebClient;
-import reactor.core.publisher.Flux;
-
import com.fasterxml.jackson.databind.JsonNode;
+import reactor.core.publisher.Flux;
import java.time.Duration;
import java.util.ArrayList;
@@ -175,7 +174,8 @@ public List getModels() {
String apiKey = resolveApiKey(credentials);
try {
- WebClient webClient = WebClient.builder().baseUrl(baseUrl.trim()).build();
+ WebClient webClient =
+ WebClient.builder().baseUrl(baseUrl.trim()).build();
WebClient.RequestHeadersSpec> requestSpec = webClient.get().uri(path);
applyModelRequestAuth(requestSpec, apiKey);
diff --git a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DashScopeAssistant.java b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DashScopeAssistant.java
index cd8e205b8..cc231f593 100644
--- a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DashScopeAssistant.java
+++ b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DashScopeAssistant.java
@@ -79,9 +79,12 @@ public ChatModel getChatModel() {
String apiKey = config.getCredentials().get("apiKey");
Assert.notNull(apiKey, "apiKey must not be null");
- OpenAiApi openAiApi =
- OpenAiApi.builder().baseUrl(resolveDefaultBaseUrl()).apiKey(apiKey).build();
- OpenAiChatOptions.Builder optionsBuilder = OpenAiChatOptions.builder().model(model);
+ OpenAiApi openAiApi = OpenAiApi.builder()
+ .baseUrl(resolveDefaultBaseUrl())
+ .apiKey(apiKey)
+ .build();
+ OpenAiChatOptions.Builder optionsBuilder =
+ OpenAiChatOptions.builder().model(model);
if (mcpAsyncClient != null) {
optionsBuilder.toolCallbacks(
new org.springframework.ai.mcp.AsyncMcpToolCallbackProvider(mcpAsyncClient).getToolCallbacks());
@@ -155,7 +158,8 @@ public Flux streamChat(String userMessage) {
String content = null;
if (chatResponse.getResult() != null
&& chatResponse.getResult().getOutput() != null) {
- content = chatResponse.getResult().getOutput().getText();
+ content =
+ chatResponse.getResult().getOutput().getText();
}
if (content != null && !content.isEmpty()) {
responseBuilder.append(content);
diff --git a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DeepSeekAssistant.java b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DeepSeekAssistant.java
index e52bac1fc..8f7e99a9b 100644
--- a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DeepSeekAssistant.java
+++ b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DeepSeekAssistant.java
@@ -159,7 +159,8 @@ public Flux streamChat(String userMessage) {
String content = null;
if (chatResponse.getResult() != null
&& chatResponse.getResult().getOutput() != null) {
- content = chatResponse.getResult().getOutput().getText();
+ content =
+ chatResponse.getResult().getOutput().getText();
}
if (content != null && !content.isEmpty()) {
responseBuilder.append(content);
diff --git a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/OpenAIAssistant.java b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/OpenAIAssistant.java
index 2fe30ae53..67c74ebf4 100644
--- a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/OpenAIAssistant.java
+++ b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/OpenAIAssistant.java
@@ -87,9 +87,12 @@ public ChatModel getChatModel() {
String apiKey = config.getCredentials().get("apiKey");
Assert.notNull(apiKey, "apiKey must not be null");
- OpenAiApi openAiApi =
- OpenAiApi.builder().baseUrl(resolveDefaultBaseUrl()).apiKey(apiKey).build();
- OpenAiChatOptions.Builder optionsBuilder = OpenAiChatOptions.builder().model(model);
+ OpenAiApi openAiApi = OpenAiApi.builder()
+ .baseUrl(resolveDefaultBaseUrl())
+ .apiKey(apiKey)
+ .build();
+ OpenAiChatOptions.Builder optionsBuilder =
+ OpenAiChatOptions.builder().model(model);
if (mcpAsyncClient != null) {
optionsBuilder.toolCallbacks(
new org.springframework.ai.mcp.AsyncMcpToolCallbackProvider(mcpAsyncClient).getToolCallbacks());
@@ -163,7 +166,8 @@ public Flux streamChat(String userMessage) {
String content = null;
if (chatResponse.getResult() != null
&& chatResponse.getResult().getOutput() != null) {
- content = chatResponse.getResult().getOutput().getText();
+ content =
+ chatResponse.getResult().getOutput().getText();
}
if (content != null && !content.isEmpty()) {
responseBuilder.append(content);
diff --git a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/QianFanAssistant.java b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/QianFanAssistant.java
index f073ba96a..ee60e1971 100644
--- a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/QianFanAssistant.java
+++ b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/QianFanAssistant.java
@@ -32,12 +32,11 @@
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
-import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.util.Assert;
-
-import reactor.core.publisher.Flux;
+import org.springframework.web.reactive.function.client.WebClient;
import com.fasterxml.jackson.databind.JsonNode;
+import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.Collections;
@@ -89,7 +88,8 @@ public List getModels() {
}
try {
- WebClient webClient = WebClient.builder().baseUrl(resolveModelsBaseUrl()).build();
+ WebClient webClient =
+ WebClient.builder().baseUrl(resolveModelsBaseUrl()).build();
JsonNode response = webClient
.get()
.uri(resolveModelsPath())
@@ -128,7 +128,8 @@ public ChatModel getChatModel() {
.completionsPath("/v2/chat/completions")
.apiKey(apiKey)
.build();
- OpenAiChatOptions.Builder optionsBuilder = OpenAiChatOptions.builder().model(model);
+ OpenAiChatOptions.Builder optionsBuilder =
+ OpenAiChatOptions.builder().model(model);
if (mcpAsyncClient != null) {
optionsBuilder.toolCallbacks(
new org.springframework.ai.mcp.AsyncMcpToolCallbackProvider(mcpAsyncClient).getToolCallbacks());
@@ -201,7 +202,8 @@ public Flux streamChat(String userMessage) {
String content = null;
if (chatResponse.getResult() != null
&& chatResponse.getResult().getOutput() != null) {
- content = chatResponse.getResult().getOutput().getText();
+ content =
+ chatResponse.getResult().getOutput().getText();
}
if (content != null && !content.isEmpty()) {
responseBuilder.append(content);
diff --git a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/mcp/tool/StackMcpTool.java b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/mcp/tool/StackMcpTool.java
index a90e0a1af..346fdb03b 100644
--- a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/mcp/tool/StackMcpTool.java
+++ b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/mcp/tool/StackMcpTool.java
@@ -18,7 +18,6 @@
*/
package org.apache.bigtop.manager.server.mcp.tool;
-import lombok.extern.slf4j.Slf4j;
import org.apache.bigtop.manager.server.mcp.converter.JsonToolCallResultConverter;
import org.apache.bigtop.manager.server.model.converter.ServiceConverter;
import org.apache.bigtop.manager.server.model.converter.StackConverter;
@@ -30,6 +29,8 @@
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
+import lombok.extern.slf4j.Slf4j;
+
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
diff --git a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/ChatbotServiceImpl.java b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/ChatbotServiceImpl.java
index c592bcd28..81cd3a763 100644
--- a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/ChatbotServiceImpl.java
+++ b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/ChatbotServiceImpl.java
@@ -230,8 +230,7 @@ private PlatformType getPlatformType(String platformName) {
private AIAssistant buildAIAssistant(
String platformName, String model, Map credentials, Long threadId, ChatbotCommand command) {
- return aiAssistantFactory.createAIService(
- getAIAssistantConfig(platformName, model, credentials, threadId));
+ return aiAssistantFactory.createAIService(getAIAssistantConfig(platformName, model, credentials, threadId));
}
private AIAssistant prepareTalk(Long threadId, ChatbotCommand command) {
diff --git a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/LLMConfigServiceImpl.java b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/LLMConfigServiceImpl.java
index 8c8b4ab38..6e2ea9944 100644
--- a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/LLMConfigServiceImpl.java
+++ b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/LLMConfigServiceImpl.java
@@ -22,7 +22,6 @@
import org.apache.bigtop.manager.ai.core.enums.PlatformType;
import org.apache.bigtop.manager.ai.core.factory.AIAssistant;
import org.apache.bigtop.manager.ai.core.factory.AIAssistantFactory;
-import org.apache.bigtop.manager.common.utils.JsonUtils;
import org.apache.bigtop.manager.dao.po.AuthPlatformPO;
import org.apache.bigtop.manager.dao.po.ChatMessagePO;
import org.apache.bigtop.manager.dao.po.ChatThreadPO;
@@ -81,7 +80,8 @@ public List platforms() {
}
private List getDynamicModels(PlatformPO platformPO, Map explicitCreds) {
- PlatformType platformType = PlatformType.getPlatformType(platformPO.getName().toLowerCase());
+ PlatformType platformType =
+ PlatformType.getPlatformType(platformPO.getName().toLowerCase());
if (platformType == null) {
return getDefaultModels(platformPO);
}
diff --git a/pom.xml b/pom.xml
index 9509b90d5..378177e2d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -163,12 +163,12 @@
- spring-milestones
- Spring Milestones
- https://repo.spring.io/milestone
false
+ spring-milestones
+ Spring Milestones
+ https://repo.spring.io/milestone
From 34277aed49b909bb88de757dd7165a70298fb294 Mon Sep 17 00:00:00 2001
From: lhpqaq <657407891@qq.com>
Date: Thu, 26 Mar 2026 20:37:06 +0800
Subject: [PATCH 3/7] fix ci
---
.../system-manage/llm-config/components/add-llm-item.vue | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/bigtop-manager-ui/src/pages/system-manage/llm-config/components/add-llm-item.vue b/bigtop-manager-ui/src/pages/system-manage/llm-config/components/add-llm-item.vue
index 1e508f5f0..c49b09ff9 100644
--- a/bigtop-manager-ui/src/pages/system-manage/llm-config/components/add-llm-item.vue
+++ b/bigtop-manager-ui/src/pages/system-manage/llm-config/components/add-llm-item.vue
@@ -196,7 +196,7 @@
>
-
+
{{ t('llmConfig.fetch_models') }}
-
+
From 1418d5c1af3b56a8142348616865d1f7f5996ffb Mon Sep 17 00:00:00 2001
From: lhpqaq <657407891@qq.com>
Date: Thu, 26 Mar 2026 22:12:53 +0800
Subject: [PATCH 4/7] feat: support multi-mcp
---
.../ai/assistant/GeneralAssistantFactory.java | 16 +-
.../ai/config/McpAsyncClientManager.java | 394 ++++++++++++++++--
.../manager/ai/core/AbstractAIAssistant.java | 27 ++
.../manager/ai/core/factory/AIAssistant.java | 2 +
.../ai/platform/DashScopeAssistant.java | 5 +-
.../ai/platform/DeepSeekAssistant.java | 5 +-
.../manager/ai/platform/OpenAIAssistant.java | 5 +-
.../manager/ai/platform/QianFanAssistant.java | 5 +-
.../ai/config/McpAsyncClientManagerTest.java | 120 +++++-
bigtop-manager-bom/pom.xml | 2 +-
.../service/impl/ChatbotServiceImpl.java | 131 +++++-
.../src/main/resources/application.yml | 18 +-
12 files changed, 639 insertions(+), 91 deletions(-)
diff --git a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/assistant/GeneralAssistantFactory.java b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/assistant/GeneralAssistantFactory.java
index 1970c47c9..53bd4c32f 100644
--- a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/assistant/GeneralAssistantFactory.java
+++ b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/assistant/GeneralAssistantFactory.java
@@ -88,10 +88,10 @@ public AIAssistant createWithPrompt(AIAssistantConfig config, SystemPrompt syste
.memoryStore(chatMemoryStoreProvider.createPersistentChatMemoryStore(id))
.withConfig(generalAssistantConfig);
- io.modelcontextprotocol.client.McpAsyncClient mcpAsyncClient = mcpAsyncClientManager.getClient();
- if (mcpAsyncClient != null) {
- log.info("MCP client available for platform {} (chat)", platformType);
- builder.withMcpClient(mcpAsyncClient);
+ List mcpAsyncClients = mcpAsyncClientManager.getClients();
+ if (!mcpAsyncClients.isEmpty()) {
+ log.info("MCP clients available for platform {} (chat), count={}", platformType, mcpAsyncClients.size());
+ builder.withMcpClients(mcpAsyncClients);
} else {
log.info("MCP client unavailable for platform {} (chat)", platformType);
}
@@ -111,10 +111,10 @@ public AIAssistant createForTest(AIAssistantConfig config) {
.memoryStore(chatMemoryStoreProvider.createInMemoryChatMemoryStore())
.withConfig(generalAssistantConfig);
- io.modelcontextprotocol.client.McpAsyncClient mcpAsyncClient = mcpAsyncClientManager.getClient();
- if (mcpAsyncClient != null) {
- log.info("MCP client available for platform {} (test)", platformType);
- builder.withMcpClient(mcpAsyncClient);
+ List mcpAsyncClients = mcpAsyncClientManager.getClients();
+ if (!mcpAsyncClients.isEmpty()) {
+ log.info("MCP clients available for platform {} (test), count={}", platformType, mcpAsyncClients.size());
+ builder.withMcpClients(mcpAsyncClients);
} else {
log.info("MCP client unavailable for platform {} (test)", platformType);
}
diff --git a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/config/McpAsyncClientManager.java b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/config/McpAsyncClientManager.java
index afbdfe4ef..90ca8661c 100644
--- a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/config/McpAsyncClientManager.java
+++ b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/config/McpAsyncClientManager.java
@@ -18,85 +18,256 @@
*/
package org.apache.bigtop.manager.ai.config;
+import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpSseClientProperties;
+import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpStdioClientProperties;
+import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpStreamableHttpClientProperties;
+import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.client.WebClient;
+import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.client.McpAsyncClient;
import io.modelcontextprotocol.client.McpClient;
+import io.modelcontextprotocol.client.transport.ServerParameters;
+import io.modelcontextprotocol.client.transport.StdioClientTransport;
+import io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport;
import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport;
+import io.modelcontextprotocol.json.McpJsonMapper;
+import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper;
+import io.modelcontextprotocol.spec.McpClientTransport;
import io.modelcontextprotocol.spec.McpSchema;
import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
@Component
@Slf4j
public class McpAsyncClientManager {
- @Value("${server.port:8080}")
- private String serverPort;
-
@Value("${spring.ai.mcp.client.enabled:false}")
private boolean enabled;
- @Value("${spring.ai.mcp.client.base-url:http://localhost:${server.port:8080}}")
- private String clientBaseUrl;
+ @Value("${spring.ai.mcp.client.request-timeout-seconds:120}")
+ private long requestTimeoutSeconds;
+
+ @Value("${spring.ai.mcp.client.init-timeout-seconds:10}")
+ private long initTimeoutSeconds;
+
+ @Value("${spring.ai.mcp.client.connections:}")
+ private String connections;
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+ private final McpJsonMapper mcpJsonMapper = new JacksonMcpJsonMapper(objectMapper);
- @Value("${spring.ai.mcp.client.sse-endpoint:/mcp/sse}")
- private String sseEndpoint;
+ private final ObjectProvider sseClientPropertiesProvider;
+ private final ObjectProvider streamableHttpClientPropertiesProvider;
+ private final ObjectProvider stdioClientPropertiesProvider;
- private McpAsyncClient mcpAsyncClient;
+ private List clients = Collections.emptyList();
private boolean initialized = false;
private boolean disabledLogged = false;
+ public McpAsyncClientManager(
+ ObjectProvider sseClientPropertiesProvider,
+ ObjectProvider streamableHttpClientPropertiesProvider,
+ ObjectProvider stdioClientPropertiesProvider) {
+ this.sseClientPropertiesProvider = sseClientPropertiesProvider;
+ this.streamableHttpClientPropertiesProvider = streamableHttpClientPropertiesProvider;
+ this.stdioClientPropertiesProvider = stdioClientPropertiesProvider;
+ }
+
public synchronized McpAsyncClient getClient() {
+ List allClients = getClients();
+ if (allClients.isEmpty()) {
+ return null;
+ }
+ return allClients.get(0);
+ }
+
+ public synchronized List getClients() {
if (!enabled) {
if (!disabledLogged) {
log.info("MCP Async Client is disabled by config (spring.ai.mcp.client.enabled=false)");
disabledLogged = true;
}
- return null;
+ return Collections.emptyList();
}
if (!initialized) {
- try {
- String baseUrl = resolveBaseUrl();
- log.info("Initializing MCP Async Client with baseUrl={}, sseEndpoint={}", baseUrl, sseEndpoint);
- WebClient.Builder webClientBuilder = WebClient.builder().baseUrl(baseUrl);
- WebFluxSseClientTransport transport = WebFluxSseClientTransport.builder(webClientBuilder)
- .sseEndpoint(sseEndpoint)
- .build();
-
- McpAsyncClient client = McpClient.async(transport)
- .requestTimeout(Duration.ofSeconds(30))
- .build();
-
- client.initialize().block(Duration.ofSeconds(10));
- client.ping().block(Duration.ofSeconds(10));
-
- mcpAsyncClient = client;
- initialized = true;
- log.info("MCP Async Client successfully initialized to {}{}", baseUrl, sseEndpoint);
- logRegisteredTools(client);
- } catch (Exception e) {
- log.warn("Failed to initialize MCP Async Client: {}", e.getMessage());
- // Leave initialized = false to retry on next getClient() call
+ initialized = true;
+ clients = initializeClients();
+ log.info("MCP Async Clients initialized: {}", clients.size());
+ }
+
+ return clients;
+ }
+
+ private List initializeClients() {
+ List parsedConnections = parseConnections();
+
+ if (parsedConnections.isEmpty()) {
+ parsedConnections.addAll(fromSpringMcpProperties());
+ }
+
+ if (parsedConnections.isEmpty()) {
+ log.warn(
+ "No MCP connections configured. Please use spring.ai.mcp.client.connections or spring.ai.mcp.client..connections.");
+ return Collections.emptyList();
+ }
+
+ List initializedClients = new ArrayList<>();
+ for (McpConnection connection : parsedConnections) {
+ McpAsyncClient client = initializeClient(connection);
+ if (client != null) {
+ initializedClients.add(client);
}
}
- return mcpAsyncClient;
+ return initializedClients;
}
- private void logRegisteredTools(McpAsyncClient client) {
+ private List fromSpringMcpProperties() {
+ List conns = new ArrayList<>();
+
+ McpSseClientProperties sseProperties = sseClientPropertiesProvider.getIfAvailable();
+ if (sseProperties != null && sseProperties.getConnections() != null) {
+ sseProperties.getConnections().forEach((name, params) -> {
+ if (params != null) {
+ conns.add(McpConnection.sse(name, params.url(), params.sseEndpoint()));
+ }
+ });
+ }
+
+ McpStreamableHttpClientProperties streamableHttpProperties =
+ streamableHttpClientPropertiesProvider.getIfAvailable();
+ if (streamableHttpProperties != null && streamableHttpProperties.getConnections() != null) {
+ streamableHttpProperties.getConnections().forEach((name, params) -> {
+ if (params != null) {
+ conns.add(McpConnection.streamableHttp(name, params.url(), params.endpoint()));
+ }
+ });
+ }
+
+ McpStdioClientProperties stdioProperties = stdioClientPropertiesProvider.getIfAvailable();
+ if (stdioProperties != null && stdioProperties.getConnections() != null) {
+ stdioProperties.getConnections().forEach((name, params) -> {
+ if (params != null) {
+ conns.add(McpConnection.local(name, params.command(), params.args(), params.env()));
+ }
+ });
+ }
+
+ return conns;
+ }
+
+ private McpAsyncClient initializeClient(McpConnection connection) {
+ try {
+ McpClientTransport transport = buildTransport(connection);
+ if (transport == null) {
+ log.warn("Skip MCP connection {} due to unsupported type {}", connection.name, connection.type);
+ return null;
+ }
+
+ Duration requestTimeout = Duration.ofSeconds(Math.max(
+ connection.requestTimeoutSeconds > 0 ? connection.requestTimeoutSeconds : requestTimeoutSeconds,
+ 1));
+ Duration initTimeout = Duration.ofSeconds(Math.max(
+ connection.initTimeoutSeconds > 0 ? connection.initTimeoutSeconds : initTimeoutSeconds, 1));
+
+ McpAsyncClient client =
+ McpClient.async(transport).requestTimeout(requestTimeout).build();
+
+ client.initialize().block(initTimeout);
+ client.ping().block(initTimeout);
+
+ log.info(
+ "MCP Async Client [{}] initialized, type={}, baseUrl={}, endpoint={}, requestTimeout={}, initTimeout={}",
+ connection.name,
+ connection.type,
+ connection.baseUrl,
+ connection.endpoint,
+ requestTimeout,
+ initTimeout);
+ logRegisteredTools(connection.name, client, initTimeout);
+ return client;
+ } catch (Exception e) {
+ log.warn("Failed to initialize MCP client [{}]: {}", connection.name, e.getMessage());
+ return null;
+ }
+ }
+
+ private McpClientTransport buildTransport(McpConnection connection) {
+ String type = connection.type.toLowerCase();
+ return switch (type) {
+ case "sse" -> buildWebFluxSseTransport(connection);
+ case "streamable-http", "http", "http-streamable" -> buildWebFluxStreamableHttpTransport(connection);
+ case "local", "stdio" -> buildStdioTransport(connection);
+ default -> null;
+ };
+ }
+
+ private McpClientTransport buildWebFluxSseTransport(McpConnection connection) {
+ String baseUrl = connection.baseUrl;
+ if (!StringUtils.hasText(baseUrl)) {
+ log.warn("MCP SSE connection {} missing baseUrl", connection.name);
+ return null;
+ }
+ String endpoint = StringUtils.hasText(connection.endpoint) ? connection.endpoint : "/mcp/sse";
+ WebClient.Builder webClientBuilder = WebClient.builder().baseUrl(baseUrl);
+ return WebFluxSseClientTransport.builder(webClientBuilder)
+ .jsonMapper(mcpJsonMapper)
+ .sseEndpoint(endpoint)
+ .build();
+ }
+
+ private McpClientTransport buildWebFluxStreamableHttpTransport(McpConnection connection) {
+ String baseUrl = connection.baseUrl;
+ if (!StringUtils.hasText(baseUrl)) {
+ log.warn("MCP streamable-http connection {} missing baseUrl", connection.name);
+ return null;
+ }
+ String endpoint = StringUtils.hasText(connection.endpoint) ? connection.endpoint : "/mcp";
+ WebClient.Builder webClientBuilder = WebClient.builder().baseUrl(baseUrl);
+ return WebClientStreamableHttpTransport.builder(webClientBuilder)
+ .jsonMapper(mcpJsonMapper)
+ .endpoint(endpoint)
+ .openConnectionOnStartup(true)
+ .build();
+ }
+
+ private McpClientTransport buildStdioTransport(McpConnection connection) {
+ if (!StringUtils.hasText(connection.command)) {
+ log.warn("MCP stdio/local connection {} missing command", connection.name);
+ return null;
+ }
+
+ ServerParameters.Builder serverBuilder = ServerParameters.builder(connection.command);
+ if (!connection.args.isEmpty()) {
+ serverBuilder.args(connection.args);
+ }
+ if (!connection.env.isEmpty()) {
+ serverBuilder.env(connection.env);
+ }
+
+ StdioClientTransport stdioTransport = new StdioClientTransport(serverBuilder.build(), mcpJsonMapper);
+ stdioTransport.setStdErrorHandler(line -> log.warn("MCP stdio [{}] stderr: {}", connection.name, line));
+ return stdioTransport;
+ }
+
+ private void logRegisteredTools(String connectionName, McpAsyncClient client, Duration initTimeout) {
try {
List toolNames = new ArrayList<>();
String cursor = null;
while (true) {
McpSchema.ListToolsResult listToolsResult = cursor == null || cursor.isBlank()
- ? client.listTools().block(Duration.ofSeconds(10))
- : client.listTools(cursor).block(Duration.ofSeconds(10));
+ ? client.listTools().block(initTimeout)
+ : client.listTools(cursor).block(initTimeout);
if (listToolsResult == null
|| listToolsResult.tools() == null
|| listToolsResult.tools().isEmpty()) {
@@ -110,22 +281,161 @@ private void logRegisteredTools(McpAsyncClient client) {
}
String nextCursor = listToolsResult.nextCursor();
- if (nextCursor == null || nextCursor.isBlank() || nextCursor.equals(cursor)) {
+ if (!StringUtils.hasText(nextCursor) || nextCursor.equals(cursor)) {
break;
}
cursor = nextCursor;
}
- log.info("MCP tools discovered: count={}, names={}", toolNames.size(), toolNames);
+ log.info("MCP tools discovered for [{}]: count={}, names={}", connectionName, toolNames.size(), toolNames);
} catch (Exception e) {
- log.warn("Failed to list MCP tools after initialization: {}", e.getMessage());
+ log.warn("Failed to list MCP tools for [{}]: {}", connectionName, e.getMessage());
+ }
+ }
+
+ private List parseConnections() {
+ if (!StringUtils.hasText(connections)) {
+ return new ArrayList<>();
+ }
+
+ List parsed = new ArrayList<>();
+ String[] segments = connections.split(";");
+ for (int i = 0; i < segments.length; i++) {
+ String segment = segments[i].trim();
+ if (!StringUtils.hasText(segment)) {
+ continue;
+ }
+
+ Map kv = new HashMap<>();
+ String[] pairs = segment.split(",");
+ for (String pair : pairs) {
+ String[] items = pair.split("=", 2);
+ if (items.length != 2) {
+ continue;
+ }
+ kv.put(items[0].trim().toLowerCase(), items[1].trim());
+ }
+
+ String name = valueOrDefault(kv.get("name"), "conn-" + (i + 1));
+ String type = valueOrDefault(kv.get("type"), "sse");
+ String baseUrl = kv.get("baseurl");
+ String endpoint = kv.get("endpoint");
+ String command = kv.get("command");
+ List args = parseList(kv.get("args"));
+ Map env = parseEnv(kv.get("env"));
+ long connectionRequestTimeout = parseLongOrDefault(kv.get("requesttimeoutseconds"), -1);
+ long connectionInitTimeout = parseLongOrDefault(kv.get("inittimeoutseconds"), -1);
+
+ parsed.add(new McpConnection(
+ name,
+ type,
+ baseUrl,
+ endpoint,
+ command,
+ args,
+ env,
+ connectionRequestTimeout,
+ connectionInitTimeout));
+ }
+
+ return parsed;
+ }
+
+ private static long parseLongOrDefault(String raw, long defaultValue) {
+ if (!StringUtils.hasText(raw)) {
+ return defaultValue;
+ }
+ try {
+ return Long.parseLong(raw);
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+ private static List parseList(String raw) {
+ if (!StringUtils.hasText(raw)) {
+ return Collections.emptyList();
+ }
+ List list = new ArrayList<>();
+ for (String item : raw.split("\\|")) {
+ String trimmed = item.trim();
+ if (StringUtils.hasText(trimmed)) {
+ list.add(trimmed);
+ }
+ }
+ return list;
+ }
+
+ private static Map parseEnv(String raw) {
+ if (!StringUtils.hasText(raw)) {
+ return Collections.emptyMap();
+ }
+ Map env = new HashMap<>();
+ for (String item : raw.split("\\|")) {
+ String[] pair = item.split(":", 2);
+ if (pair.length == 2 && StringUtils.hasText(pair[0])) {
+ env.put(pair[0].trim(), pair[1].trim());
+ }
}
+ return env;
}
- private String resolveBaseUrl() {
- if (clientBaseUrl != null && !clientBaseUrl.isBlank()) {
- return clientBaseUrl;
+ private static String valueOrDefault(String value, String defaultValue) {
+ return StringUtils.hasText(value) ? value : defaultValue;
+ }
+
+ private static class McpConnection {
+ private final String name;
+ private final String type;
+ private final String baseUrl;
+ private final String endpoint;
+ private final String command;
+ private final List args;
+ private final Map env;
+ private final long requestTimeoutSeconds;
+ private final long initTimeoutSeconds;
+
+ private McpConnection(
+ String name,
+ String type,
+ String baseUrl,
+ String endpoint,
+ String command,
+ List args,
+ Map env,
+ long requestTimeoutSeconds,
+ long initTimeoutSeconds) {
+ this.name = name;
+ this.type = type;
+ this.baseUrl = baseUrl;
+ this.endpoint = endpoint;
+ this.command = command;
+ this.args = args == null ? Collections.emptyList() : args;
+ this.env = env == null ? Collections.emptyMap() : env;
+ this.requestTimeoutSeconds = requestTimeoutSeconds;
+ this.initTimeoutSeconds = initTimeoutSeconds;
+ }
+
+ private static McpConnection sse(String name, String baseUrl, String endpoint) {
+ return new McpConnection(
+ name, "sse", baseUrl, endpoint, null, Collections.emptyList(), Collections.emptyMap(), -1, -1);
+ }
+
+ private static McpConnection streamableHttp(String name, String baseUrl, String endpoint) {
+ return new McpConnection(
+ name,
+ "streamable-http",
+ baseUrl,
+ endpoint,
+ null,
+ Collections.emptyList(),
+ Collections.emptyMap(),
+ -1,
+ -1);
+ }
+
+ private static McpConnection local(String name, String command, List args, Map env) {
+ return new McpConnection(name, "local", null, null, command, args, env, -1, -1);
}
- return "http://localhost:" + serverPort;
}
}
diff --git a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/AbstractAIAssistant.java b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/AbstractAIAssistant.java
index 457846e6e..c41720718 100644
--- a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/AbstractAIAssistant.java
+++ b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/AbstractAIAssistant.java
@@ -76,6 +76,7 @@ public abstract static class Builder implements AIAssistant.Builder {
protected String systemPrompt;
protected io.modelcontextprotocol.client.McpAsyncClient mcpAsyncClient;
+ protected List mcpAsyncClients = new ArrayList<>();
private static final String AUTHORIZATION_HEADER = "Authorization";
@@ -103,9 +104,35 @@ public Builder memoryStore(ChatMemory chatMemory) {
public Builder withMcpClient(io.modelcontextprotocol.client.McpAsyncClient mcpAsyncClient) {
this.mcpAsyncClient = mcpAsyncClient;
+ if (mcpAsyncClient != null) {
+ this.mcpAsyncClients = List.of(mcpAsyncClient);
+ }
return this;
}
+ @Override
+ public Builder withMcpClients(List mcpAsyncClients) {
+ if (mcpAsyncClients == null || mcpAsyncClients.isEmpty()) {
+ this.mcpAsyncClients = Collections.emptyList();
+ this.mcpAsyncClient = null;
+ return this;
+ }
+
+ this.mcpAsyncClients = List.copyOf(mcpAsyncClients);
+ this.mcpAsyncClient = this.mcpAsyncClients.get(0);
+ return this;
+ }
+
+ protected List getMcpAsyncClients() {
+ if (mcpAsyncClients != null && !mcpAsyncClients.isEmpty()) {
+ return mcpAsyncClients;
+ }
+ if (mcpAsyncClient != null) {
+ return List.of(mcpAsyncClient);
+ }
+ return Collections.emptyList();
+ }
+
public ChatMemory getChatMemory() {
if (chatMemory == null) {
chatMemory = MessageWindowChatMemory.builder()
diff --git a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/factory/AIAssistant.java b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/factory/AIAssistant.java
index cf76ec1ab..ced590e4e 100644
--- a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/factory/AIAssistant.java
+++ b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/core/factory/AIAssistant.java
@@ -79,6 +79,8 @@ interface Builder {
Builder withMcpClient(io.modelcontextprotocol.client.McpAsyncClient mcpAsyncClient);
+ Builder withMcpClients(List mcpAsyncClients);
+
Builder withSystemPrompt(String systemPrompt);
AIAssistant build();
diff --git a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DashScopeAssistant.java b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DashScopeAssistant.java
index cc231f593..d85dddc97 100644
--- a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DashScopeAssistant.java
+++ b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DashScopeAssistant.java
@@ -85,9 +85,10 @@ public ChatModel getChatModel() {
.build();
OpenAiChatOptions.Builder optionsBuilder =
OpenAiChatOptions.builder().model(model);
- if (mcpAsyncClient != null) {
+ List mcpClients = getMcpAsyncClients();
+ if (!mcpClients.isEmpty()) {
optionsBuilder.toolCallbacks(
- new org.springframework.ai.mcp.AsyncMcpToolCallbackProvider(mcpAsyncClient).getToolCallbacks());
+ new org.springframework.ai.mcp.AsyncMcpToolCallbackProvider(mcpClients).getToolCallbacks());
}
OpenAiChatOptions options = optionsBuilder.build();
return OpenAiChatModel.builder()
diff --git a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DeepSeekAssistant.java b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DeepSeekAssistant.java
index 8f7e99a9b..278cfea92 100644
--- a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DeepSeekAssistant.java
+++ b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/DeepSeekAssistant.java
@@ -86,9 +86,10 @@ public ChatModel getChatModel() {
.build();
DeepSeekChatOptions.Builder optionsBuilder =
DeepSeekChatOptions.builder().model(model);
- if (mcpAsyncClient != null) {
+ List mcpClients = getMcpAsyncClients();
+ if (!mcpClients.isEmpty()) {
optionsBuilder.toolCallbacks(
- new org.springframework.ai.mcp.AsyncMcpToolCallbackProvider(mcpAsyncClient).getToolCallbacks());
+ new org.springframework.ai.mcp.AsyncMcpToolCallbackProvider(mcpClients).getToolCallbacks());
}
DeepSeekChatOptions options = optionsBuilder.build();
return DeepSeekChatModel.builder()
diff --git a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/OpenAIAssistant.java b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/OpenAIAssistant.java
index 67c74ebf4..d955e6889 100644
--- a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/OpenAIAssistant.java
+++ b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/OpenAIAssistant.java
@@ -93,9 +93,10 @@ public ChatModel getChatModel() {
.build();
OpenAiChatOptions.Builder optionsBuilder =
OpenAiChatOptions.builder().model(model);
- if (mcpAsyncClient != null) {
+ List mcpClients = getMcpAsyncClients();
+ if (!mcpClients.isEmpty()) {
optionsBuilder.toolCallbacks(
- new org.springframework.ai.mcp.AsyncMcpToolCallbackProvider(mcpAsyncClient).getToolCallbacks());
+ new org.springframework.ai.mcp.AsyncMcpToolCallbackProvider(mcpClients).getToolCallbacks());
}
OpenAiChatOptions options = optionsBuilder.build();
return OpenAiChatModel.builder()
diff --git a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/QianFanAssistant.java b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/QianFanAssistant.java
index ee60e1971..a0f63bbe2 100644
--- a/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/QianFanAssistant.java
+++ b/bigtop-manager-ai/src/main/java/org/apache/bigtop/manager/ai/platform/QianFanAssistant.java
@@ -130,9 +130,10 @@ public ChatModel getChatModel() {
.build();
OpenAiChatOptions.Builder optionsBuilder =
OpenAiChatOptions.builder().model(model);
- if (mcpAsyncClient != null) {
+ List mcpClients = getMcpAsyncClients();
+ if (!mcpClients.isEmpty()) {
optionsBuilder.toolCallbacks(
- new org.springframework.ai.mcp.AsyncMcpToolCallbackProvider(mcpAsyncClient).getToolCallbacks());
+ new org.springframework.ai.mcp.AsyncMcpToolCallbackProvider(mcpClients).getToolCallbacks());
}
OpenAiChatOptions options = optionsBuilder.build();
return OpenAiChatModel.builder()
diff --git a/bigtop-manager-ai/src/test/java/org/apache/bigtop/manager/ai/config/McpAsyncClientManagerTest.java b/bigtop-manager-ai/src/test/java/org/apache/bigtop/manager/ai/config/McpAsyncClientManagerTest.java
index cccc83039..544f89d9c 100644
--- a/bigtop-manager-ai/src/test/java/org/apache/bigtop/manager/ai/config/McpAsyncClientManagerTest.java
+++ b/bigtop-manager-ai/src/test/java/org/apache/bigtop/manager/ai/config/McpAsyncClientManagerTest.java
@@ -19,37 +19,97 @@
package org.apache.bigtop.manager.ai.config;
import org.junit.jupiter.api.Test;
+import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpSseClientProperties;
+import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpStdioClientProperties;
+import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpStreamableHttpClientProperties;
+import org.springframework.beans.factory.ObjectProvider;
+import java.lang.reflect.Field;
import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
class McpAsyncClientManagerTest {
+ private static final ObjectProvider SSE_PROVIDER = new EmptyObjectProvider<>();
+ private static final ObjectProvider STREAMABLE_HTTP_PROVIDER =
+ new EmptyObjectProvider<>();
+ private static final ObjectProvider STDIO_PROVIDER = new EmptyObjectProvider<>();
+
@Test
void resolveBaseUrlShouldPreferConfiguredValue() throws Exception {
- McpAsyncClientManager manager = new McpAsyncClientManager();
- setField(manager, "clientBaseUrl", "http://127.0.0.1:9000");
- setField(manager, "serverPort", "8080");
+ McpAsyncClientManager manager =
+ new McpAsyncClientManager(SSE_PROVIDER, STREAMABLE_HTTP_PROVIDER, STDIO_PROVIDER);
+ setField(manager, "connections", "name=sseMain,type=sse,baseUrl=http://127.0.0.1:9000,endpoint=/mcp/sse");
- String baseUrl = invokeResolveBaseUrl(manager);
- assertEquals("http://127.0.0.1:9000", baseUrl);
+ Method parseMethod = McpAsyncClientManager.class.getDeclaredMethod("parseConnections");
+ parseMethod.setAccessible(true);
+ @SuppressWarnings("unchecked")
+ List