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 @@ >