From f22f5f76566696c97717964be34384b096344468 Mon Sep 17 00:00:00 2001 From: Makoto <2762006003@qq.com> Date: Tue, 12 Aug 2025 14:00:23 +0800 Subject: [PATCH 01/10] feat(n2sql): reconstruct the prompt word configuration to support the optimization of prompt word functions --- .../alibaba/cloud/ai/dto/PromptConfigDTO.java | 10 +- .../cloud/ai/entity/UserPromptConfig.java | 18 +- .../cloud/ai/node/ReportGeneratorNode.java | 14 +- .../alibaba/cloud/ai/prompt/PromptHelper.java | 69 +- .../ai/service/UserPromptConfigService.java | 130 ++-- .../resources/prompts/report-generator.txt | 2 + .../ai/controller/PromptConfigController.java | 43 +- .../components/PromptOptimizationConfig.vue | 639 ++++++++++++++++++ .../src/views/AgentDetail.vue | 27 +- 9 files changed, 842 insertions(+), 110 deletions(-) create mode 100644 spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-web-ui/src/components/PromptOptimizationConfig.vue diff --git a/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/dto/PromptConfigDTO.java b/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/dto/PromptConfigDTO.java index 0d85fc2d10..8fbd63776d 100644 --- a/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/dto/PromptConfigDTO.java +++ b/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/dto/PromptConfigDTO.java @@ -24,13 +24,13 @@ public record PromptConfigDTO(String id, // 配置ID(更新时需要) String name, // 配置名称 String promptType, // 提示词类型 - String systemPrompt, // 用户自定义的系统提示词内容 + String optimizationPrompt, // 用户添加的优化提示词内容 Boolean enabled, // 是否启用该配置 String description, // 配置描述 String creator // 创建者 ) { - public PromptConfigDTO(String promptType, String systemPrompt) { - this(null, null, promptType, systemPrompt, true, null, null); + public PromptConfigDTO(String promptType, String optimizationPrompt) { + this(null, null, promptType, optimizationPrompt, true, null, null); } @Override @@ -61,8 +61,8 @@ public String promptType() { } @Override - public String systemPrompt() { - return systemPrompt; + public String optimizationPrompt() { + return optimizationPrompt; } @Override diff --git a/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/entity/UserPromptConfig.java b/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/entity/UserPromptConfig.java index 5ca3d085b8..81f6b08ce0 100644 --- a/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/entity/UserPromptConfig.java +++ b/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/entity/UserPromptConfig.java @@ -19,7 +19,7 @@ import java.time.LocalDateTime; /** - * 用户自定义提示词配置实体类 + * 用户提示词优化配置实体类 * * @author Makoto */ @@ -41,9 +41,9 @@ public class UserPromptConfig { private String promptType; /** - * 用户自定义的系统提示词内容 + * 用户添加的优化提示词内容(附加到原始模板) */ - private String systemPrompt; + private String optimizationPrompt; /** * 是否启用该配置 @@ -77,10 +77,10 @@ public UserPromptConfig() { this.updateTime = LocalDateTime.now(); } - public UserPromptConfig(String promptType, String systemPrompt) { + public UserPromptConfig(String promptType, String optimizationPrompt) { this(); this.promptType = promptType; - this.systemPrompt = systemPrompt; + this.optimizationPrompt = optimizationPrompt; } // Getters and Setters @@ -108,12 +108,12 @@ public void setPromptType(String promptType) { this.promptType = promptType; } - public String getSystemPrompt() { - return systemPrompt; + public String getOptimizationPrompt() { + return optimizationPrompt; } - public void setSystemPrompt(String systemPrompt) { - this.systemPrompt = systemPrompt; + public void setOptimizationPrompt(String optimizationPrompt) { + this.optimizationPrompt = optimizationPrompt; this.updateTime = LocalDateTime.now(); } diff --git a/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/node/ReportGeneratorNode.java b/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/node/ReportGeneratorNode.java index ae87ba7ce5..36028d4c98 100644 --- a/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/node/ReportGeneratorNode.java +++ b/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/node/ReportGeneratorNode.java @@ -16,6 +16,7 @@ package com.alibaba.cloud.ai.node; +import com.alibaba.cloud.ai.entity.UserPromptConfig; import com.alibaba.cloud.ai.enums.StreamResponseType; import com.alibaba.cloud.ai.graph.OverAllState; import com.alibaba.cloud.ai.graph.action.NodeAction; @@ -136,14 +137,15 @@ private Flux generateReport(String userInput, Plan plan, HashMap optimizationConfigs = promptConfigService.getOptimizationConfigs("report-generator"); - // Use PromptHelper to build report generation prompt with custom prompt support - String reportPrompt = PromptHelper.buildReportGeneratorPromptWithCustom(userRequirementsAndPlan, - analysisStepsAndData, summaryAndRecommendations, customPrompt); + // Use PromptHelper to build report generation prompt with optimization support + String reportPrompt = PromptHelper.buildReportGeneratorPromptWithOptimization(userRequirementsAndPlan, + analysisStepsAndData, summaryAndRecommendations, optimizationConfigs); - logger.info("Using {} prompt for report generation", customPrompt != null ? "custom" : "default"); + logger.info("Using {} prompt for report generation", + !optimizationConfigs.isEmpty() ? "optimized (" + optimizationConfigs.size() + " configs)" : "default"); return chatClient.prompt().user(reportPrompt).stream().chatResponse(); } diff --git a/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/prompt/PromptHelper.java b/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/prompt/PromptHelper.java index 0ee223022a..86874c8eff 100644 --- a/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/prompt/PromptHelper.java +++ b/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/prompt/PromptHelper.java @@ -228,28 +228,28 @@ public static String buildSemanticConsistenPrompt(String nlReq, String sql) { } /** - * 构建带自定义提示词的报告生成提示词 + * 构建带优化提示词的报告生成提示词 * @param userRequirementsAndPlan 用户需求和计划 * @param analysisStepsAndData 分析步骤和数据 * @param summaryAndRecommendations 总结和建议 - * @param customPrompt 用户自定义的提示词内容,如果为null则使用默认提示词 + * @param optimizationConfigs 用户配置的优化提示词列表 * @return 构建的提示词 */ - public static String buildReportGeneratorPromptWithCustom(String userRequirementsAndPlan, - String analysisStepsAndData, String summaryAndRecommendations, String customPrompt) { + public static String buildReportGeneratorPromptWithOptimization(String userRequirementsAndPlan, + String analysisStepsAndData, String summaryAndRecommendations, + List optimizationConfigs) { + Map params = new HashMap<>(); params.put("user_requirements_and_plan", userRequirementsAndPlan); params.put("analysis_steps_and_data", analysisStepsAndData); params.put("summary_and_recommendations", summaryAndRecommendations); - if (customPrompt != null && !customPrompt.trim().isEmpty()) { - // 使用自定义提示词 - return new org.springframework.ai.chat.prompt.PromptTemplate(customPrompt).render(params); - } - else { - // 使用默认提示词 - return PromptConstant.getReportGeneratorPromptTemplate().render(params); - } + // 构建优化部分内容 + String optimizationSection = buildOptimizationSection(optimizationConfigs, params); + params.put("optimization_section", optimizationSection); + + // 渲染完整模板 + return PromptConstant.getReportGeneratorPromptTemplate().render(params); } public static String buildSqlErrorFixerPrompt(String question, DbConfig dbConfig, SchemaDTO schemaDTO, @@ -285,4 +285,49 @@ public static String buildSemanticModelPrompt(List semanticMod return PromptConstant.getSemanticModelPromptTemplate().render(params); } + /** + * 构建优化提示词部分内容 + * @param optimizationConfigs 优化配置列表 + * @param params 模板参数 + * @return 优化部分的内容 + */ + private static String buildOptimizationSection( + List optimizationConfigs, Map params) { + + if (optimizationConfigs == null || optimizationConfigs.isEmpty()) { + return ""; + } + + StringBuilder result = new StringBuilder(); + result.append("## 优化要求\n"); + + for (com.alibaba.cloud.ai.entity.UserPromptConfig config : optimizationConfigs) { + String optimizationContent = renderOptimizationPrompt(config.getOptimizationPrompt(), params); + if (!optimizationContent.trim().isEmpty()) { + result.append("- ").append(optimizationContent).append("\n"); + } + } + + return result.toString().trim(); + } + + /** + * 渲染优化提示词模板 + * @param optimizationPrompt 优化提示词模板 + * @param params 参数 + * @return 渲染后的内容 + */ + private static String renderOptimizationPrompt(String optimizationPrompt, Map params) { + if (optimizationPrompt == null || optimizationPrompt.trim().isEmpty()) { + return ""; + } + try { + return new org.springframework.ai.chat.prompt.PromptTemplate(optimizationPrompt).render(params); + } + catch (Exception e) { + // 如果模板渲染失败,直接返回原始内容 + return optimizationPrompt; + } + } + } diff --git a/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/service/UserPromptConfigService.java b/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/service/UserPromptConfigService.java index 171965362f..86257d1960 100644 --- a/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/service/UserPromptConfigService.java +++ b/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/service/UserPromptConfigService.java @@ -27,7 +27,7 @@ import java.util.concurrent.ConcurrentHashMap; /** - * 用户提示词配置管理服务 提供提示词配置的增删改查功能,支持运行时配置更新 + * 用户提示词优化配置管理服务 提供提示词优化配置的增删改查功能,支持多个优化配置同时生效 * * @author Makoto */ @@ -42,24 +42,24 @@ public class UserPromptConfigService { private final Map configStorage = new ConcurrentHashMap<>(); /** - * 根据提示词类型存储配置ID的映射 + * 根据提示词类型存储启用的配置ID列表(支持多个配置同时启用) */ - private final Map promptTypeToConfigId = new ConcurrentHashMap<>(); + private final Map> promptTypeToConfigIds = new ConcurrentHashMap<>(); /** - * 创建或更新提示词配置 + * 创建或更新提示词优化配置 * @param configDTO 配置数据传输对象 * @return 保存后的配置对象 */ public UserPromptConfig saveOrUpdateConfig(PromptConfigDTO configDTO) { - logger.info("保存或更新提示词配置:{}", configDTO); + logger.info("保存或更新提示词优化配置:{}", configDTO); UserPromptConfig config; if (configDTO.id() != null && configStorage.containsKey(configDTO.id())) { // 更新现有配置 config = configStorage.get(configDTO.id()); config.setName(configDTO.name()); - config.setSystemPrompt(configDTO.systemPrompt()); + config.setOptimizationPrompt(configDTO.optimizationPrompt()); config.setEnabled(configDTO.enabled()); config.setDescription(configDTO.description()); config.setUpdateTime(LocalDateTime.now()); @@ -70,7 +70,7 @@ public UserPromptConfig saveOrUpdateConfig(PromptConfigDTO configDTO) { config.setId(UUID.randomUUID().toString()); config.setName(configDTO.name()); config.setPromptType(configDTO.promptType()); - config.setSystemPrompt(configDTO.systemPrompt()); + config.setOptimizationPrompt(configDTO.optimizationPrompt()); config.setEnabled(configDTO.enabled()); config.setDescription(configDTO.description()); config.setCreator(configDTO.creator()); @@ -78,11 +78,8 @@ public UserPromptConfig saveOrUpdateConfig(PromptConfigDTO configDTO) { configStorage.put(config.getId(), config); - // 如果配置启用,更新类型映射 - if (Boolean.TRUE.equals(config.getEnabled())) { - promptTypeToConfigId.put(config.getPromptType(), config.getId()); - logger.info("已启用提示词类型 [{}] 的配置:{}", config.getPromptType(), config.getId()); - } + // 更新类型映射(支持多个配置) + updatePromptTypeMapping(config); return config; } @@ -97,19 +94,32 @@ public UserPromptConfig getConfigById(String id) { } /** - * 根据提示词类型获取启用的配置 + * 根据提示词类型获取所有启用的配置 + * @param promptType 提示词类型 + * @return 配置列表 + */ + public List getActiveConfigsByType(String promptType) { + List configIds = promptTypeToConfigIds.get(promptType); + if (configIds == null || configIds.isEmpty()) { + return new ArrayList<>(); + } + + return configIds.stream() + .map(configStorage::get) + .filter(Objects::nonNull) + .filter(config -> Boolean.TRUE.equals(config.getEnabled())) + .sorted(Comparator.comparing(UserPromptConfig::getUpdateTime).reversed()) + .toList(); + } + + /** + * 根据提示词类型获取启用的配置(兼容旧接口) * @param promptType 提示词类型 * @return 配置对象,不存在时返回null */ public UserPromptConfig getActiveConfigByType(String promptType) { - String configId = promptTypeToConfigId.get(promptType); - if (configId != null) { - UserPromptConfig config = configStorage.get(configId); - if (config != null && Boolean.TRUE.equals(config.getEnabled())) { - return config; - } - } - return null; + List configs = getActiveConfigsByType(promptType); + return configs.isEmpty() ? null : configs.get(0); } /** @@ -141,12 +151,8 @@ public List getConfigsByType(String promptType) { public boolean deleteConfig(String id) { UserPromptConfig config = configStorage.remove(id); if (config != null) { - // 如果删除的是当前启用的配置,需要清除类型映射 - String currentActiveId = promptTypeToConfigId.get(config.getPromptType()); - if (id.equals(currentActiveId)) { - promptTypeToConfigId.remove(config.getPromptType()); - logger.info("已删除提示词类型 [{}] 的活跃配置", config.getPromptType()); - } + // 从类型映射中移除该配置 + removeFromPromptTypeMapping(config); logger.info("已删除配置:{}", id); return true; } @@ -161,14 +167,9 @@ public boolean deleteConfig(String id) { public boolean enableConfig(String id) { UserPromptConfig config = configStorage.get(id); if (config != null) { - // 先禁用同类型的其他配置 - disableConfigsByType(config.getPromptType()); - - // 启用当前配置 config.setEnabled(true); config.setUpdateTime(LocalDateTime.now()); - promptTypeToConfigId.put(config.getPromptType(), id); - + updatePromptTypeMapping(config); logger.info("已启用配置:{}", id); return true; } @@ -185,13 +186,7 @@ public boolean disableConfig(String id) { if (config != null) { config.setEnabled(false); config.setUpdateTime(LocalDateTime.now()); - - // 如果是当前活跃配置,移除类型映射 - String currentActiveId = promptTypeToConfigId.get(config.getPromptType()); - if (id.equals(currentActiveId)) { - promptTypeToConfigId.remove(config.getPromptType()); - } - + removeFromPromptTypeMapping(config); logger.info("已禁用配置:{}", id); return true; } @@ -199,25 +194,58 @@ public boolean disableConfig(String id) { } /** - * 禁用指定类型的所有配置 + * 更新提示词类型映射 + * @param config 配置对象 + */ + private void updatePromptTypeMapping(UserPromptConfig config) { + if (Boolean.TRUE.equals(config.getEnabled())) { + promptTypeToConfigIds.computeIfAbsent(config.getPromptType(), k -> new ArrayList<>()); + List configIds = promptTypeToConfigIds.get(config.getPromptType()); + if (!configIds.contains(config.getId())) { + configIds.add(config.getId()); + logger.info("已将配置 {} 添加到提示词类型 [{}] 的映射中", config.getId(), config.getPromptType()); + } + } + else { + removeFromPromptTypeMapping(config); + } + } + + /** + * 从提示词类型映射中移除配置 + * @param config 配置对象 + */ + private void removeFromPromptTypeMapping(UserPromptConfig config) { + List configIds = promptTypeToConfigIds.get(config.getPromptType()); + if (configIds != null) { + configIds.remove(config.getId()); + if (configIds.isEmpty()) { + promptTypeToConfigIds.remove(config.getPromptType()); + } + logger.info("已从提示词类型 [{}] 的映射中移除配置 {}", config.getPromptType(), config.getId()); + } + } + + /** + * 获取优化提示词内容列表 * @param promptType 提示词类型 + * @return 优化提示词内容列表 */ - private void disableConfigsByType(String promptType) { - configStorage.values().stream().filter(config -> promptType.equals(config.getPromptType())).forEach(config -> { - config.setEnabled(false); - config.setUpdateTime(LocalDateTime.now()); - }); - promptTypeToConfigId.remove(promptType); + public List getOptimizationConfigs(String promptType) { + return getActiveConfigsByType(promptType); } /** - * 获取自定义提示词内容,如果没有自定义配置则返回null + * 获取自定义提示词内容,如果没有自定义配置则返回null(兼容旧接口) * @param promptType 提示词类型 * @return 自定义提示词内容 */ public String getCustomPromptContent(String promptType) { - UserPromptConfig config = getActiveConfigByType(promptType); - return config != null ? config.getSystemPrompt() : null; + List configs = getActiveConfigsByType(promptType); + if (!configs.isEmpty()) { + return configs.get(0).getOptimizationPrompt(); + } + return null; } /** @@ -226,7 +254,7 @@ public String getCustomPromptContent(String promptType) { * @return 是否有自定义配置 */ public boolean hasCustomConfig(String promptType) { - return getActiveConfigByType(promptType) != null; + return !getActiveConfigsByType(promptType).isEmpty(); } } diff --git a/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/resources/prompts/report-generator.txt b/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/resources/prompts/report-generator.txt index 2bb425f96b..03a279de26 100644 --- a/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/resources/prompts/report-generator.txt +++ b/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/resources/prompts/report-generator.txt @@ -49,4 +49,6 @@ ## 总结建议要求 {summary_and_recommendations} +{optimization_section} + 请根据以上信息生成一份专业、全面的数据分析报告。 diff --git a/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-management/src/main/java/com/alibaba/cloud/ai/controller/PromptConfigController.java b/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-management/src/main/java/com/alibaba/cloud/ai/controller/PromptConfigController.java index f8995cb047..54deea0039 100644 --- a/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-management/src/main/java/com/alibaba/cloud/ai/controller/PromptConfigController.java +++ b/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-management/src/main/java/com/alibaba/cloud/ai/controller/PromptConfigController.java @@ -29,7 +29,7 @@ import java.util.Map; /** - * 用户提示词配置管理 + * 用户提示词优化配置管理 支持多个优化配置同时生效,增强原始提示词而不是完全替换 * * @author Makoto */ @@ -47,30 +47,30 @@ public PromptConfigController(UserPromptConfigService promptConfigService) { } /** - * 创建或更新提示词配置 + * 创建或更新提示词优化配置 * @param configDTO 配置数据 * @return 操作结果 */ @PostMapping("/save") public ResponseEntity> saveConfig(@RequestBody PromptConfigDTO configDTO) { try { - logger.info("保存提示词配置请求:{}", configDTO); + logger.info("保存提示词优化配置请求:{}", configDTO); UserPromptConfig savedConfig = promptConfigService.saveOrUpdateConfig(configDTO); Map response = new HashMap<>(); response.put("success", true); - response.put("message", "配置保存成功"); + response.put("message", "优化配置保存成功"); response.put("data", savedConfig); return ResponseEntity.ok(response); } catch (Exception e) { - logger.error("保存提示词配置失败", e); + logger.error("保存提示词优化配置失败", e); Map response = new HashMap<>(); response.put("success", false); - response.put("message", "配置保存失败:" + e.getMessage()); + response.put("message", "优化配置保存失败:" + e.getMessage()); return ResponseEntity.badRequest().body(response); } @@ -165,7 +165,7 @@ public ResponseEntity> getConfigsByType(@PathVariable String } /** - * 获取当前启用的配置 + * 获取当前启用的配置(兼容旧接口) * @param promptType 提示词类型 * @return 当前启用的配置 */ @@ -192,6 +192,35 @@ public ResponseEntity> getActiveConfig(@PathVariable String } } + /** + * 获取某个类型的所有启用的优化配置 + * @param promptType 提示词类型 + * @return 启用的优化配置列表 + */ + @GetMapping("/active-all/{promptType}") + public ResponseEntity> getActiveConfigs(@PathVariable String promptType) { + try { + List configs = promptConfigService.getActiveConfigsByType(promptType); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("data", configs); + response.put("total", configs.size()); + response.put("hasOptimizationConfigs", !configs.isEmpty()); + + return ResponseEntity.ok(response); + } + catch (Exception e) { + logger.error("获取启用配置列表失败", e); + + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "获取启用配置列表失败:" + e.getMessage()); + + return ResponseEntity.badRequest().body(response); + } + } + /** * 删除配置 * @param id 配置ID diff --git a/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-web-ui/src/components/PromptOptimizationConfig.vue b/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-web-ui/src/components/PromptOptimizationConfig.vue new file mode 100644 index 0000000000..43580c1e45 --- /dev/null +++ b/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-web-ui/src/components/PromptOptimizationConfig.vue @@ -0,0 +1,639 @@ + + + + + + \ No newline at end of file diff --git a/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-web-ui/src/views/AgentDetail.vue b/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-web-ui/src/views/AgentDetail.vue index ff374efb61..5899704a34 100644 --- a/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-web-ui/src/views/AgentDetail.vue +++ b/spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-web-ui/src/views/AgentDetail.vue @@ -464,20 +464,10 @@
-
-

自定义Prompt配置(待实现)

-

TODO:这里配置的Prompt仅用作效果优化,需支持多个提示词配置,系统已内置提示词
如:
1. 查询的年销售额精确到小数点后两位。
2. 报告格式第一章节请先总结年销售额

-
-
-
- - -
-
- -
-
+
@@ -1406,11 +1396,13 @@ import { ref, reactive, onMounted, onUnmounted, computed } from 'vue' import { useRouter, useRoute } from 'vue-router' import { agentApi, businessKnowledgeApi, semanticModelApi, datasourceApi, presetQuestionApi } from '../utils/api.js' import AgentDebugPanel from '../components/AgentDebugPanel.vue' +import PromptOptimizationConfig from '../components/PromptOptimizationConfig.vue' export default { name: 'AgentDetail', components: { - AgentDebugPanel + AgentDebugPanel, + PromptOptimizationConfig }, setup() { const router = useRouter() @@ -3785,11 +3777,6 @@ html { border: 1px solid #d9d9d9; } -/* Prompt配置样式 */ -.prompt-config-section { - max-width: 800px; -} - /* 审计日志样式 */ /* 初始化信息源样式 */ .schema-init-section { From 4063f6eadee4aa9529f17789fd826bcdfa501d9e Mon Sep 17 00:00:00 2001 From: Makoto <2762006003@qq.com> Date: Tue, 12 Aug 2025 14:48:58 +0800 Subject: [PATCH 02/10] fix conflict --- .../pom.xml | 91 -- .../spring-configuration-metadata.json | 75 -- ...ot.autoconfigure.AutoConfiguration.imports | 20 - .../spring-configuration-metadata.json | 74 -- .../pom.xml | 14 +- .../mcp/client/NacosMcpAutoConfiguration.java | 0 .../NacosMcpClientAutoConfiguration.java | 0 .../client/NacosMcpSseClientProperties.java | 0 ...NacosMcpToolCallbackAutoConfiguration.java | 0 ...sMcpTransportBuilderAutoConfiguration.java | 0 .../NacosMcpRegisterAutoConfiguration.java | 4 +- .../tracing/McpTracingAutoConfiguration.java | 0 .../spring-configuration-metadata.json | 32 +- ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../pom.xml | 14 +- .../McpGatewayServerAutoConfiguration.java | 2 +- .../McpGatewaySseServerAutoConfiguration.java | 2 +- ...ewayStreamableServerAutoConfiguration.java | 2 +- .../NacosMcpGatewayAutoConfiguration.java | 2 +- .../NacosMcpRouterAutoConfiguration.java | 141 +++ .../spring-configuration-metadata.json | 175 ++++ ...ot.autoconfigure.AutoConfiguration.imports | 6 +- .../pom.xml | 62 -- .../spring-configuration-metadata.json | 108 -- ...ot.autoconfigure.AutoConfiguration.imports | 18 - pom.xml | 11 +- spring-ai-alibaba-bom/pom.xml | 48 +- .../advisor/CompositeDocumentRetriever.java | 201 ++++ .../ai/advisor/DocumentRetrievalAdvisor.java | 51 +- .../CompositeDocumentRetrieverTests.java | 688 +++++++++++++ .../docker-compose-middleware.yml | 73 ++ spring-ai-alibaba-deepresearch/pom.xml | 14 +- .../controller/RagDataController.java | 10 +- .../controller/graph/GraphProcess.java | 31 +- .../deepresearch/model/ApiResponse.java | 42 + .../src/main/resources/application.yml | 11 + .../ui-vue3/src/base/i18n/zh.ts | 2 +- .../ui-vue3/src/components/layout/index.vue | 8 +- .../ui-vue3/src/components/report/index.vue | 87 +- .../ui-vue3/src/router/defaultRoutes.ts | 9 +- .../ui-vue3/src/store/ConfigStore.ts | 4 +- .../ui-vue3/src/store/KnowledgeStore.ts | 156 +++ .../ui-vue3/src/store/MessageStore.ts | 83 +- .../ui-vue3/src/types/message.ts | 48 + .../ui-vue3/src/types/node.ts | 25 +- .../ui-vue3/src/utils/jsonParser.ts | 44 +- .../ui-vue3/src/utils/request.ts | 14 +- .../ui-vue3/src/views/chat/index.vue | 356 +++---- .../ui-vue3/src/views/config/index.vue | 12 - .../src/views/knowledge/Management.vue | 583 +++++++++++ .../ui-vue3/src/views/knowledge/index.vue | 11 +- .../cloud/ai/graph/node/AnswerNode.java | 18 +- .../ai/graph/node/DocumentExtractorNode.java | 103 +- .../cloud/ai/graph/node/IterationNode.java | 2 +- .../ai/graph/node/KnowledgeRetrievalNode.java | 81 +- .../cloud/ai/graph/node/ListOperatorNode.java | 410 ++------ .../ai/graph/node/ParameterParsingNode.java | 27 +- .../ai/graph/node/VariableAggregatorNode.java | 4 +- .../streaming/StreamingChatGenerator.java | 2 +- .../node/KnowledgeRetrievalNodeTest.java | 27 +- .../ai/graph/node/ListOperatorNodeTest.java | 120 ++- .../node/VariableAggregatorNodeTest.java | 2 +- .../com/alibaba/cloud/ai/model/Variable.java | 1 + .../cloud/ai/model/VariableSelector.java | 14 + .../cloud/ai/model/workflow/NodeType.java | 4 +- .../workflow/nodedata/AnswerNodeData.java | 4 +- .../workflow/nodedata/AssignerNodeData.java | 13 +- .../workflow/nodedata/BranchNodeData.java | 11 - .../model/workflow/nodedata/CodeNodeData.java | 5 + .../nodedata/DocumentExtractorNodeData.java | 28 +- .../model/workflow/nodedata/EndNodeData.java | 5 + .../model/workflow/nodedata/HttpNodeData.java | 9 + .../workflow/nodedata/IterationNodeData.java | 33 +- .../nodedata/KnowledgeRetrievalNodeData.java | 28 +- .../model/workflow/nodedata/LLMNodeData.java | 4 +- .../nodedata/ListOperatorNodeData.java | 80 +- .../nodedata/ParameterParsingNodeData.java | 37 +- .../nodedata/QuestionClassifierNodeData.java | 4 +- .../workflow/nodedata/RetrieverNodeData.java | 268 ----- .../nodedata/TemplateTransformNodeData.java | 5 + .../model/workflow/nodedata/ToolNodeData.java | 6 + .../nodedata/VariableAggregatorNodeData.java | 10 + .../ai/service/dsl/NodeDataConverter.java | 71 +- .../service/dsl/adapters/DifyDSLAdapter.java | 80 +- .../dsl/nodes/AnswerNodeDataConverter.java | 22 +- .../dsl/nodes/AssignerNodeDataConverter.java | 42 +- .../dsl/nodes/BranchNodeDataConverter.java | 16 +- .../dsl/nodes/CodeNodeDataConverter.java | 12 +- .../DocumentExtractorNodeDataConverter.java | 32 +- .../dsl/nodes/EmptyNodeDataConverter.java | 11 - .../dsl/nodes/EndNodeDataConverter.java | 13 +- .../dsl/nodes/HttpNodeDataConverter.java | 80 +- .../dsl/nodes/IterationNodeDataConverter.java | 56 +- .../KnowledgeRetrievalNodeDataConverter.java | 29 +- .../dsl/nodes/LLMNodeDataConverter.java | 49 +- .../nodes/ListOperatorNodeDataConverter.java | 113 ++- .../ParameterParsingNodeDataConverter.java | 78 +- .../QuestionClassifyNodeDataConverter.java | 26 +- .../dsl/nodes/RetrieverNodeDataConverter.java | 112 --- .../dsl/nodes/StartNodeDataConverter.java | 20 +- .../TemplateTransformNodeDataConverter.java | 45 +- .../dsl/nodes/ToolNodeDataConverter.java | 9 + .../VariableAggregatorNodeDataConverter.java | 60 +- .../generator/workflow/NodeSection.java | 22 +- .../workflow/WorkflowProjectGenerator.java | 211 +--- .../workflow/sections/AnswerNodeSection.java | 7 +- .../sections/AssignerNodeSection.java | 8 +- .../workflow/sections/BranchNodeSection.java | 102 +- .../workflow/sections/CodeNodeSection.java | 25 +- .../DocumentExtractorNodeSection.java | 13 +- .../workflow/sections/EmptyNodeSection.java | 2 +- .../workflow/sections/EndNodeSection.java | 28 +- .../workflow/sections/HttpNodeSection.java | 20 +- .../workflow/sections/HumanNodeSection.java | 2 +- .../sections/IterationNodeSection.java | 2 +- .../KnowledgeRetrievalNodeSection.java | 38 +- .../workflow/sections/LLMNodeSection.java | 19 +- .../sections/ListOperatorNodeSection.java | 72 +- .../workflow/sections/MCPNodeSection.java | 2 +- .../sections/ParameterParsingNodeSection.java | 37 +- .../QuestionClassifierNodeSection.java | 63 +- .../workflow/sections/StartNodeSection.java | 3 +- .../TemplateTransformNodeSection.java | 4 +- .../workflow/sections/ToolNodeSection.java | 9 +- .../VariableAggregatorNodeSection.java | 110 +- .../templates/GraphBuilder.java.mustache | 1 + .../GraphRunController.java.mustache | 2 +- .../nodes/AnswerNodeDataConverterTest.java | 19 - ...emplateTransformNodeDataConverterTest.java | 19 +- spring-ai-alibaba-mcp/pom.xml | 4 +- .../spring-ai-alibaba-mcp-discovery/pom.xml | 80 -- .../tracing/McpTracingComprehensiveTest.java | 356 ------- .../spring-ai-alibaba-mcp-gateway/pom.xml | 28 - .../pom.xml | 77 -- .../README.md | 98 -- .../pom.xml | 72 -- .../pom.xml | 6 +- .../WebFluxSseClientTransportBuilder.java | 0 .../LoadbalancedAsyncMcpToolCallback.java | 0 ...dbalancedAsyncMcpToolCallbackProvider.java | 0 .../tool/LoadbalancedSyncMcpToolCallback.java | 0 ...adbalancedSyncMcpToolCallbackProvider.java | 0 .../transport/LoadbalancedMcpAsyncClient.java | 0 .../transport/LoadbalancedMcpSyncClient.java | 0 .../utils/ApplicationContextHolder.java | 0 .../client/utils/NacosMcpClientUtils.java | 0 .../ai/mcp/register/NacosMcpRegister.java | 0 .../register/NacosMcpRegisterProperties.java | 0 .../mcp/register/utils/JsonSchemaUtils.java | 0 .../spring-ai-alibaba-mcp-router/pom.xml | 79 +- .../core/AbstractMcpGatewayToolsWatcher.java | 0 .../gateway}/core/McpGatewayProperties.java | 0 .../core/McpGatewayToolCallbackProvider.java | 0 .../core/McpGatewayToolDefinition.java | 0 .../gateway}/core/McpGatewayToolManager.java | 0 .../core/McpGatewayToolsInitializer.java | 0 .../jsontemplate/RequestTemplateInfo.java | 0 .../jsontemplate/RequestTemplateParser.java | 0 .../jsontemplate/ResponseTemplateParser.java | 0 .../gateway}/core/utils/SpringBeanUtils.java | 0 .../callback/NacosMcpGatewayToolCallback.java | 0 .../NacosMcpGatewayToolDefinition.java | 0 .../properties/NacosMcpGatewayProperties.java | 0 .../NacosMcpAsyncGatewayToolsProvider.java | 0 .../NacosMcpSyncGatewayToolsProvider.java | 0 .../NacosMcpGatewayToolsInitializer.java | 0 .../watcher/NacosMcpGatewayToolsWatcher.java | 0 .../ai/mcp/router/McpRouterApplication.java | 34 - .../router/config/EmbeddingModelConfig.java | 50 - .../ai/mcp/router/config/McpRouterConfig.java | 103 -- .../ai/mcp/router/core/McpRouterWatcher.java | 25 +- .../nacos/NacosMcpServiceDiscovery.java | 2 - .../mcp/router/service/McpProxyService.java | 2 - .../service/McpRouterManagementService.java | 92 -- .../mcp/router/service/McpRouterService.java | 2 - .../src/main/resources/application.yml | 69 -- spring-ai-alibaba-nl2sql/pom.xml | 5 + .../spring-ai-alibaba-nl2sql-chat/pom.xml | 11 + .../ai/annotation/VectorStoreConfig.java | 2 +- .../cloud/ai/config/MyBatisPlusConfig.java | 70 ++ .../cloud/ai/entity/AgentDatasource.java | 9 + .../alibaba/cloud/ai/entity/Datasource.java | 17 + .../ai/mapper/AgentDatasourceMapper.java | 71 ++ .../cloud/ai/mapper/DatasourceMapper.java | 66 ++ .../ai/mapper/UserPromptConfigMapper.java | 66 ++ .../cloud/ai/service/DatasourceService.java | 207 ++-- .../pom.xml | 11 + .../cloud/ai/config/MyBatisPlusConfig.java | 70 ++ .../cloud/ai/entity/BusinessKnowledge.java | 15 +- .../alibaba/cloud/ai/entity/ChatMessage.java | 11 +- .../alibaba/cloud/ai/entity/ChatSession.java | 12 +- .../cloud/ai/entity/SemanticModel.java | 18 +- .../ai/mapper/BusinessKnowledgeMapper.java | 68 ++ .../cloud/ai/mapper/ChatMessageMapper.java | 53 + .../cloud/ai/mapper/ChatSessionMapper.java | 81 ++ .../cloud/ai/mapper/SemanticModelMapper.java | 68 ++ .../BusinessKnowledgePersistenceService.java | 218 +--- .../cloud/ai/service/ChatMessageService.java | 42 +- .../cloud/ai/service/ChatSessionService.java | 71 +- .../SemanticModelPersistenceService.java | 212 +--- .../src/main/resources/application.yml | 61 +- .../pnpm-lock.yaml | 948 ++++++++++++++++++ .../src/components/AgentDebugPanel.vue | 7 + .../src/views/AgentRun.vue | 12 +- .../pom.xml | 11 +- .../pom.xml | 6 +- .../pom.xml | 69 -- 207 files changed, 6096 insertions(+), 4317 deletions(-) delete mode 100644 auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-core/pom.xml delete mode 100644 auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-core/src/main/resources/META-INF/spring-configuration-metadata.json delete mode 100644 auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports delete mode 100644 auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-nacos/src/main/resources/META-INF/spring-configuration-metadata.json rename auto-configurations/{spring-ai-alibaba-autoconfigure-nacos-mcp-register => spring-ai-alibaba-autoconfigure-mcp-registry}/pom.xml (82%) rename auto-configurations/{spring-ai-alibaba-autoconfigure-nacos-mcp-discovery => spring-ai-alibaba-autoconfigure-mcp-registry}/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpAutoConfiguration.java (100%) rename auto-configurations/{spring-ai-alibaba-autoconfigure-nacos-mcp-discovery => spring-ai-alibaba-autoconfigure-mcp-registry}/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpClientAutoConfiguration.java (100%) rename auto-configurations/{spring-ai-alibaba-autoconfigure-nacos-mcp-discovery => spring-ai-alibaba-autoconfigure-mcp-registry}/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpSseClientProperties.java (100%) rename auto-configurations/{spring-ai-alibaba-autoconfigure-nacos-mcp-discovery => spring-ai-alibaba-autoconfigure-mcp-registry}/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpToolCallbackAutoConfiguration.java (100%) rename auto-configurations/{spring-ai-alibaba-autoconfigure-nacos-mcp-discovery => spring-ai-alibaba-autoconfigure-mcp-registry}/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpTransportBuilderAutoConfiguration.java (100%) rename auto-configurations/{spring-ai-alibaba-autoconfigure-nacos-mcp-register => spring-ai-alibaba-autoconfigure-mcp-registry}/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/register/NacosMcpRegisterAutoConfiguration.java (97%) rename auto-configurations/{spring-ai-alibaba-autoconfigure-nacos-mcp-discovery => spring-ai-alibaba-autoconfigure-mcp-registry}/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/tracing/McpTracingAutoConfiguration.java (100%) rename auto-configurations/{spring-ai-alibaba-autoconfigure-nacos-mcp-register => spring-ai-alibaba-autoconfigure-mcp-registry}/src/main/resources/META-INF/spring-configuration-metadata.json (72%) rename auto-configurations/{spring-ai-alibaba-autoconfigure-nacos-mcp-discovery => spring-ai-alibaba-autoconfigure-mcp-registry}/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (92%) rename auto-configurations/{spring-ai-alibaba-autoconfigure-mcp-gateway-nacos => spring-ai-alibaba-autoconfigure-mcp-router}/pom.xml (70%) rename auto-configurations/{spring-ai-alibaba-autoconfigure-mcp-gateway-core/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway => spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/core}/McpGatewayServerAutoConfiguration.java (99%) rename auto-configurations/{spring-ai-alibaba-autoconfigure-mcp-gateway-core/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway => spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/core}/McpGatewaySseServerAutoConfiguration.java (99%) rename auto-configurations/{spring-ai-alibaba-autoconfigure-mcp-gateway-core/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway => spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/core}/McpGatewayStreamableServerAutoConfiguration.java (99%) rename auto-configurations/{spring-ai-alibaba-autoconfigure-mcp-gateway-nacos => spring-ai-alibaba-autoconfigure-mcp-router}/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/nacos/NacosMcpGatewayAutoConfiguration.java (97%) create mode 100644 auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/router/NacosMcpRouterAutoConfiguration.java create mode 100644 auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/resources/META-INF/spring-configuration-metadata.json rename auto-configurations/{spring-ai-alibaba-autoconfigure-mcp-gateway-nacos => spring-ai-alibaba-autoconfigure-mcp-router}/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (66%) delete mode 100644 auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-discovery/pom.xml delete mode 100644 auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-discovery/src/main/resources/META-INF/spring-configuration-metadata.json delete mode 100644 auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-register/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/advisor/CompositeDocumentRetriever.java create mode 100644 spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/advisor/CompositeDocumentRetrieverTests.java create mode 100644 spring-ai-alibaba-deepresearch/docker-compose-middleware.yml create mode 100644 spring-ai-alibaba-deepresearch/src/main/java/com/alibaba/cloud/ai/example/deepresearch/model/ApiResponse.java create mode 100644 spring-ai-alibaba-deepresearch/ui-vue3/src/store/KnowledgeStore.ts create mode 100644 spring-ai-alibaba-deepresearch/ui-vue3/src/types/message.ts create mode 100644 spring-ai-alibaba-deepresearch/ui-vue3/src/views/knowledge/Management.vue delete mode 100644 spring-ai-alibaba-graph/spring-ai-alibaba-graph-studio/src/main/java/com/alibaba/cloud/ai/model/workflow/nodedata/RetrieverNodeData.java delete mode 100644 spring-ai-alibaba-graph/spring-ai-alibaba-graph-studio/src/main/java/com/alibaba/cloud/ai/service/dsl/nodes/RetrieverNodeDataConverter.java delete mode 100644 spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-discovery/pom.xml delete mode 100644 spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-discovery/src/test/java/com/alibaba/cloud/ai/mcp/discovery/tracing/McpTracingComprehensiveTest.java delete mode 100644 spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-gateway/pom.xml delete mode 100644 spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-gateway/spring-ai-alibaba-mcp-gateway-core/pom.xml delete mode 100644 spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-gateway/spring-ai-alibaba-mcp-gateway-nacos/README.md delete mode 100644 spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-gateway/spring-ai-alibaba-mcp-gateway-nacos/pom.xml rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-register => spring-ai-alibaba-mcp-registry}/pom.xml (93%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-discovery => spring-ai-alibaba-mcp-registry}/src/main/java/com/alibaba/cloud/ai/mcp/discovery/client/builder/WebFluxSseClientTransportBuilder.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-discovery => spring-ai-alibaba-mcp-registry}/src/main/java/com/alibaba/cloud/ai/mcp/discovery/client/tool/LoadbalancedAsyncMcpToolCallback.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-discovery => spring-ai-alibaba-mcp-registry}/src/main/java/com/alibaba/cloud/ai/mcp/discovery/client/tool/LoadbalancedAsyncMcpToolCallbackProvider.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-discovery => spring-ai-alibaba-mcp-registry}/src/main/java/com/alibaba/cloud/ai/mcp/discovery/client/tool/LoadbalancedSyncMcpToolCallback.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-discovery => spring-ai-alibaba-mcp-registry}/src/main/java/com/alibaba/cloud/ai/mcp/discovery/client/tool/LoadbalancedSyncMcpToolCallbackProvider.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-discovery => spring-ai-alibaba-mcp-registry}/src/main/java/com/alibaba/cloud/ai/mcp/discovery/client/transport/LoadbalancedMcpAsyncClient.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-discovery => spring-ai-alibaba-mcp-registry}/src/main/java/com/alibaba/cloud/ai/mcp/discovery/client/transport/LoadbalancedMcpSyncClient.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-discovery => spring-ai-alibaba-mcp-registry}/src/main/java/com/alibaba/cloud/ai/mcp/discovery/client/utils/ApplicationContextHolder.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-discovery => spring-ai-alibaba-mcp-registry}/src/main/java/com/alibaba/cloud/ai/mcp/discovery/client/utils/NacosMcpClientUtils.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-register => spring-ai-alibaba-mcp-registry}/src/main/java/com/alibaba/cloud/ai/mcp/register/NacosMcpRegister.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-register => spring-ai-alibaba-mcp-registry}/src/main/java/com/alibaba/cloud/ai/mcp/register/NacosMcpRegisterProperties.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-register => spring-ai-alibaba-mcp-registry}/src/main/java/com/alibaba/cloud/ai/mcp/register/utils/JsonSchemaUtils.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-gateway/spring-ai-alibaba-mcp-gateway-core/src/main/java/com.alibaba.cloud.ai.mcp.gateway => spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/gateway}/core/AbstractMcpGatewayToolsWatcher.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-gateway/spring-ai-alibaba-mcp-gateway-core/src/main/java/com.alibaba.cloud.ai.mcp.gateway => spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/gateway}/core/McpGatewayProperties.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-gateway/spring-ai-alibaba-mcp-gateway-core/src/main/java/com.alibaba.cloud.ai.mcp.gateway => spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/gateway}/core/McpGatewayToolCallbackProvider.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-gateway/spring-ai-alibaba-mcp-gateway-core/src/main/java/com.alibaba.cloud.ai.mcp.gateway => spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/gateway}/core/McpGatewayToolDefinition.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-gateway/spring-ai-alibaba-mcp-gateway-core/src/main/java/com.alibaba.cloud.ai.mcp.gateway => spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/gateway}/core/McpGatewayToolManager.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-gateway/spring-ai-alibaba-mcp-gateway-core/src/main/java/com.alibaba.cloud.ai.mcp.gateway => spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/gateway}/core/McpGatewayToolsInitializer.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-gateway/spring-ai-alibaba-mcp-gateway-core/src/main/java/com.alibaba.cloud.ai.mcp.gateway => spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/gateway}/core/jsontemplate/RequestTemplateInfo.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-gateway/spring-ai-alibaba-mcp-gateway-core/src/main/java/com.alibaba.cloud.ai.mcp.gateway => spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/gateway}/core/jsontemplate/RequestTemplateParser.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-gateway/spring-ai-alibaba-mcp-gateway-core/src/main/java/com.alibaba.cloud.ai.mcp.gateway => spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/gateway}/core/jsontemplate/ResponseTemplateParser.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-gateway/spring-ai-alibaba-mcp-gateway-core/src/main/java/com.alibaba.cloud.ai.mcp.gateway => spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/gateway}/core/utils/SpringBeanUtils.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-gateway/spring-ai-alibaba-mcp-gateway-nacos => spring-ai-alibaba-mcp-router}/src/main/java/com/alibaba/cloud/ai/mcp/gateway/nacos/callback/NacosMcpGatewayToolCallback.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-gateway/spring-ai-alibaba-mcp-gateway-nacos => spring-ai-alibaba-mcp-router}/src/main/java/com/alibaba/cloud/ai/mcp/gateway/nacos/definition/NacosMcpGatewayToolDefinition.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-gateway/spring-ai-alibaba-mcp-gateway-nacos => spring-ai-alibaba-mcp-router}/src/main/java/com/alibaba/cloud/ai/mcp/gateway/nacos/properties/NacosMcpGatewayProperties.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-gateway/spring-ai-alibaba-mcp-gateway-nacos => spring-ai-alibaba-mcp-router}/src/main/java/com/alibaba/cloud/ai/mcp/gateway/nacos/provider/NacosMcpAsyncGatewayToolsProvider.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-gateway/spring-ai-alibaba-mcp-gateway-nacos => spring-ai-alibaba-mcp-router}/src/main/java/com/alibaba/cloud/ai/mcp/gateway/nacos/provider/NacosMcpSyncGatewayToolsProvider.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-gateway/spring-ai-alibaba-mcp-gateway-nacos => spring-ai-alibaba-mcp-router}/src/main/java/com/alibaba/cloud/ai/mcp/gateway/nacos/tools/NacosMcpGatewayToolsInitializer.java (100%) rename spring-ai-alibaba-mcp/{spring-ai-alibaba-mcp-gateway/spring-ai-alibaba-mcp-gateway-nacos => spring-ai-alibaba-mcp-router}/src/main/java/com/alibaba/cloud/ai/mcp/gateway/nacos/watcher/NacosMcpGatewayToolsWatcher.java (100%) delete mode 100644 spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/McpRouterApplication.java delete mode 100644 spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/config/EmbeddingModelConfig.java delete mode 100644 spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/config/McpRouterConfig.java delete mode 100644 spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/service/McpRouterManagementService.java delete mode 100644 spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/resources/application.yml create mode 100644 spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/config/MyBatisPlusConfig.java create mode 100644 spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/mapper/AgentDatasourceMapper.java create mode 100644 spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/mapper/DatasourceMapper.java create mode 100644 spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/mapper/UserPromptConfigMapper.java create mode 100644 spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-management/src/main/java/com/alibaba/cloud/ai/config/MyBatisPlusConfig.java create mode 100644 spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-management/src/main/java/com/alibaba/cloud/ai/mapper/BusinessKnowledgeMapper.java create mode 100644 spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-management/src/main/java/com/alibaba/cloud/ai/mapper/ChatMessageMapper.java create mode 100644 spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-management/src/main/java/com/alibaba/cloud/ai/mapper/ChatSessionMapper.java create mode 100644 spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-management/src/main/java/com/alibaba/cloud/ai/mapper/SemanticModelMapper.java create mode 100644 spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-web-ui/pnpm-lock.yaml rename spring-ai-alibaba-spring-boot-starters/{spring-ai-alibaba-starter-nacos-mcp-register => spring-ai-alibaba-starter-mcp-registry}/pom.xml (86%) rename spring-ai-alibaba-spring-boot-starters/{spring-ai-alibaba-starter-mcp-gateway-nacos => spring-ai-alibaba-starter-mcp-router}/pom.xml (91%) delete mode 100644 spring-ai-alibaba-spring-boot-starters/spring-ai-alibaba-starter-nacos-mcp-discovery/pom.xml diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-core/pom.xml b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-core/pom.xml deleted file mode 100644 index c0e6ab585a..0000000000 --- a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-core/pom.xml +++ /dev/null @@ -1,91 +0,0 @@ - - - 4.0.0 - - com.alibaba.cloud.ai - spring-ai-alibaba - ${revision} - ../../pom.xml - - - spring-ai-alibaba-autoconfigure-mcp-gateway-core - - Spring AI Alibaba Mcp Gateway Autoconfiguration - Spring AI Alibaba Mcp Gateway Autoconfiguration - https://github.com/alibaba/spring-ai-alibaba - - git://github.com/alibaba/spring-ai-alibaba.git - git@github.com:alibaba/spring-ai-alibaba.git - https://github.com/alibaba/spring-ai-alibaba - - - - - - com.alibaba.cloud.ai - spring-ai-alibaba-mcp-gateway-core - ${revision} - - - - - - - - - - org.springframework.ai - spring-ai-starter-mcp-server - true - - - io.modelcontextprotocol.sdk - mcp - - - - - - org.springframework.ai - spring-ai-autoconfigure-mcp-server - true - - - - org.springframework.ai - spring-ai-retry - - - - org.springframework.boot - spring-boot-starter - true - - - - org.springframework.boot - spring-boot-starter-web - true - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - - org.springframework.boot - spring-boot-starter-test - test - - - - org.junit.jupiter - junit-jupiter-api - test - - - - diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-core/src/main/resources/META-INF/spring-configuration-metadata.json b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-core/src/main/resources/META-INF/spring-configuration-metadata.json deleted file mode 100644 index 7038a67f35..0000000000 --- a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-core/src/main/resources/META-INF/spring-configuration-metadata.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "groups": [ - { - "name": "spring.ai.alibaba.mcp.gateway", - "type": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties", - "sourceType": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties", - "description": "Configuration properties for MCP gateway service discovery." - } - ], - "properties": [ - { - "name": "spring.ai.alibaba.mcp.gateway.enabled", - "type": "java.lang.Boolean", - "description": "Enable or disable the MCP gateway service discovery.", - "sourceType": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties", - "defaultValue": true - }, - { - "name": "spring.ai.alibaba.mcp.gateway.registry", - "type": "java.lang.String", - "description": "Which registry center gateway discovery of MCP server info from .", - "sourceType": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties", - "defaultValue": "nacos" - }, - { - "name": "spring.ai.alibaba.mcp.gateway.sse.enabled", - "type": "java.lang.Boolean", - "description": "Enable or disable SSE transport for MCP Gateway.", - "sourceType": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties$SseConfig", - "defaultValue": true - }, - { - "name": "spring.ai.alibaba.mcp.gateway.sse.endpoint", - "type": "java.lang.String", - "description": "SSE endpoint path for MCP Gateway.", - "sourceType": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties$SseConfig", - "defaultValue": "/sse" - }, - { - "name": "spring.ai.alibaba.mcp.gateway.sse.protocol-version", - "type": "java.lang.String", - "description": "Protocol version for SSE transport.", - "sourceType": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties$SseConfig", - "defaultValue": "2025-03-26" - }, - { - "name": "spring.ai.alibaba.mcp.gateway.streamable.enabled", - "type": "java.lang.Boolean", - "description": "Enable or disable Streamable HTTP transport for MCP Gateway.", - "sourceType": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties$StreamableConfig", - "defaultValue": false - }, - { - "name": "spring.ai.alibaba.mcp.gateway.streamable.endpoint", - "type": "java.lang.String", - "description": "Streamable HTTP endpoint path for MCP Gateway.", - "sourceType": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties$StreamableConfig", - "defaultValue": "/streamable" - }, - { - "name": "spring.ai.alibaba.mcp.gateway.streamable.protocol-version", - "type": "java.lang.String", - "description": "Protocol version for Streamable HTTP transport.", - "sourceType": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties$StreamableConfig", - "defaultValue": "2025-06-18" - }, - { - "name": "spring.ai.alibaba.mcp.gateway.message-endpoint", - "type": "java.lang.String", - "description": "Message endpoint for MCP Gateway.", - "defaultValue": "/mcp/message" - } - ], - "hints": [] -} diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index 67dfd2d3e5..0000000000 --- a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1,20 +0,0 @@ -# -# Copyright 2025-2025 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -com.alibaba.cloud.ai.autoconfigure.mcp.gateway.McpGatewayServerAutoConfiguration -com.alibaba.cloud.ai.autoconfigure.mcp.gateway.McpGatewaySseServerAutoConfiguration -com.alibaba.cloud.ai.autoconfigure.mcp.gateway.McpGatewayStreamableServerAutoConfiguration - - diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-nacos/src/main/resources/META-INF/spring-configuration-metadata.json b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-nacos/src/main/resources/META-INF/spring-configuration-metadata.json deleted file mode 100644 index d6482ecc14..0000000000 --- a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-nacos/src/main/resources/META-INF/spring-configuration-metadata.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "groups": [ - { - "name": "spring.ai.alibaba.mcp.nacos", - "type": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", - "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", - "description": "Configuration properties for Nacos MCP integration." - }, - { - "name": "spring.ai.alibaba.mcp.gateway.nacos", - "type": "com.alibaba.cloud.ai.mcp.gateway.nacos.properties.NacosMcpGatewayProperties", - "sourceType": "com.alibaba.cloud.ai.mcp.nacos.gateway.properties.NacosMcpGatewayProperties", - "description": "Configuration properties for Nacos MCP gateway service discovery." - } - ], - "properties": [ - { - "name": "spring.ai.alibaba.mcp.nacos.server-addr", - "type": "java.lang.String", - "description": "Nacos server address.", - "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", - "defaultValue": "localhost:8848" - }, - { - "name": "spring.ai.alibaba.mcp.nacos.namespace", - "type": "java.lang.String", - "description": "Nacos namespace for MCP service.", - "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", - "defaultValue": "" - }, - { - "name": "spring.ai.alibaba.mcp.nacos.username", - "type": "java.lang.String", - "description": "Nacos username to authenticate.", - "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", - "defaultValue": "" - }, - { - "name": "spring.ai.alibaba.mcp.nacos.password", - "type": "java.lang.String", - "description": "Nacos password to authenticate.", - "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", - "defaultValue": "" - }, - { - "name": "spring.ai.alibaba.mcp.nacos.access-key", - "type": "java.lang.String", - "description": "Nacos access key to authenticate.", - "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", - "defaultValue": "" - }, - { - "name": "spring.ai.alibaba.mcp.nacos.secret-key", - "type": "java.lang.String", - "description": "Nacos secret key to authenticate.", - "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", - "defaultValue": "" - }, - { - "name": "spring.ai.alibaba.mcp.nacos.endpoint", - "type": "java.lang.String", - "description": "Nacos server endpoint.", - "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", - "defaultValue": "" - }, - { - "name": "spring.ai.alibaba.mcp.gateway.nacos.service-names", - "type": "java.util.List", - "description": "Service names for gateway discovery of MCP servers.", - "sourceType": "com.alibaba.cloud.ai.mcp.gateway.nacos.properties.NacosMcpGatewayProperties" - } - ], - "hints": [] -} diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-register/pom.xml b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry/pom.xml similarity index 82% rename from auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-register/pom.xml rename to auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry/pom.xml index 6e2a34fa38..34d13f846d 100644 --- a/auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-register/pom.xml +++ b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry/pom.xml @@ -8,10 +8,10 @@ ../../pom.xml - spring-ai-alibaba-autoconfigure-nacos-mcp-register + spring-ai-alibaba-autoconfigure-mcp-registry - Spring AI Alibaba Nacos Mcp Register Autoconfiguration - Spring AI Alibaba Nacos Mcp Register Autoconfiguration + Spring AI Alibaba Nacos Mcp Registry Autoconfiguration + Spring AI Alibaba Nacos Mcp Registry Autoconfiguration https://github.com/alibaba/spring-ai-alibaba git://github.com/alibaba/spring-ai-alibaba.git @@ -23,7 +23,7 @@ com.alibaba.cloud.ai - spring-ai-alibaba-mcp-register + spring-ai-alibaba-mcp-registry ${revision} @@ -44,6 +44,12 @@ true + + org.springframework.ai + spring-ai-starter-mcp-client-webflux + true + + org.springframework.boot spring-boot-configuration-processor diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-discovery/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpAutoConfiguration.java b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpAutoConfiguration.java similarity index 100% rename from auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-discovery/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpAutoConfiguration.java rename to auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpAutoConfiguration.java diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-discovery/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpClientAutoConfiguration.java b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpClientAutoConfiguration.java similarity index 100% rename from auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-discovery/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpClientAutoConfiguration.java rename to auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpClientAutoConfiguration.java diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-discovery/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpSseClientProperties.java b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpSseClientProperties.java similarity index 100% rename from auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-discovery/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpSseClientProperties.java rename to auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpSseClientProperties.java diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-discovery/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpToolCallbackAutoConfiguration.java b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpToolCallbackAutoConfiguration.java similarity index 100% rename from auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-discovery/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpToolCallbackAutoConfiguration.java rename to auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpToolCallbackAutoConfiguration.java diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-discovery/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpTransportBuilderAutoConfiguration.java b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpTransportBuilderAutoConfiguration.java similarity index 100% rename from auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-discovery/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpTransportBuilderAutoConfiguration.java rename to auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/client/NacosMcpTransportBuilderAutoConfiguration.java diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-register/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/register/NacosMcpRegisterAutoConfiguration.java b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/register/NacosMcpRegisterAutoConfiguration.java similarity index 97% rename from auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-register/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/register/NacosMcpRegisterAutoConfiguration.java rename to auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/register/NacosMcpRegisterAutoConfiguration.java index 845be5eb7b..19c11dc835 100644 --- a/auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-register/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/register/NacosMcpRegisterAutoConfiguration.java +++ b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/register/NacosMcpRegisterAutoConfiguration.java @@ -43,8 +43,8 @@ @EnableConfigurationProperties({ NacosMcpRegisterProperties.class, NacosMcpProperties.class, McpServerProperties.class }) @AutoConfiguration(after = McpServerAutoConfiguration.class) -@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) +@ConditionalOnProperty(prefix = NacosMcpRegisterProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = false) public class NacosMcpRegisterAutoConfiguration { @Bean diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-discovery/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/tracing/McpTracingAutoConfiguration.java b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/tracing/McpTracingAutoConfiguration.java similarity index 100% rename from auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-discovery/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/tracing/McpTracingAutoConfiguration.java rename to auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/tracing/McpTracingAutoConfiguration.java diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-register/src/main/resources/META-INF/spring-configuration-metadata.json b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry/src/main/resources/META-INF/spring-configuration-metadata.json similarity index 72% rename from auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-register/src/main/resources/META-INF/spring-configuration-metadata.json rename to auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry/src/main/resources/META-INF/spring-configuration-metadata.json index 5afeefd90e..088b4b8f34 100644 --- a/auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-register/src/main/resources/META-INF/spring-configuration-metadata.json +++ b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry/src/main/resources/META-INF/spring-configuration-metadata.json @@ -11,6 +11,12 @@ "type": "com.alibaba.cloud.ai.mcp.register.NacosMcpRegisterProperties", "sourceType": "com.alibaba.cloud.ai.mcp.register.NacosMcpRegisterProperties", "description": "Configuration properties for Nacos MCP server registration." + }, + { + "name": "spring.ai.alibaba.mcp.nacos.client.sse", + "type": "com.alibaba.cloud.ai.autoconfigure.mcp.client.NacosMcpSseClientProperties", + "sourceType": "com.alibaba.cloud.ai.autoconfigure.mcp.client.NacosMcpSseClientProperties", + "description": "Configuration properties for Nacos MCP SSE client connections." } ], "properties": [ @@ -26,7 +32,7 @@ "type": "java.lang.String", "description": "Nacos namespace for MCP service.", "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", - "defaultValue": "" + "defaultValue": "public" }, { "name": "spring.ai.alibaba.mcp.nacos.username", @@ -63,6 +69,12 @@ "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", "defaultValue": "" }, + { + "name": "spring.ai.alibaba.mcp.nacos.ip", + "type": "java.lang.String", + "description": "Local IP address for MCP service registration.", + "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties" + }, { "name": "spring.ai.alibaba.mcp.nacos.register.enabled", "type": "java.lang.Boolean", @@ -91,11 +103,29 @@ "sourceType": "com.alibaba.cloud.ai.mcp.register.NacosMcpRegisterProperties", "defaultValue": "/mcp" }, + { + "name": "spring.ai.alibaba.mcp.nacos.client.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable Nacos MCP client functionality.", + "defaultValue": false + }, + { + "name": "spring.ai.alibaba.mcp.nacos.client.sse.connections", + "type": "java.util.Map", + "description": "SSE client connections configuration for Nacos MCP services.", + "sourceType": "com.alibaba.cloud.ai.autoconfigure.mcp.client.NacosMcpSseClientProperties" + }, { "name": "spring.ai.mcp.server.enabled", "type": "java.lang.Boolean", "description": "Whether to enable MCP server.", "defaultValue": true + }, + { + "name": "spring.ai.mcp.client.type", + "type": "java.lang.String", + "description": "MCP client type (SYNC or ASYNC).", + "defaultValue": "SYNC" } ], "hints": [] diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-discovery/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports similarity index 92% rename from auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-discovery/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports rename to auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index fe5f3ada2d..e1a33162cd 100644 --- a/auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-discovery/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +com.alibaba.cloud.ai.autoconfigure.mcp.register.NacosMcpRegisterAutoConfiguration com.alibaba.cloud.ai.autoconfigure.mcp.client.NacosMcpAutoConfiguration com.alibaba.cloud.ai.autoconfigure.mcp.client.NacosMcpClientAutoConfiguration com.alibaba.cloud.ai.autoconfigure.mcp.client.NacosMcpToolCallbackAutoConfiguration diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-nacos/pom.xml b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/pom.xml similarity index 70% rename from auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-nacos/pom.xml rename to auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/pom.xml index 6138ca9ab3..e275380d18 100644 --- a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-nacos/pom.xml +++ b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/pom.xml @@ -8,10 +8,10 @@ ../../pom.xml - spring-ai-alibaba-autoconfigure-mcp-gateway-nacos + spring-ai-alibaba-autoconfigure-mcp-router - Spring AI Alibaba Nacos Mcp Gateway Autoconfiguration - Spring AI Alibaba Nacos Mcp Gateway Autoconfiguration + Spring AI Alibaba Nacos Mcp Router Autoconfiguration + Spring AI Alibaba Nacos Mcp Router Autoconfiguration https://github.com/alibaba/spring-ai-alibaba git://github.com/alibaba/spring-ai-alibaba.git @@ -23,13 +23,7 @@ com.alibaba.cloud.ai - spring-ai-alibaba-mcp-gateway-nacos - ${revision} - - - - com.alibaba.cloud.ai - spring-ai-alibaba-autoconfigure-mcp-gateway-core + spring-ai-alibaba-mcp-router ${revision} diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-core/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/McpGatewayServerAutoConfiguration.java b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/core/McpGatewayServerAutoConfiguration.java similarity index 99% rename from auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-core/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/McpGatewayServerAutoConfiguration.java rename to auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/core/McpGatewayServerAutoConfiguration.java index c447752dcf..5f80c1ec00 100644 --- a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-core/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/McpGatewayServerAutoConfiguration.java +++ b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/core/McpGatewayServerAutoConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.alibaba.cloud.ai.autoconfigure.mcp.gateway; +package com.alibaba.cloud.ai.autoconfigure.mcp.gateway.core; import com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties; import com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayToolCallbackProvider; diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-core/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/McpGatewaySseServerAutoConfiguration.java b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/core/McpGatewaySseServerAutoConfiguration.java similarity index 99% rename from auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-core/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/McpGatewaySseServerAutoConfiguration.java rename to auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/core/McpGatewaySseServerAutoConfiguration.java index 90bbdcfaaf..79e434c36b 100644 --- a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-core/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/McpGatewaySseServerAutoConfiguration.java +++ b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/core/McpGatewaySseServerAutoConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.alibaba.cloud.ai.autoconfigure.mcp.gateway; +package com.alibaba.cloud.ai.autoconfigure.mcp.gateway.core; import com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-core/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/McpGatewayStreamableServerAutoConfiguration.java b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/core/McpGatewayStreamableServerAutoConfiguration.java similarity index 99% rename from auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-core/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/McpGatewayStreamableServerAutoConfiguration.java rename to auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/core/McpGatewayStreamableServerAutoConfiguration.java index 7ba3e9c082..d01c733f7e 100644 --- a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-core/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/McpGatewayStreamableServerAutoConfiguration.java +++ b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/core/McpGatewayStreamableServerAutoConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.alibaba.cloud.ai.autoconfigure.mcp.gateway; +package com.alibaba.cloud.ai.autoconfigure.mcp.gateway.core; import com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-nacos/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/nacos/NacosMcpGatewayAutoConfiguration.java b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/nacos/NacosMcpGatewayAutoConfiguration.java similarity index 97% rename from auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-nacos/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/nacos/NacosMcpGatewayAutoConfiguration.java rename to auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/nacos/NacosMcpGatewayAutoConfiguration.java index 73c0308e05..c3b7b36607 100644 --- a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-nacos/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/nacos/NacosMcpGatewayAutoConfiguration.java +++ b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/gateway/nacos/NacosMcpGatewayAutoConfiguration.java @@ -16,7 +16,7 @@ package com.alibaba.cloud.ai.autoconfigure.mcp.gateway.nacos; -import com.alibaba.cloud.ai.autoconfigure.mcp.gateway.McpGatewayServerAutoConfiguration; +import com.alibaba.cloud.ai.autoconfigure.mcp.gateway.core.McpGatewayServerAutoConfiguration; import com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayToolManager; import com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayToolsInitializer; import com.alibaba.cloud.ai.mcp.gateway.nacos.properties.NacosMcpGatewayProperties; diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/router/NacosMcpRouterAutoConfiguration.java b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/router/NacosMcpRouterAutoConfiguration.java new file mode 100644 index 0000000000..db97711a61 --- /dev/null +++ b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/router/NacosMcpRouterAutoConfiguration.java @@ -0,0 +1,141 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.ai.autoconfigure.mcp.router; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel; +import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingOptions; +import com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties; +import com.alibaba.cloud.ai.mcp.nacos.service.NacosMcpOperationService; +import com.alibaba.cloud.ai.mcp.router.config.McpRouterProperties; +import com.alibaba.cloud.ai.mcp.router.core.McpRouterWatcher; +import com.alibaba.cloud.ai.mcp.router.core.discovery.McpServiceDiscovery; +import com.alibaba.cloud.ai.mcp.router.core.vectorstore.McpServerVectorStore; +import com.alibaba.cloud.ai.mcp.router.core.vectorstore.SimpleMcpServerVectorStore; +import com.alibaba.cloud.ai.mcp.router.nacos.NacosMcpServiceDiscovery; +import com.alibaba.cloud.ai.mcp.router.service.McpProxyService; +import com.alibaba.cloud.ai.mcp.router.service.McpRouterService; +import com.alibaba.nacos.api.exception.NacosException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.document.MetadataMode; +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties; +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.ai.tool.method.MethodToolCallbackProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +import java.util.Properties; + +/** + * @author aias00 + */ +@EnableConfigurationProperties({ McpRouterProperties.class, NacosMcpProperties.class, McpServerProperties.class }) +@ConditionalOnProperty(prefix = McpRouterProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) +public class NacosMcpRouterAutoConfiguration { + + private static final Logger log = LoggerFactory.getLogger(NacosMcpRouterAutoConfiguration.class); + + @Value("${spring.ai.dashscope.api-key:default_api_key}") + private String apiKey; + + @Bean + @ConditionalOnMissingBean + public EmbeddingModel embeddingModel() { + if (apiKey == null || apiKey.isEmpty() || "default_api_key".equals(apiKey)) { + throw new IllegalArgumentException("Environment variable DASHSCOPE_API_KEY is not set."); + } + DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(apiKey).build(); + + return new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, + DashScopeEmbeddingOptions.builder().withModel("text-embedding-v2").build()); + } + + @Bean + @ConditionalOnMissingBean(NacosMcpOperationService.class) + public NacosMcpOperationService nacosMcpOperationService(NacosMcpProperties nacosMcpProperties) { + Properties nacosProperties = nacosMcpProperties.getNacosProperties(); + try { + return new NacosMcpOperationService(nacosProperties); + } + catch (NacosException e) { + throw new RuntimeException(e); + } + } + + /** + * 配置 MCP 服务发现 + */ + @Bean + @ConditionalOnMissingBean + public McpServiceDiscovery mcpServiceDiscovery(NacosMcpOperationService nacosMcpOperationService) { + return new NacosMcpServiceDiscovery(nacosMcpOperationService); + } + + /** + * 配置 MCP Server 向量存储 + */ + @Bean + @ConditionalOnMissingBean + public McpServerVectorStore mcpServerVectorStore(EmbeddingModel embeddingModel) { + return new SimpleMcpServerVectorStore(embeddingModel); + } + + /** + * 配置 MCP 代理服务 + */ + @Bean + @ConditionalOnMissingBean + public McpProxyService mcpProxyService(NacosMcpOperationService nacosMcpOperationService) { + return new McpProxyService(nacosMcpOperationService); + } + + /** + * 配置 MCP 路由服务 + */ + @Bean + @ConditionalOnMissingBean + public McpRouterService mcpRouterService(McpServiceDiscovery mcpServiceDiscovery, + McpServerVectorStore mcpServerVectorStore, NacosMcpOperationService nacosMcpOperationService, + McpProxyService mcpProxyService) { + return new McpRouterService(mcpServiceDiscovery, mcpServerVectorStore, nacosMcpOperationService, + mcpProxyService); + } + + /** + * 配置 MCP 路由工具回调提供者 + */ + @Bean + public ToolCallbackProvider routerTools(McpRouterService routerService) { + return MethodToolCallbackProvider.builder().toolObjects(routerService).build(); + } + + /** + * 配置 MCP 路由监视器 + */ + @Bean(initMethod = "startScheduledPolling", destroyMethod = "stop") + public McpRouterWatcher mcpRouterWatcher(McpServiceDiscovery mcpServiceDiscovery, + McpServerVectorStore mcpServerVectorStore, McpRouterProperties mcpRouterProperties) { + return new McpRouterWatcher(mcpServiceDiscovery, mcpServerVectorStore, mcpRouterProperties.getServiceNames()); + } + +} diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/resources/META-INF/spring-configuration-metadata.json b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 0000000000..758980d79c --- /dev/null +++ b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,175 @@ +{ + "groups": [ + { + "name": "spring.ai.alibaba.mcp.nacos", + "type": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", + "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", + "description": "Configuration properties for Nacos MCP integration." + }, + { + "name": "spring.ai.alibaba.mcp.router", + "type": "com.alibaba.cloud.ai.mcp.router.config.McpRouterProperties", + "sourceType": "com.alibaba.cloud.ai.mcp.router.config.McpRouterProperties", + "description": "Configuration properties for MCP router service." + }, + { + "name": "spring.ai.alibaba.mcp.gateway", + "type": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties", + "sourceType": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties", + "description": "Configuration properties for MCP Gateway core functionality." + }, + { + "name": "spring.ai.alibaba.mcp.gateway.sse", + "type": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties$SseConfig", + "sourceType": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties$SseConfig", + "description": "Configuration properties for MCP Gateway SSE transport." + }, + { + "name": "spring.ai.alibaba.mcp.gateway.streamable", + "type": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties$StreamableConfig", + "sourceType": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties$StreamableConfig", + "description": "Configuration properties for MCP Gateway Streamable HTTP transport." + }, + { + "name": "spring.ai.alibaba.mcp.gateway.nacos", + "type": "com.alibaba.cloud.ai.mcp.gateway.nacos.properties.NacosMcpGatewayProperties", + "sourceType": "com.alibaba.cloud.ai.mcp.gateway.nacos.properties.NacosMcpGatewayProperties", + "description": "Configuration properties for Nacos MCP gateway service discovery." + } + ], + "properties": [ + { + "name": "spring.ai.alibaba.mcp.nacos.server-addr", + "type": "java.lang.String", + "description": "Nacos server address.", + "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", + "defaultValue": "localhost:8848" + }, + { + "name": "spring.ai.alibaba.mcp.nacos.namespace", + "type": "java.lang.String", + "description": "Nacos namespace for MCP service.", + "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", + "defaultValue": "public" + }, + { + "name": "spring.ai.alibaba.mcp.nacos.username", + "type": "java.lang.String", + "description": "Nacos username to authenticate.", + "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", + "defaultValue": "" + }, + { + "name": "spring.ai.alibaba.mcp.nacos.password", + "type": "java.lang.String", + "description": "Nacos password to authenticate.", + "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", + "defaultValue": "" + }, + { + "name": "spring.ai.alibaba.mcp.nacos.access-key", + "type": "java.lang.String", + "description": "Nacos access key to authenticate.", + "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", + "defaultValue": "" + }, + { + "name": "spring.ai.alibaba.mcp.nacos.secret-key", + "type": "java.lang.String", + "description": "Nacos secret key to authenticate.", + "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", + "defaultValue": "" + }, + { + "name": "spring.ai.alibaba.mcp.nacos.endpoint", + "type": "java.lang.String", + "description": "Nacos server endpoint.", + "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", + "defaultValue": "" + }, + { + "name": "spring.ai.alibaba.mcp.router.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable MCP router service.", + "sourceType": "com.alibaba.cloud.ai.mcp.router.config.McpRouterProperties", + "defaultValue": true + }, + { + "name": "spring.ai.alibaba.mcp.router.service-names", + "type": "java.util.List", + "description": "List of MCP service names for router to manage.", + "sourceType": "com.alibaba.cloud.ai.mcp.router.config.McpRouterProperties", + "defaultValue": [] + }, + { + "name": "spring.ai.alibaba.mcp.gateway.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable MCP Gateway functionality.", + "sourceType": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties", + "defaultValue": true + }, + { + "name": "spring.ai.alibaba.mcp.gateway.registry", + "type": "java.lang.String", + "description": "Registry type for MCP Gateway (e.g., nacos).", + "sourceType": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties", + "defaultValue": "nacos" + }, + { + "name": "spring.ai.alibaba.mcp.gateway.message-endpoint", + "type": "java.lang.String", + "description": "Message endpoint for MCP Gateway.", + "sourceType": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties", + "defaultValue": "/message" + }, + { + "name": "spring.ai.alibaba.mcp.gateway.sse.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable SSE transport for MCP Gateway.", + "sourceType": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties$SseConfig", + "defaultValue": true + }, + { + "name": "spring.ai.alibaba.mcp.gateway.sse.endpoint", + "type": "java.lang.String", + "description": "SSE endpoint for MCP Gateway.", + "sourceType": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties$SseConfig", + "defaultValue": "/sse" + }, + { + "name": "spring.ai.alibaba.mcp.gateway.sse.protocol-version", + "type": "java.lang.String", + "description": "SSE protocol version for MCP Gateway.", + "sourceType": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties$SseConfig", + "defaultValue": "2025-03-26" + }, + { + "name": "spring.ai.alibaba.mcp.gateway.streamable.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable Streamable HTTP transport for MCP Gateway.", + "sourceType": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties$StreamableConfig", + "defaultValue": false + }, + { + "name": "spring.ai.alibaba.mcp.gateway.streamable.endpoint", + "type": "java.lang.String", + "description": "Streamable HTTP endpoint for MCP Gateway.", + "sourceType": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties$StreamableConfig", + "defaultValue": "/streamable" + }, + { + "name": "spring.ai.alibaba.mcp.gateway.streamable.protocol-version", + "type": "java.lang.String", + "description": "Streamable HTTP protocol version for MCP Gateway.", + "sourceType": "com.alibaba.cloud.ai.mcp.gateway.core.McpGatewayProperties$StreamableConfig", + "defaultValue": "2025-06-18" + }, + { + "name": "spring.ai.alibaba.mcp.gateway.nacos.service-names", + "type": "java.util.List", + "description": "Service names for gateway discovery of MCP servers.", + "sourceType": "com.alibaba.cloud.ai.mcp.gateway.nacos.properties.NacosMcpGatewayProperties" + } + ], + "hints": [] +} diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-nacos/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports similarity index 66% rename from auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-nacos/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports rename to auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 48c253767b..86898007b0 100644 --- a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-nacos/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -13,6 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. # -com.alibaba.cloud.ai.autoconfigure.mcp.gateway.nacos.NacosMcpGatewayAutoConfiguration +com.alibaba.cloud.ai.autoconfigure.mcp.router.NacosMcpRouterAutoConfiguration +com.alibaba.cloud.ai.autoconfigure.mcp.gateway.core.McpGatewayServerAutoConfiguration +com.alibaba.cloud.ai.autoconfigure.mcp.gateway.core.McpGatewaySseServerAutoConfiguration +com.alibaba.cloud.ai.autoconfigure.mcp.gateway.core.McpGatewayStreamableServerAutoConfiguration +com.alibaba.cloud.ai.autoconfigure.mcp.gateway.nacos.NacosMcpGatewayAutoConfiguration diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-discovery/pom.xml b/auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-discovery/pom.xml deleted file mode 100644 index e077e48493..0000000000 --- a/auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-discovery/pom.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - 4.0.0 - - com.alibaba.cloud.ai - spring-ai-alibaba - ${revision} - ../../pom.xml - - - spring-ai-alibaba-autoconfigure-nacos-mcp-discovery - - Spring AI Alibaba Nacos Mcp Discovery Autoconfiguration - Spring AI Alibaba Nacos Mcp Discovery Autoconfiguration - https://github.com/alibaba/spring-ai-alibaba - - git://github.com/alibaba/spring-ai-alibaba.git - git@github.com:alibaba/spring-ai-alibaba.git - https://github.com/alibaba/spring-ai-alibaba - - - - - - com.alibaba.cloud.ai - spring-ai-alibaba-mcp-discovery - ${revision} - - - - org.springframework.ai - spring-ai-starter-mcp-client-webflux - true - - - - org.springframework.boot - spring-boot-starter - true - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - - org.springframework.boot - spring-boot-starter-test - test - - - - org.junit.jupiter - junit-jupiter-api - test - - - - diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-discovery/src/main/resources/META-INF/spring-configuration-metadata.json b/auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-discovery/src/main/resources/META-INF/spring-configuration-metadata.json deleted file mode 100644 index 951db0f5b9..0000000000 --- a/auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-discovery/src/main/resources/META-INF/spring-configuration-metadata.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "groups": [ - { - "name": "spring.ai.mcp.client", - "type": "org.springframework.ai.mcp.client.autoconfigure.properties.McpClientCommonProperties", - "sourceType": "org.springframework.ai.mcp.client.autoconfigure.properties.McpClientCommonProperties", - "description": "Configuration properties for MCP client." - }, - { - "name": "spring.ai.alibaba.mcp.nacos", - "type": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", - "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", - "description": "Configuration properties for Nacos MCP integration." - }, - { - "name": "spring.ai.alibaba.mcp.nacos.client.sse", - "type": "com.alibaba.cloud.ai.autoconfigure.mcp.client.NacosMcpSseClientProperties", - "sourceType": "com.alibaba.cloud.ai.autoconfigure.mcp.client.NacosMcpSseClientProperties", - "description": "Configuration properties for Nacos MCP SSE client." - } - ], - "properties": [ - { - "name": "spring.ai.mcp.client.nacos-enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable Nacos MCP client integration.", - "sourceType": "org.springframework.ai.mcp.client.autoconfigure.properties.McpClientCommonProperties", - "defaultValue": false - }, - { - "name": "spring.ai.mcp.client.type", - "type": "java.lang.String", - "description": "Type of MCP client, either SYNC (synchronous) or ASYNC (asynchronous).", - "sourceType": "org.springframework.ai.mcp.client.autoconfigure.properties.McpClientCommonProperties", - "defaultValue": "SYNC" - }, - { - "name": "spring.ai.alibaba.mcp.nacos.client.sse.connections", - "type": "java.util.Map", - "description": "Mapping of server keys to server names for Nacos MCP SSE connections.", - "sourceType": "com.alibaba.cloud.ai.autoconfigure.mcp.client.NacosMcpSseClientProperties" - }, - { - "name": "spring.ai.alibaba.mcp.nacos.server-addr", - "type": "java.lang.String", - "description": "Nacos server address in the format of 'host:port'.", - "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", - "defaultValue": "localhost:8848" - }, - { - "name": "spring.ai.alibaba.mcp.nacos.namespace", - "type": "java.lang.String", - "description": "Nacos namespace ID. Empty string means using Nacos default namespace.", - "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", - "defaultValue": "" - }, - { - "name": "spring.ai.alibaba.mcp.nacos.username", - "type": "java.lang.String", - "description": "Nacos username for authentication when Nacos auth is enabled.", - "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", - "defaultValue": "" - }, - { - "name": "spring.ai.alibaba.mcp.nacos.password", - "type": "java.lang.String", - "description": "Nacos password for authentication when Nacos auth is enabled.", - "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", - "defaultValue": "" - }, - { - "name": "spring.ai.alibaba.mcp.nacos.access-key", - "type": "java.lang.String", - "description": "Nacos access key for ACM authentication.", - "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", - "defaultValue": "" - }, - { - "name": "spring.ai.alibaba.mcp.nacos.secret-key", - "type": "java.lang.String", - "description": "Nacos secret key for ACM authentication.", - "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", - "defaultValue": "" - }, - { - "name": "spring.ai.alibaba.mcp.nacos.endpoint", - "type": "java.lang.String", - "description": "Endpoint for Nacos ACM service discovery. Only needed when using Alibaba Cloud ACM.", - "sourceType": "com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties", - "defaultValue": "" - } - ], - "hints": [ - { - "name": "spring.ai.mcp.client.type", - "values": [ - { - "value": "SYNC", - "description": "Use synchronous MCP client implementation." - }, - { - "value": "ASYNC", - "description": "Use asynchronous MCP client implementation." - } - ] - } - ] -} diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-register/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-register/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index bcb98e2dde..0000000000 --- a/auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-register/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1,18 +0,0 @@ -# -# Copyright 2025-2025 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -com.alibaba.cloud.ai.autoconfigure.mcp.register.NacosMcpRegisterAutoConfiguration - - diff --git a/pom.xml b/pom.xml index d358fcc8f6..6a2ece4ecb 100644 --- a/pom.xml +++ b/pom.xml @@ -163,11 +163,9 @@ - auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-discovery - auto-configurations/spring-ai-alibaba-autoconfigure-nacos-mcp-register + auto-configurations/spring-ai-alibaba-autoconfigure-mcp-registry - auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-core - auto-configurations/spring-ai-alibaba-autoconfigure-mcp-gateway-nacos + auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router auto-configurations/spring-ai-alibaba-autoconfigure-nacos-prompt @@ -180,9 +178,8 @@ spring-ai-alibaba-spring-boot-starters/spring-ai-alibaba-starter-dashscope spring-ai-alibaba-spring-boot-starters/spring-ai-alibaba-starter-nacos-prompt spring-ai-alibaba-spring-boot-starters/spring-ai-alibaba-starter-arms-observation - spring-ai-alibaba-spring-boot-starters/spring-ai-alibaba-starter-nacos-mcp-discovery - spring-ai-alibaba-spring-boot-starters/spring-ai-alibaba-starter-nacos-mcp-register - spring-ai-alibaba-spring-boot-starters/spring-ai-alibaba-starter-mcp-gateway-nacos + spring-ai-alibaba-spring-boot-starters/spring-ai-alibaba-starter-mcp-registry + spring-ai-alibaba-spring-boot-starters/spring-ai-alibaba-starter-mcp-router spring-ai-alibaba-spring-boot-starters/spring-ai-alibaba-starter-nl2sql spring-ai-alibaba-spring-boot-starters/spring-ai-alibaba-starter-graph-observation diff --git a/spring-ai-alibaba-bom/pom.xml b/spring-ai-alibaba-bom/pom.xml index 605ec33949..f20ab4de16 100644 --- a/spring-ai-alibaba-bom/pom.xml +++ b/spring-ai-alibaba-bom/pom.xml @@ -96,13 +96,6 @@ spring-ai-alibaba-core ${project.version} - - - - com.alibaba.cloud.ai - spring-ai-alibaba-mcp-gateway-core - ${project.version} - @@ -125,31 +118,13 @@ com.alibaba.cloud.ai - spring-ai-alibaba-starter-nacos2-mcp-client - ${project.version} - - - - com.alibaba.cloud.ai - spring-ai-alibaba-starter-nacos2-mcp-server - ${project.version} - - - - com.alibaba.cloud.ai - spring-ai-alibaba-starter-nacos-mcp-discovery - ${project.version} - - - - com.alibaba.cloud.ai - spring-ai-alibaba-starter-nacos-mcp-register + spring-ai-alibaba-starter-mcp-registry ${project.version} com.alibaba.cloud.ai - spring-ai-alibaba-starter-mcp-gateway-nacos + spring-ai-alibaba-starter-mcp-router ${project.version} @@ -184,18 +159,6 @@ ${project.version} - - com.alibaba.cloud.ai - spring-ai-alibaba-autoconfigure-nacos2-mcp-client - ${project.version} - - - - com.alibaba.cloud.ai - spring-ai-alibaba-autoconfigure-nacos2-mcp-server - ${project.version} - - com.alibaba.cloud.ai spring-ai-alibaba-autoconfigure-nacos-prompt @@ -216,13 +179,6 @@ ${project.version} - - - com.alibaba.cloud.ai - spring-ai-alibaba-mcp-nacos2 - ${project.version} - - diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/advisor/CompositeDocumentRetriever.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/advisor/CompositeDocumentRetriever.java new file mode 100644 index 0000000000..968e05336d --- /dev/null +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/advisor/CompositeDocumentRetriever.java @@ -0,0 +1,201 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.cloud.ai.advisor; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.document.Document; +import org.springframework.ai.rag.Query; +import org.springframework.ai.rag.retrieval.search.DocumentRetriever; +import org.springframework.util.Assert; + +/** + * Composite document retriever that combines multiple document retrievers. + * + * @author mengnankkkk + * @since 1.0.0-M2 + */ +public class CompositeDocumentRetriever implements DocumentRetriever { + + private static final Logger logger = LoggerFactory.getLogger(CompositeDocumentRetriever.class); + + private final List retrievers; + + private final Integer maxResultsPerRetriever; + + private final ResultMergeStrategy mergeStrategy; + + public enum ResultMergeStrategy { + + SIMPLE_MERGE, // Simple merge strategy + + SCORE_BASED, // Score-based merge strategy + + ROUND_ROBIN// Round-robin merge strategy + + } + + public CompositeDocumentRetriever(List retrievers) { + this(retrievers, 10, ResultMergeStrategy.SCORE_BASED); + } + + public CompositeDocumentRetriever(List retrievers, Integer maxResultsPerRetriever) { + this(retrievers, maxResultsPerRetriever, ResultMergeStrategy.SCORE_BASED); + } + + public CompositeDocumentRetriever(List retrievers, Integer maxResultsPerRetriever, + ResultMergeStrategy mergeStrategy) { + Assert.notNull(retrievers, "Retrievers list must not be null!"); + Assert.isTrue(!retrievers.isEmpty(), "Retrievers list must not be empty!"); + Assert.isTrue(maxResultsPerRetriever > 0, "MaxResultsPerRetriever must be positive!"); + Assert.notNull(mergeStrategy, "MergeStrategy must not be null!"); + + this.retrievers = new ArrayList<>(retrievers); + this.maxResultsPerRetriever = maxResultsPerRetriever; + this.mergeStrategy = mergeStrategy; + } + + @Override + public List retrieve(Query query) { + if (mergeStrategy == ResultMergeStrategy.ROUND_ROBIN) { + return roundRobinRetrieve(query); + } + + List allDocuments = new ArrayList<>(); + + for (DocumentRetriever retriever : retrievers) { + try { + List documents = retriever.retrieve(query); + if (documents != null && !documents.isEmpty()) { + List limitedDocuments = documents.stream() + .limit(maxResultsPerRetriever) + .collect(Collectors.toList()); + allDocuments.addAll(limitedDocuments); + } + } + catch (Exception e) { + logger.error("Error retrieving from one of the retrievers: {}", e.getMessage(), e); + } + } + + return mergeResults(allDocuments); + } + + private List roundRobinRetrieve(Query query) { + List> allResults = new ArrayList<>(); + + for (DocumentRetriever retriever : retrievers) { + try { + List documents = retriever.retrieve(query); + if (documents != null && !documents.isEmpty()) { + + List limitedDocuments = documents.stream() + .limit(maxResultsPerRetriever) + .collect(Collectors.toList()); + allResults.add(limitedDocuments); + } + else { + allResults.add(new ArrayList<>()); + } + } + catch (Exception e) { + logger.error("Error retrieving from one of the retrievers: {}", e.getMessage(), e); + allResults.add(new ArrayList<>()); + } + } + + Integer maxSize = allResults.stream().mapToInt(List::size).max().orElse(0); + + return java.util.stream.IntStream.range(0, maxSize) + .boxed() + .flatMap(i -> allResults.stream() + .filter(documents -> i < documents.size()) + .map(documents -> documents.get(i))) + .collect(Collectors.toList()); + } + + private List mergeResults(List documents) { + if (documents.isEmpty()) { + return documents; + } + + return switch (mergeStrategy) { + case SIMPLE_MERGE -> documents; + case SCORE_BASED -> documents.stream().sorted((d1, d2) -> { + Double score1 = d1.getScore(); + Double score2 = d2.getScore(); + + if (score1 == null) + score1 = 0.0; + if (score2 == null) + score2 = 0.0; + return Double.compare(score2, score1); + }).collect(Collectors.toList()); + case ROUND_ROBIN -> documents; + default -> documents; + }; + } + + public static class Builder { + + private List retrievers = new ArrayList<>(); + + private Integer maxResultsPerRetriever = 10; + + private ResultMergeStrategy mergeStrategy = ResultMergeStrategy.SCORE_BASED; + + private Builder() { + } + + public Builder addRetriever(DocumentRetriever retriever) { + if (retriever != null) { + this.retrievers.add(retriever); + } + return this; + } + + public Builder retrievers(List retrievers) { + if (retrievers != null) { + this.retrievers.addAll(retrievers); + } + return this; + } + + public Builder maxResultsPerRetriever(Integer maxResultsPerRetriever) { + this.maxResultsPerRetriever = maxResultsPerRetriever; + return this; + } + + public Builder mergeStrategy(ResultMergeStrategy mergeStrategy) { + this.mergeStrategy = mergeStrategy; + return this; + } + + public CompositeDocumentRetriever build() { + return new CompositeDocumentRetriever(retrievers, maxResultsPerRetriever, mergeStrategy); + } + + } + + public static Builder builder() { + return new Builder(); + } + +} diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/advisor/DocumentRetrievalAdvisor.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/advisor/DocumentRetrievalAdvisor.java index 9068b62d92..77640960e7 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/advisor/DocumentRetrievalAdvisor.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/advisor/DocumentRetrievalAdvisor.java @@ -18,7 +18,6 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; - import org.springframework.ai.chat.client.ChatClientRequest; import org.springframework.ai.chat.client.ChatClientResponse; import org.springframework.ai.chat.client.advisor.api.AdvisorChain; @@ -78,8 +77,56 @@ public DocumentRetrievalAdvisor(DocumentRetriever retriever, PromptTemplate prom this.order = order; } - @Override + public DocumentRetrievalAdvisor(List retrievers) { + this(retrievers, DEFAULT_PROMPT_TEMPLATE, DEFAULT_ORDER); + } + + public DocumentRetrievalAdvisor(List retrievers, PromptTemplate promptTemplate) { + this(retrievers, promptTemplate, DEFAULT_ORDER); + } + + public DocumentRetrievalAdvisor(List retrievers, PromptTemplate promptTemplate, int order) { + Assert.notEmpty(retrievers, "The retrievers list must not be null or empty!"); + Assert.notNull(promptTemplate, "The promptTemplate must not be null!"); + + // Create a composite retriever for multiple vector stores + this.retriever = new CompositeDocumentRetriever(retrievers); + this.promptTemplate = promptTemplate; + this.order = order; + } + + /** + * Constructor for multiple vector stores with custom merge strategy + * @param retrievers List of document retrievers for multi-vector store support + * @param mergeStrategy Strategy for merging results from multiple retrievers + * @param maxResultsPerRetriever Maximum results per retriever + */ + public DocumentRetrievalAdvisor(List retrievers, + CompositeDocumentRetriever.ResultMergeStrategy mergeStrategy, int maxResultsPerRetriever) { + this(retrievers, mergeStrategy, maxResultsPerRetriever, DEFAULT_PROMPT_TEMPLATE, DEFAULT_ORDER); + } + + /** + * Constructor for multiple vector stores with full customization + * @param retrievers List of document retrievers for multi-vector store support + * @param mergeStrategy Strategy for merging results from multiple retrievers + * @param maxResultsPerRetriever Maximum results per retriever + * @param promptTemplate Custom prompt template + * @param order Advisor execution order + */ + public DocumentRetrievalAdvisor(List retrievers, + CompositeDocumentRetriever.ResultMergeStrategy mergeStrategy, int maxResultsPerRetriever, + PromptTemplate promptTemplate, int order) { + Assert.notEmpty(retrievers, "The retrievers list must not be null or empty!"); + Assert.notNull(promptTemplate, "The promptTemplate must not be null!"); + // Create a composite retriever for multiple vector stores with custom settings + this.retriever = new CompositeDocumentRetriever(retrievers, maxResultsPerRetriever, mergeStrategy); + this.promptTemplate = promptTemplate; + this.order = order; + } + + @Override public int getOrder() { return this.order; } diff --git a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/advisor/CompositeDocumentRetrieverTests.java b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/advisor/CompositeDocumentRetrieverTests.java new file mode 100644 index 0000000000..c50a7413a8 --- /dev/null +++ b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/advisor/CompositeDocumentRetrieverTests.java @@ -0,0 +1,688 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.cloud.ai.advisor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.ai.document.Document; +import org.springframework.ai.rag.Query; +import org.springframework.ai.rag.retrieval.search.DocumentRetriever; + +class CompositeDocumentRetrieverTests { + + private DocumentRetriever retriever1; + + private DocumentRetriever retriever2; + + private DocumentRetriever retriever3; + + private Query testQuery; + + @BeforeEach + void setUp() { + retriever1 = mock(DocumentRetriever.class); + retriever2 = mock(DocumentRetriever.class); + retriever3 = mock(DocumentRetriever.class); + testQuery = new Query("test query"); + } + + private Document createDocumentWithScore(String id, String content, double score) { + Map metadata = new HashMap<>(); + metadata.put("score", score); + Document doc = new Document(id, content, metadata); + return doc; + } + + @Test + void testConstructorValidation() { + assertThrows(IllegalArgumentException.class, () -> { + new CompositeDocumentRetriever(Arrays.asList()); + }); + + assertThrows(IllegalArgumentException.class, () -> { + new CompositeDocumentRetriever(null); + }); + + assertThrows(IllegalArgumentException.class, () -> { + new CompositeDocumentRetriever(Arrays.asList(retriever1), 0); + }); + } + + @Test + void testSingleRetrieverComposition() { + Document doc1 = createDocumentWithScore("1", "content1", 0.9); + when(retriever1.retrieve(any(Query.class))).thenReturn(Arrays.asList(doc1)); + + CompositeDocumentRetriever composite = new CompositeDocumentRetriever(Arrays.asList(retriever1)); + + List results = composite.retrieve(testQuery); + + assertThat(results).hasSize(1); + assertThat(results.get(0).getId()).isEqualTo("1"); + assertThat(results.get(0).getText()).isEqualTo("content1"); + } + + @Test + void testMultipleRetrieversComposition() { + Document doc1 = createDocumentWithScore("1", "content1", 0.9); + Document doc2 = createDocumentWithScore("2", "content2", 0.8); + Document doc3 = createDocumentWithScore("3", "content3", 0.95); + + when(retriever1.retrieve(any(Query.class))).thenReturn(Arrays.asList(doc1)); + when(retriever2.retrieve(any(Query.class))).thenReturn(Arrays.asList(doc2)); + when(retriever3.retrieve(any(Query.class))).thenReturn(Arrays.asList(doc3)); + + CompositeDocumentRetriever composite = new CompositeDocumentRetriever( + Arrays.asList(retriever1, retriever2, retriever3)); + + List results = composite.retrieve(testQuery); + + assertThat(results).hasSize(3); + assertThat(results.stream().map(Document::getId)).containsExactlyInAnyOrder("1", "2", "3"); + } + + @Test + void testSimpleMergeStrategy() { + Document doc1 = createDocumentWithScore("1", "content1", 0.9); + Document doc2 = createDocumentWithScore("2", "content2", 0.8); + + when(retriever1.retrieve(any(Query.class))).thenReturn(Arrays.asList(doc1)); + when(retriever2.retrieve(any(Query.class))).thenReturn(Arrays.asList(doc2)); + + CompositeDocumentRetriever composite = new CompositeDocumentRetriever(Arrays.asList(retriever1, retriever2), 10, + CompositeDocumentRetriever.ResultMergeStrategy.SIMPLE_MERGE); + + List results = composite.retrieve(testQuery); + + assertThat(results).hasSize(2); + assertThat(results.get(0).getId()).isEqualTo("1"); + assertThat(results.get(1).getId()).isEqualTo("2"); + } + + @Test + void testMaxResultsPerRetrieverLimit() { + Document doc1 = createDocumentWithScore("1", "content1", 0.9); + Document doc2 = createDocumentWithScore("2", "content2", 0.8); + Document doc3 = createDocumentWithScore("3", "content3", 0.7); + Document doc4 = createDocumentWithScore("4", "content4", 0.6); + + when(retriever1.retrieve(any(Query.class))).thenReturn(Arrays.asList(doc1, doc2, doc3, doc4)); + + CompositeDocumentRetriever composite = new CompositeDocumentRetriever(Arrays.asList(retriever1), 2); + + List results = composite.retrieve(testQuery); + + assertThat(results).hasSize(2); + assertThat(results.get(0).getId()).isEqualTo("1"); + assertThat(results.get(1).getId()).isEqualTo("2"); + } + + @Test + void testErrorHandling() { + Document doc2 = createDocumentWithScore("2", "content2", 0.8); + + when(retriever1.retrieve(any(Query.class))).thenThrow(new RuntimeException("Database error")); + when(retriever2.retrieve(any(Query.class))).thenReturn(Arrays.asList(doc2)); + + CompositeDocumentRetriever composite = new CompositeDocumentRetriever(Arrays.asList(retriever1, retriever2)); + + List results = composite.retrieve(testQuery); + + assertThat(results).hasSize(1); + assertThat(results.get(0).getId()).isEqualTo("2"); + } + + @Test + void testBuilderPattern() { + Document doc1 = createDocumentWithScore("1", "content1", 0.9); + when(retriever1.retrieve(any(Query.class))).thenReturn(Arrays.asList(doc1)); + + CompositeDocumentRetriever composite = CompositeDocumentRetriever.builder() + .addRetriever(retriever1) + .addRetriever(retriever2) + .maxResultsPerRetriever(5) + .mergeStrategy(CompositeDocumentRetriever.ResultMergeStrategy.SIMPLE_MERGE) + .build(); + + List results = composite.retrieve(testQuery); + + assertThat(results).hasSize(1); + assertThat(results.get(0).getId()).isEqualTo("1"); + } + + @Test + void testEmptyResults() { + when(retriever1.retrieve(any(Query.class))).thenReturn(Arrays.asList()); + when(retriever2.retrieve(any(Query.class))).thenReturn(null); + + CompositeDocumentRetriever composite = new CompositeDocumentRetriever(Arrays.asList(retriever1, retriever2)); + + List results = composite.retrieve(testQuery); + + assertThat(results).isEmpty(); + } + + @Test + void testRoundRobinMergeStrategy() { + Document doc1 = createDocumentWithScore("1", "content1", 0.9); + Document doc2 = createDocumentWithScore("2", "content2", 0.8); + Document doc3 = createDocumentWithScore("3", "content3", 0.7); + Document doc4 = createDocumentWithScore("4", "content4", 0.95); + + when(retriever1.retrieve(any(Query.class))).thenReturn(Arrays.asList(doc1, doc2)); + when(retriever2.retrieve(any(Query.class))).thenReturn(Arrays.asList(doc3, doc4)); + + CompositeDocumentRetriever composite = new CompositeDocumentRetriever(Arrays.asList(retriever1, retriever2), 10, + CompositeDocumentRetriever.ResultMergeStrategy.ROUND_ROBIN); + + List results = composite.retrieve(testQuery); + + assertThat(results).hasSize(4); + assertThat(results.get(0).getId()).isEqualTo("1"); + assertThat(results.get(1).getId()).isEqualTo("3"); + assertThat(results.get(2).getId()).isEqualTo("2"); + assertThat(results.get(3).getId()).isEqualTo("4"); + } + + @Test + void testRealEnterpriseScenario() { + DocumentRetriever techDocsRetriever = createRealTechDocsRetriever(); + DocumentRetriever policyRetriever = createRealPolicyRetriever(); + DocumentRetriever productRetriever = createRealProductRetriever(); + + CompositeDocumentRetriever composite = CompositeDocumentRetriever.builder() + .addRetriever(techDocsRetriever) + .addRetriever(policyRetriever) + .addRetriever(productRetriever) + .maxResultsPerRetriever(3) + .mergeStrategy(CompositeDocumentRetriever.ResultMergeStrategy.SCORE_BASED) + .build(); + + Query techQuery = new Query("Spring AI API 使用方法"); + List techResults = composite.retrieve(techQuery); + assertThat(techResults).isNotEmpty(); + assertThat(techResults).anyMatch(doc -> doc.getText().contains("Spring AI") && doc.getScore() > 0.0); + + Query securityQuery = new Query("数据安全管理政策"); + List securityResults = composite.retrieve(securityQuery); + assertThat(securityResults).isNotEmpty(); + assertThat(securityResults).anyMatch(doc -> doc.getText().contains("安全") && doc.getScore() > 0.0); + + Query productQuery = new Query("通义千问大语言模型"); + List productResults = composite.retrieve(productQuery); + assertThat(productResults).isNotEmpty(); + assertThat(productResults).anyMatch(doc -> doc.getText().contains("通义千问") && doc.getScore() > 0.0); + } + + @Test + void testDifferentMergeStrategiesWithRealData() { + DocumentRetriever techRetriever = createRealTechDocsRetriever(); + DocumentRetriever policyRetriever = createRealPolicyRetriever(); + + Query testQuery = new Query("微服务架构设计"); + + CompositeDocumentRetriever scoreBasedComposite = CompositeDocumentRetriever.builder() + .addRetriever(techRetriever) + .addRetriever(policyRetriever) + .maxResultsPerRetriever(3) + .mergeStrategy(CompositeDocumentRetriever.ResultMergeStrategy.SCORE_BASED) + .build(); + + List scoreBasedResults = scoreBasedComposite.retrieve(testQuery); + assertThat(scoreBasedResults).isNotEmpty(); + + CompositeDocumentRetriever roundRobinComposite = CompositeDocumentRetriever.builder() + .addRetriever(techRetriever) + .addRetriever(policyRetriever) + .maxResultsPerRetriever(3) + .mergeStrategy(CompositeDocumentRetriever.ResultMergeStrategy.ROUND_ROBIN) + .build(); + + List roundRobinResults = roundRobinComposite.retrieve(testQuery); + assertThat(roundRobinResults).isNotEmpty(); + + CompositeDocumentRetriever simpleMergeComposite = CompositeDocumentRetriever.builder() + .addRetriever(techRetriever) + .addRetriever(policyRetriever) + .maxResultsPerRetriever(3) + .mergeStrategy(CompositeDocumentRetriever.ResultMergeStrategy.SIMPLE_MERGE) + .build(); + + List simpleMergeResults = simpleMergeComposite.retrieve(testQuery); + assertThat(simpleMergeResults).isNotEmpty(); + + assertThat(scoreBasedResults.size()).isPositive(); + assertThat(roundRobinResults.size()).isPositive(); + assertThat(simpleMergeResults.size()).isPositive(); + } + + @Test + void testPerformanceWithLargeDataset() { + DocumentRetriever largeRetriever1 = createLargeDatasetRetriever("技术文档库", 100); + DocumentRetriever largeRetriever2 = createLargeDatasetRetriever("企业政策库", 80); + DocumentRetriever largeRetriever3 = createLargeDatasetRetriever("产品知识库", 120); + + CompositeDocumentRetriever composite = CompositeDocumentRetriever.builder() + .addRetriever(largeRetriever1) + .addRetriever(largeRetriever2) + .addRetriever(largeRetriever3) + .maxResultsPerRetriever(10) + .mergeStrategy(CompositeDocumentRetriever.ResultMergeStrategy.SCORE_BASED) + .build(); + + long startTime = System.currentTimeMillis(); + + Query performanceQuery = new Query("性能优化最佳实践"); + List results = composite.retrieve(performanceQuery); + + long endTime = System.currentTimeMillis(); + long executionTime = endTime - startTime; + + assertThat(results).isNotEmpty(); + assertThat(results.size()).isLessThanOrEqualTo(30); + assertThat(executionTime).isLessThan(5000); + } + + @Test + void testMinimalQueryHandling() { + DocumentRetriever techRetriever = createRealTechDocsRetriever(); + + CompositeDocumentRetriever composite = CompositeDocumentRetriever.builder() + .addRetriever(techRetriever) + .maxResultsPerRetriever(5) + .mergeStrategy(CompositeDocumentRetriever.ResultMergeStrategy.SCORE_BASED) + .build(); + + Query minimalQuery = new Query("a"); + List results = composite.retrieve(minimalQuery); + + assertThat(results).isNotNull(); + assertThat(results).isNotEmpty(); + } + + @Test + void testChineseQuerySupport() { + DocumentRetriever techRetriever = createRealTechDocsRetriever(); + DocumentRetriever policyRetriever = createRealPolicyRetriever(); + + CompositeDocumentRetriever composite = CompositeDocumentRetriever.builder() + .addRetriever(techRetriever) + .addRetriever(policyRetriever) + .maxResultsPerRetriever(3) + .mergeStrategy(CompositeDocumentRetriever.ResultMergeStrategy.SCORE_BASED) + .build(); + + Query chineseQuery = new Query("微服务架构设计原则和最佳实践"); + List results = composite.retrieve(chineseQuery); + + assertThat(results).isNotEmpty(); + assertThat(results).anyMatch(doc -> doc.getText().contains("微服务")); + } + + private DocumentRetriever createRealTechDocsRetriever() { + List techDocs = Arrays.asList( + "Spring AI Alibaba 是阿里巴巴开源的 Spring AI 框架扩展,提供了与阿里云 AI 服务的无缝集成。支持通义千问、向量检索、语音识别等多种 AI 能力。", + "微服务架构设计原则:单一职责、松耦合、高内聚。每个服务应该有明确的边界和职责,通过轻量级通信机制进行交互,如 REST API 或消息队列。", + "微服务部署最佳实践:使用容器化技术,实现服务的独立部署和扩展。建立完善的监控体系,确保微服务系统的可观测性和稳定性。", + "分布式系统故障排查指南:1. 检查服务健康状态;2. 查看日志聚合信息;3. 监控关键指标;4. 分析调用链路;5. 验证配置参数;6. 检查依赖服务状态。", + "API 版本控制最佳实践:使用语义化版本号,向后兼容的更改使用 PATCH 版本,新增功能使用 MINOR 版本,破坏性更改使用 MAJOR 版本。", + "容器化部署优化:合理设置资源限制,使用多阶段构建减小镜像大小,配置健康检查,实现优雅关闭,使用 ConfigMap 和 Secret 管理配置。", + "Redis 缓存设计模式:Cache-Aside 模式适用于读多写少场景,Write-Through 模式确保数据一致性,Write-Behind 模式提高写入性能。", + "数据库连接池配置:最大连接数应根据应用并发量和数据库性能确定,连接超时时间设置为 30 秒,空闲连接回收时间设置为 10 分钟。", + "消息队列使用规范:消息要幂等处理,设置合理的重试机制,使用死信队列处理失败消息,避免消息堆积影响系统性能。"); + + return createMockRetrieverWithRealData("技术文档库", "技术文档", techDocs, "张三丰"); + } + + private DocumentRetriever createRealPolicyRetriever() { + List policies = Arrays.asList("代码审查规范:所有代码必须经过至少一名高级工程师审查才能合并到主分支。审查内容包括代码质量、安全性、性能和可维护性。", + "数据安全管理政策:个人敏感信息必须加密存储,数据库访问需要审计日志,生产环境数据不得在测试环境使用,定期进行安全漏洞扫描。", + "企业级数据安全保护方案:建立完善的数据分类分级制度,实施数据加密传输和存储,部署企业级防火墙和入侵检测系统,制定数据备份和灾难恢复计划。", + "发布流程管理:生产环境发布必须在工作日进行,重大版本发布需要提前 3 天通知,所有发布都要有回滚预案,发布后 2 小时内监控系统状态。", + "开发环境管理规范:开发环境配置要与生产环境保持一致,使用 Docker 容器化开发环境,定期清理无用的开发资源,禁止在开发环境存储真实用户数据。", + "第三方依赖管理:新增第三方库需要安全审查,优先使用公司内部维护的组件,定期更新依赖版本修复安全漏洞,维护依赖关系清单。", + "API 安全规范:所有 API 接口必须实现认证授权,使用 HTTPS 传输,限制请求频率防止滥用,记录 API 调用日志用于审计。", + "员工信息安全培训:新员工入职必须完成信息安全培训,每年进行安全意识考试,及时通报最新安全威胁,建立安全事件应急响应机制。", + "合规性要求:遵守 GDPR 数据保护法规,符合 SOC 2 安全控制标准,通过 ISO 27001 信息安全管理体系认证,定期进行合规性审计。"); + + return createMockRetrieverWithRealData("企业政策库", "企业政策", policies, "李四光"); + } + + private DocumentRetriever createRealProductRetriever() { + List products = Arrays.asList("通义千问大语言模型:支持多轮对话、代码生成、文本总结、翻译等功能。提供 API 接口和 SDK,支持流式输出,具有强大的中文理解能力。", + "向量检索服务:基于深度学习的语义检索引擎,支持文本、图片、音频等多模态数据检索。提供毫秒级响应,支持亿级数据规模。", + "智能客服机器人:集成自然语言处理和知识图谱技术,支持多渠道接入,提供 7x24 小时服务。支持情感识别和个性化回复。", + "文档智能处理:自动识别和提取文档中的关键信息,支持 PDF、Word、Excel 等格式。提供表格识别、印章检测、手写识别等功能。", + "语音识别与合成:支持多种语言和方言,实时语音转文字,自然度极高的语音合成。支持声纹识别和情感化语音合成。", + "图像识别与分析:提供人脸识别、物体检测、场景理解等功能。支持实时图像处理,准确率达到业界领先水平。", + "数据可视化平台:拖拽式图表制作,支持实时数据更新,丰富的图表类型和样式。提供数据钻取和交互式分析功能。", + "AI 模型训练平台:提供完整的机器学习工作流,支持分布式训练,自动调参优化。内置常用算法和预训练模型。"); + + return createMockRetrieverWithRealData("产品知识库", "产品介绍", products, "王五郎"); + } + + private DocumentRetriever createLargeDatasetRetriever(String source, int documentCount) { + List documents = new java.util.ArrayList<>(); + for (int i = 0; i < documentCount; i++) { + documents.add(source + " - 文档 " + i + ":这是一个测试文档,包含性能优化、最佳实践、架构设计等相关内容。用于验证系统在大数据量下的检索性能。"); + } + return createMockRetrieverWithRealData(source, "测试文档", documents, "测试作者"); + } + + private DocumentRetriever createMockRetrieverWithRealData(String source, String category, List documents, + String author) { + return new DocumentRetriever() { + @Override + public List retrieve(Query query) { + List results = new java.util.ArrayList<>(); + for (int i = 0; i < documents.size(); i++) { + Map metadata = new HashMap<>(); + metadata.put("source", source); + metadata.put("category", category); + metadata.put("author", author); + metadata.put("lastUpdated", + "2025-07-" + (20 + i % 5) + "T" + String.format("%02d", 9 + i % 12) + ":00:00Z"); + metadata.put("documentId", source.replaceAll("[^a-zA-Z0-9]", "") + "_" + i); + + double score = calculateRelevanceScore(documents.get(i), query.text()); + + Document document = Document.builder() + .id(source + "_doc_" + i) + .text(documents.get(i)) + .metadata(metadata) + .score(score) + .build(); + + results.add(document); + } + + results.sort((d1, d2) -> Double.compare(d2.getScore(), d1.getScore())); + + return results; + } + }; + } + + private double calculateRelevanceScore(String text, String query) { + if (query == null || query.trim().isEmpty()) { + return 0.5; + } + + String lowerText = text.toLowerCase(); + String lowerQuery = query.toLowerCase(); + double score = 0.1; + + if (lowerText.contains(lowerQuery)) { + score += 0.6; + } + + String[] queryWords = lowerQuery.split("[\\s\\p{Punct}]+"); + int matchCount = 0; + double keywordScore = 0.0; + + for (String word : queryWords) { + if (word.length() > 1) { + if (lowerText.contains(word)) { + matchCount++; + keywordScore += 0.2; + } + else if (isSemanticMatch(word, lowerText)) { + matchCount++; + keywordScore += 0.15; + } + } + } + + score += keywordScore; + + if (queryWords.length > 0) { + double ratio = (double) matchCount / queryWords.length; + score += ratio * 0.25; + + if (ratio >= 0.5) { + score += 0.15; + } + } + + if (text.length() < 200 && matchCount > 0) { + score += 0.05; + } + + return Math.max(0.1, Math.min(score, 1.0)); + } + + private boolean isSemanticMatch(String queryWord, String text) { + if ("安全".equals(queryWord) && text.contains("保护")) + return true; + if ("保护".equals(queryWord) && text.contains("安全")) + return true; + if ("方案".equals(queryWord) && text.contains("政策")) + return true; + if ("政策".equals(queryWord) && text.contains("方案")) + return true; + if ("企业级".equals(queryWord) && text.contains("企业")) + return true; + if ("企业".equals(queryWord) && text.contains("企业级")) + return true; + if ("管理".equals(queryWord) && text.contains("配置")) + return true; + if ("配置".equals(queryWord) && text.contains("管理")) + return true; + if ("最佳实践".equals(queryWord) && text.contains("规范")) + return true; + if ("规范".equals(queryWord) && text.contains("最佳实践")) + return true; + if ("集成".equals(queryWord) && text.contains("整合")) + return true; + if ("整合".equals(queryWord) && text.contains("集成")) + return true; + if ("模型".equals(queryWord) && text.contains("框架")) + return true; + if ("框架".equals(queryWord) && text.contains("模型")) + return true; + return false; + } + + @Test + void testDocumentRetrievalAdvisorWithMultipleVectorStores() { + + DocumentRetriever retriever1 = mock(DocumentRetriever.class); + DocumentRetriever retriever2 = mock(DocumentRetriever.class); + + when(retriever1.retrieve(any(Query.class))).thenReturn(List.of(createDocumentWithScore("1", "文档1", 0.9))); + when(retriever2.retrieve(any(Query.class))).thenReturn(List.of(createDocumentWithScore("2", "文档2", 0.8))); + + List retrievers = Arrays.asList(retriever1, retriever2); + DocumentRetrievalAdvisor advisor = new DocumentRetrievalAdvisor(retrievers); + + assertThat(advisor).isNotNull(); + assertThat(advisor.getOrder()).isEqualTo(0); + + System.out.println("多向量库DocumentRetrievalAdvisor验证成功"); + } + + @Test + void testMultipleVectorStoresWithDefaults() { + + DocumentRetriever techRetriever = mock(DocumentRetriever.class); + DocumentRetriever policyRetriever = mock(DocumentRetriever.class); + DocumentRetriever productRetriever = mock(DocumentRetriever.class); + + when(techRetriever.retrieve(any(Query.class))) + .thenReturn(List.of(createDocumentWithScore("tech", "技术文档:微服务架构", 0.9))); + when(policyRetriever.retrieve(any(Query.class))) + .thenReturn(List.of(createDocumentWithScore("policy", "政策文档:安全规范", 0.8))); + when(productRetriever.retrieve(any(Query.class))) + .thenReturn(List.of(createDocumentWithScore("product", "产品文档:云原生方案", 0.85))); + + List retrievers = Arrays.asList(techRetriever, policyRetriever, productRetriever); + DocumentRetrievalAdvisor advisor = new DocumentRetrievalAdvisor(retrievers); + + assertThat(advisor).isNotNull(); + assertThat(advisor.getOrder()).isEqualTo(0); + + System.out.println("多向量库默认设置功能验证成功"); + } + + @Test + void testMultipleVectorStoresWithCustomStrategy() { + + DocumentRetriever retriever1 = mock(DocumentRetriever.class); + DocumentRetriever retriever2 = mock(DocumentRetriever.class); + + when(retriever1.retrieve(any(Query.class))).thenReturn(List.of(createDocumentWithScore("1", "文档1", 0.9))); + when(retriever2.retrieve(any(Query.class))).thenReturn(List.of(createDocumentWithScore("2", "文档2", 0.8))); + + List retrievers = Arrays.asList(retriever1, retriever2); + DocumentRetrievalAdvisor advisor = new DocumentRetrievalAdvisor(retrievers, + CompositeDocumentRetriever.ResultMergeStrategy.SCORE_BASED, 5); + + assertThat(advisor).isNotNull(); + assertThat(advisor.getOrder()).isEqualTo(0); + + System.out.println("多向量库自定义策略功能验证成功"); + } + + @Test + void testUserChoiceBetweenSingleAndMultiple() { + + DocumentRetriever singleRetriever = mock(DocumentRetriever.class); + DocumentRetriever multiRetriever1 = mock(DocumentRetriever.class); + DocumentRetriever multiRetriever2 = mock(DocumentRetriever.class); + + when(singleRetriever.retrieve(any(Query.class))) + .thenReturn(List.of(createDocumentWithScore("single", "单库结果", 0.9))); + when(multiRetriever1.retrieve(any(Query.class))) + .thenReturn(List.of(createDocumentWithScore("multi1", "多库结果1", 0.8))); + when(multiRetriever2.retrieve(any(Query.class))) + .thenReturn(List.of(createDocumentWithScore("multi2", "多库结果2", 0.7))); + + boolean needMultipleVectorStores = true; + + DocumentRetrievalAdvisor advisor; + if (needMultipleVectorStores) { + + advisor = new DocumentRetrievalAdvisor(Arrays.asList(multiRetriever1, multiRetriever2)); + System.out.println("选择了多向量库调用方式"); + } + else { + + advisor = new DocumentRetrievalAdvisor(singleRetriever); + System.out.println("选择了单向量库调用方式"); + } + + assertThat(advisor).isNotNull(); + System.out.println("用户选择功能验证成功:支持在单向量库和多向量库之间灵活选择"); + } + + @Test + void testBusinessScenarioIntegration() { + System.out.println("=== 业务场景集成测试 ==="); + + String department = "技术部门"; + String queryComplexity = "复杂查询"; + + DocumentRetriever techKB = mock(DocumentRetriever.class); + DocumentRetriever policyKB = mock(DocumentRetriever.class); + DocumentRetriever productKB = mock(DocumentRetriever.class); + + when(techKB.retrieve(any(Query.class))).thenReturn(List.of(createDocumentWithScore("tech", "技术知识库文档", 0.95))); + when(policyKB.retrieve(any(Query.class))) + .thenReturn(List.of(createDocumentWithScore("policy", "政策知识库文档", 0.88))); + when(productKB.retrieve(any(Query.class))) + .thenReturn(List.of(createDocumentWithScore("product", "产品知识库文档", 0.92))); + + DocumentRetrievalAdvisor advisor = createAdvisorForDepartment(department, queryComplexity, techKB, policyKB, + productKB); + + assertThat(advisor).isNotNull(); + System.out.println("部门: " + department + ", 查询类型: " + queryComplexity); + System.out.println("业务场景集成验证成功"); + } + + private DocumentRetrievalAdvisor createAdvisorForDepartment(String department, String queryComplexity, + DocumentRetriever techKB, DocumentRetriever policyKB, DocumentRetriever productKB) { + + if ("技术部门".equals(department)) { + if ("简单查询".equals(queryComplexity)) { + + return new DocumentRetrievalAdvisor(techKB); + } + else { + + return new DocumentRetrievalAdvisor(Arrays.asList(techKB, productKB), + CompositeDocumentRetriever.ResultMergeStrategy.SCORE_BASED, 10); + } + } + else if ("管理部门".equals(department)) { + if ("简单查询".equals(queryComplexity)) { + + return new DocumentRetrievalAdvisor(policyKB); + } + else { + + return new DocumentRetrievalAdvisor(Arrays.asList(policyKB, productKB), + CompositeDocumentRetriever.ResultMergeStrategy.SIMPLE_MERGE, 8); + } + } + else { + + if ("复杂查询".equals(queryComplexity)) { + + return new DocumentRetrievalAdvisor(Arrays.asList(techKB, policyKB, productKB)); + } + else { + + return new DocumentRetrievalAdvisor(techKB); + } + } + } + + @Test + void testCompatibilityWithExistingCode() { + + DocumentRetriever existingRetriever = mock(DocumentRetriever.class); + when(existingRetriever.retrieve(any(Query.class))) + .thenReturn(List.of(createDocumentWithScore("existing", "现有功能文档", 0.9))); + + DocumentRetrievalAdvisor advisor1 = new DocumentRetrievalAdvisor(existingRetriever); + + org.springframework.ai.chat.prompt.PromptTemplate customTemplate = new org.springframework.ai.chat.prompt.PromptTemplate( + "Custom prompt: {query}"); + + DocumentRetrievalAdvisor advisor2 = new DocumentRetrievalAdvisor(existingRetriever, customTemplate); + DocumentRetrievalAdvisor advisor3 = new DocumentRetrievalAdvisor(existingRetriever, customTemplate, 1); + + assertThat(advisor1).isNotNull(); + assertThat(advisor2).isNotNull(); + assertThat(advisor3).isNotNull(); + assertThat(advisor3.getOrder()).isEqualTo(1); + + System.out.println("与现有代码兼容性验证成功:所有现有功能保持不变"); + } + +} diff --git a/spring-ai-alibaba-deepresearch/docker-compose-middleware.yml b/spring-ai-alibaba-deepresearch/docker-compose-middleware.yml new file mode 100644 index 0000000000..9cd795c5e8 --- /dev/null +++ b/spring-ai-alibaba-deepresearch/docker-compose-middleware.yml @@ -0,0 +1,73 @@ +# Copyright 2024-2025 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +version: "3.8" + +# Custom named volumes +volumes: + deep-research-data: + driver: local + +services: + # Redis container configuration + deep-research-redis: + image: redis:7.0 + container_name: deep-research-redis + restart: always + ports: + - "6379:6379" + environment: + - TZ=Asia/Shanghai + volumes: + - ./dockerConfig/redis.conf:/usr/local/etc/redis/redis.conf + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 1s + retries: 3 + command: ["redis-server", "/usr/local/etc/redis/redis.conf"] + + # ElasticSearch container configuration + deep-research-es: + image: docker.elastic.co/elasticsearch/elasticsearch:8.16.1 + container_name: deep-research-es + restart: always + privileged: true + environment: + - "cluster.name=elasticsearch" + - "discovery.type=single-node" + - "ES_JAVA_OPTS=-Xms512m -Xmx1024m" + - "bootstrap.memory_lock=true" + ulimits: + memlock: + soft: -1 + hard: -1 + volumes: + - ./dockerConfig/es.yaml:/usr/share/elasticsearch/config/elasticsearch.yml + - deep-research-data:/usr/share/elasticsearch/data + ports: + - "9200:9200" + - "9300:9300" + deploy: + resources: + limits: + cpus: "2" + memory: 1G + reservations: + memory: 200M + healthcheck: + test: ["CMD-SHELL", "curl -s -o /dev/null -w '%{http_code}' http://localhost:9200 || exit 1"] + interval: 5s + timeout: 5s + retries: 3 diff --git a/spring-ai-alibaba-deepresearch/pom.xml b/spring-ai-alibaba-deepresearch/pom.xml index 320e2d4c21..48281b7520 100644 --- a/spring-ai-alibaba-deepresearch/pom.xml +++ b/spring-ai-alibaba-deepresearch/pom.xml @@ -39,6 +39,10 @@ + + org.springframework.ai + spring-ai-tika-document-reader + com.alibaba.cloud.ai @@ -104,10 +108,6 @@ spring-ai-starter-vector-store-elasticsearch - - org.springframework.ai - spring-ai-tika-document-reader - org.springframework.boot @@ -124,6 +124,12 @@ com.openhtmltopdf openhtmltopdf-pdfbox ${openhtmltopdf.version} + + + pdfbox + org.apache.pdfbox + + diff --git a/spring-ai-alibaba-deepresearch/src/main/java/com/alibaba/cloud/ai/example/deepresearch/controller/RagDataController.java b/spring-ai-alibaba-deepresearch/src/main/java/com/alibaba/cloud/ai/example/deepresearch/controller/RagDataController.java index fd9fdd7763..0a5d6e8b21 100644 --- a/spring-ai-alibaba-deepresearch/src/main/java/com/alibaba/cloud/ai/example/deepresearch/controller/RagDataController.java +++ b/spring-ai-alibaba-deepresearch/src/main/java/com/alibaba/cloud/ai/example/deepresearch/controller/RagDataController.java @@ -16,6 +16,7 @@ package com.alibaba.cloud.ai.example.deepresearch.controller; +import com.alibaba.cloud.ai.example.deepresearch.model.ApiResponse; import com.alibaba.cloud.ai.example.deepresearch.service.VectorStoreDataIngestionService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -158,13 +159,13 @@ public ResponseEntity> handleProfessionalKbUpload(@RequestPa * 批量上传文件到专业知识库ES接口 */ @PostMapping(value = "/professional-kb/batch-upload", consumes = "multipart/form-data") - public ResponseEntity> handleBatchProfessionalKbUpload( + public ResponseEntity>> handleBatchProfessionalKbUpload( @RequestParam("files") List files, @RequestParam("kb_id") String kbId, @RequestParam("kb_name") String kbName, @RequestParam("kb_description") String kbDescription, @RequestParam(value = "category", required = false) String category) { if (files == null || files.isEmpty()) { - return ResponseEntity.badRequest().body(Map.of("error", "No files provided")); + return ResponseEntity.badRequest().body(ApiResponse.error("No files provided")); } try { @@ -181,7 +182,7 @@ public ResponseEntity> handleBatchProfessionalKbUpload( response.put("total_chunks", totalChunks); response.put("filenames", files.stream().map(MultipartFile::getOriginalFilename).toList()); - return ResponseEntity.ok(response); + return ResponseEntity.ok(ApiResponse.success(response)); } catch (Exception e) { Map errorResponse = new HashMap<>(); @@ -189,7 +190,8 @@ public ResponseEntity> handleBatchProfessionalKbUpload( errorResponse.put("message", e.getMessage()); errorResponse.put("kb_id", kbId); errorResponse.put("file_count", files.size()); - return ResponseEntity.internalServerError().body(errorResponse); + return ResponseEntity.internalServerError() + .body(ApiResponse.error("Failed to process professional KB batch upload", errorResponse)); } } diff --git a/spring-ai-alibaba-deepresearch/src/main/java/com/alibaba/cloud/ai/example/deepresearch/controller/graph/GraphProcess.java b/spring-ai-alibaba-deepresearch/src/main/java/com/alibaba/cloud/ai/example/deepresearch/controller/graph/GraphProcess.java index eec8cad3df..cce05f2329 100644 --- a/spring-ai-alibaba-deepresearch/src/main/java/com/alibaba/cloud/ai/example/deepresearch/controller/graph/GraphProcess.java +++ b/spring-ai-alibaba-deepresearch/src/main/java/com/alibaba/cloud/ai/example/deepresearch/controller/graph/GraphProcess.java @@ -29,7 +29,6 @@ import com.alibaba.cloud.ai.graph.exception.GraphRunnerException; import com.alibaba.cloud.ai.graph.state.StateSnapshot; import com.alibaba.cloud.ai.graph.streaming.StreamingOutput; -import com.alibaba.fastjson.JSON; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.core.JsonProcessingException; import org.apache.commons.lang3.StringUtils; @@ -41,6 +40,7 @@ import org.springframework.http.codec.ServerSentEvent; import reactor.core.publisher.Sinks; +import java.io.Serializable; import java.util.Map; import java.util.Objects; import java.util.concurrent.CompletableFuture; @@ -139,9 +139,9 @@ public void processStream(GraphId graphId, AsyncGenerator generator, String nodeName = output.node(); String content; if (output instanceof StreamingOutput streamingOutput) { - logger.debug("Streaming output from node {}: {}", nodeName, - streamingOutput.chatResponse().getResult().getOutput().getText()); - content = buildLLMNodeContent(nodeName, streamingOutput, output); + logger.debug("Streaming output from node {}: {}, {}", nodeName, + streamingOutput.chatResponse().getResult().getOutput().getText(), graphId); + content = buildLLMNodeContent(nodeName, graphId, streamingOutput, output); } else { logger.debug("Normal output from node {}: {}", nodeName, output.state().value("messages")); @@ -190,7 +190,8 @@ public boolean stopGraph(GraphId graphId) { return future.cancel(true); } - private String buildLLMNodeContent(String nodeName, StreamingOutput streamingOutput, NodeOutput output) { + private String buildLLMNodeContent(String nodeName, GraphId graphId, StreamingOutput streamingOutput, + NodeOutput output) { StreamNodePrefixEnum prefixEnum = StreamNodePrefixEnum.match(nodeName); if (prefixEnum == null) { return ""; @@ -201,11 +202,21 @@ private String buildLLMNodeContent(String nodeName, StreamingOutput streamingOut .map(Generation::getMetadata) .map(ChatGenerationMetadata::getFinishReason) .orElse(""); - return JSON.toJSONString(Map.of(nodeName, streamingOutput.chatResponse().getResult().getOutput().getText(), - "step_title", stepTitle, "visible", prefixEnum.isVisible(), "finishReason", finishReason)); + + Map response = Map.of(nodeName, + streamingOutput.chatResponse().getResult().getOutput().getText(), "step_title", stepTitle, "visible", + prefixEnum.isVisible(), "finishReason", finishReason, "graphId", graphId); + + try { + return OBJECT_MAPPER.writeValueAsString(response); + } + catch (JsonProcessingException e) { + throw new RuntimeException("Failed to serialize NodeResponse", e); + } } - private record NodeResponse(String nodeName, String displayTitle, Object content, Object siteInformation) { + private record NodeResponse(String nodeName, GraphId graphId, String displayTitle, Object content, + Object siteInformation) { } private String buildNormalNodeContent(GraphId graphId, String nodeName, NodeOutput output) { @@ -218,7 +229,7 @@ private String buildNormalNodeContent(GraphId graphId, String nodeName, NodeOutp content = switch (nodeEnum) { case START -> { String query = output.state().data().get("query").toString(); - yield Map.of("query", query, "graphId", graphId); + yield Map.of("query", query); } case COORDINATOR -> output.state().data().get("deep_research"); case REWRITE_MULTI_QUERY, HUMAN_FEEDBACK, END -> output.state().data(); @@ -236,7 +247,7 @@ private String buildNormalNodeContent(GraphId graphId, String nodeName, NodeOutp || (Objects.equals(content, "") && Objects.equals(site_information, ""))) { return ""; } - NodeResponse response = new NodeResponse(nodeName, displayTitle, content, site_information); + NodeResponse response = new NodeResponse(nodeName, graphId, displayTitle, content, site_information); try { return OBJECT_MAPPER.writeValueAsString(response); } diff --git a/spring-ai-alibaba-deepresearch/src/main/java/com/alibaba/cloud/ai/example/deepresearch/model/ApiResponse.java b/spring-ai-alibaba-deepresearch/src/main/java/com/alibaba/cloud/ai/example/deepresearch/model/ApiResponse.java new file mode 100644 index 0000000000..8d0663bf23 --- /dev/null +++ b/spring-ai-alibaba-deepresearch/src/main/java/com/alibaba/cloud/ai/example/deepresearch/model/ApiResponse.java @@ -0,0 +1,42 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.ai.example.deepresearch.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record ApiResponse( + + @JsonProperty("code") Integer code, + + @JsonProperty("status") String status, + + @JsonProperty("message") String message, + + @JsonProperty("data") T data) { + + public static ApiResponse success(T data) { + return new ApiResponse<>(200, "success", "", data); + } + + public static ApiResponse error(String message) { + return new ApiResponse<>(500, "error", message, null); + } + + public static ApiResponse error(String message, T data) { + return new ApiResponse<>(500, "error", message, data); + } +} diff --git a/spring-ai-alibaba-deepresearch/src/main/resources/application.yml b/spring-ai-alibaba-deepresearch/src/main/resources/application.yml index 809d56f017..c6bf22ad25 100644 --- a/spring-ai-alibaba-deepresearch/src/main/resources/application.yml +++ b/spring-ai-alibaba-deepresearch/src/main/resources/application.yml @@ -1,6 +1,12 @@ server: port: 8080 spring: + servlet: + multipart: + max-file-size: 10MB + max-request-size: 100MB + enabled: true + file-size-threshold: 1MB profiles: # LangFuse和可观测配置以及专业知识库配置 include: observability, kb @@ -93,6 +99,11 @@ spring: # 启动时加载 classpath下data目录中的所有文件 locations: - "classpath:/data/" + # Elasticsearch配置 + #elasticsearch: + # uris: http://localhost:9200 + # similarity-function: cosine + # 报告导出路径配置 export: path: ${AI_DEEPRESEARCH_EXPORT_PATH} diff --git a/spring-ai-alibaba-deepresearch/ui-vue3/src/base/i18n/zh.ts b/spring-ai-alibaba-deepresearch/ui-vue3/src/base/i18n/zh.ts index 37369812fa..db7bce83c0 100644 --- a/spring-ai-alibaba-deepresearch/ui-vue3/src/base/i18n/zh.ts +++ b/spring-ai-alibaba-deepresearch/ui-vue3/src/base/i18n/zh.ts @@ -32,7 +32,7 @@ const words: I18nType = { chat: '聊天', knowledge_base: '知识库', system_config: '系统配置', - knowledge_management: '知识管理', + knowledge_management: '知识库管理', document_upload: '文档上传', knowledge_search: '知识搜索', } diff --git a/spring-ai-alibaba-deepresearch/ui-vue3/src/components/layout/index.vue b/spring-ai-alibaba-deepresearch/ui-vue3/src/components/layout/index.vue index 1dbf0e8654..695a008c18 100644 --- a/spring-ai-alibaba-deepresearch/ui-vue3/src/components/layout/index.vue +++ b/spring-ai-alibaba-deepresearch/ui-vue3/src/components/layout/index.vue @@ -100,12 +100,12 @@ @@ -186,6 +186,8 @@ import { useMessageStore } from '@/store/MessageStore' const router = useRouter() const route = useRoute() const username = useAuthStore().token +// add navigation handler for knowledge management +const goToKnowledgeManagement = () => router.push('/knowledge/management') const { useToken } = theme const collapse = ref(false) const showConfigView = ref(false) diff --git a/spring-ai-alibaba-deepresearch/ui-vue3/src/components/report/index.vue b/spring-ai-alibaba-deepresearch/ui-vue3/src/components/report/index.vue index 53e442098a..4d8a342812 100644 --- a/spring-ai-alibaba-deepresearch/ui-vue3/src/components/report/index.vue +++ b/spring-ai-alibaba-deepresearch/ui-vue3/src/components/report/index.vue @@ -22,15 +22,16 @@ +
+
- @@ -76,19 +77,21 @@ import { Flex, Button, Modal } from 'ant-design-vue' import { CloseOutlined, LoadingOutlined, CheckCircleOutlined, GlobalOutlined, DownloadOutlined } from '@ant-design/icons-vue' import { parseJsonTextStrict } from '@/utils/jsonParser'; import { useMessageStore } from '@/store/MessageStore' -import { computed, h, watch, onUnmounted, ref } from 'vue' +import { computed, h, watch, onUnmounted, onMounted, ref } from 'vue' import { ThoughtChain, type ThoughtChainProps, type ThoughtChainItem } from 'ant-design-x-vue'; import MD from '@/components/md/index.vue' import HtmlRenderer from '@/components/html/index.vue' import ReferenceSources from '@/components/reference-sources/index.vue' import { XStreamBody } from '@/utils/stream' import request from '@/utils/request' +import type { NormalNode } from '@/types/node'; const messageStore = useMessageStore() interface Props { visible?: boolean convId: string + threadId: string } interface Emits { @@ -98,7 +101,8 @@ interface Emits { const props = withDefaults(defineProps(), { visible: false, - convId: '' + convId: '', + threadId: '' }) // HTML 渲染组件相关状态 @@ -112,45 +116,28 @@ const endContent = ref('') const sources = ref([]) const arrayTemp: ThoughtChainProps['items'] = [] -// 用于控制历史记录的显示 -let isLoading = false // 用于缓存llm_stream节点的内容 const llmStreamCache = new Map() // 从messageStore 拿出消息,然后进行解析并且渲染 const items = computed(() => { // 思维链显示的列表 const array: ThoughtChainProps['items'] = [] - if(!props.convId || !messageStore.history[props.convId]){ + if(!props.threadId || !messageStore.report[props.threadId]){ return array } - // 过滤出非人类的消息 - const messages = messageStore.history[props.convId].filter(item => item.status != 'local') + // TODO 性能问题? + const messages = messageStore.report[props.threadId] + arrayTemp.length = 0 + llmStreamCache.clear() + endFlag.value = false + endContent.value = '' + sources.value = [] // 遍历messages 用于渲染思维链 - messages.forEach(msg => { - // 单个chunk - // xchat组件的第一个chunk是 Waiting... 所以需要跳过 - if(msg.status === 'loading' && msg.message != 'Waiting...') { - isLoading = true - const node = JSON.parse(msg.message) - if(node.nodeName) { - processJsonNodeLogic(node) - }else{ - processLlmStreamNodeLogic(node) - } - } - // 完整的text, 历史记录的渲染 - // 当stream完成,xchat还会返回一次success,为避免思维链重复渲染,如果是loading状态,则不在重复增加节点 - if(msg.status === 'success' && !isLoading) { - isLoading = false - const jsonArray = parseJsonTextStrict(msg.message) - jsonArray.forEach(node => { - if(node.nodeName) { - processJsonNodeLogic(node) - }else{ - processLlmStreamNodeLogic(node) - } - }) - + messages.forEach(node => { + if(node.nodeName) { + processJsonNodeLogic(node) + }else{ + processLlmStreamNodeLogic(node) } }) @@ -159,7 +146,7 @@ const items = computed(() => { }) // 处理json节点 -const processJsonNodeLogic = (node: any) => { +const processJsonNodeLogic = (node: NormalNode) => { // 渲染普通节点 processJsonNode(node) // 普通节点处理完后,添加pending节点 @@ -183,7 +170,8 @@ const processLlmStreamNodeLogic = (node: any) => { for (const key of llmStreamKeys) { // 流式节点:移除pending节点,完成之前的流式节点 removeLastPendingNode() - item = processLlmStreamNode(node, key) + const k = node.graphId.thread_id + '-' + key + item = processLlmStreamNode(node, key, k) } if(item) { // 检查是否已经存在相同的item(针对llm_stream节点) @@ -197,7 +185,7 @@ const processLlmStreamNodeLogic = (node: any) => { // 移除所有pending状态的节点 const removeLastPendingNode = () => { for (let i = arrayTemp.length - 1; i >= 0; i--) { - if (arrayTemp[i].status === 'pending' && arrayTemp[i].title === '【处理中】正在请求后端内容') { + if (arrayTemp[i].status === 'pending' && arrayTemp[i].title === '思考中') { arrayTemp.splice(i, 1) } } @@ -208,18 +196,18 @@ const appendPendingNode = () => { // 如果不存在pending节点,创建一个新的 const pendingItem: ThoughtChainItem = { - title: '【处理中】正在请求后端内容', - description: '正在向后端发送请求并等待响应', + title: '思考中', + description: '正在等待思考结果', icon: h(LoadingOutlined), status: 'pending' } arrayTemp.push(pendingItem) } -const processLlmStreamNode = (node: any, key: string): ThoughtChainItem => { +const processLlmStreamNode = (node: any, key: string, cacheKey: string): ThoughtChainItem => { // 检查缓存中是否已存在该节点 - if (llmStreamCache.has(key)) { - const cached = llmStreamCache.get(key)! + if (llmStreamCache.has(cacheKey)) { + const cached = llmStreamCache.get(cacheKey)! // 累积新的内容 cached.content += node[key] // 更新MD组件的内容 @@ -245,7 +233,7 @@ const processLlmStreamNode = (node: any, key: string): ThoughtChainItem => { } // 缓存该节点 - llmStreamCache.set(key, { + llmStreamCache.set(cacheKey, { item, content: initialContent }) @@ -263,7 +251,7 @@ const processJsonNode = (node: any) => { switch(node.nodeName) { case '__START__': title = node.displayTitle - description = node.content + description = node.content.query break case 'coordinator': title = node.displayTitle @@ -337,15 +325,20 @@ const processJsonNode = (node: any) => { } arrayTemp.push(item) } - +onMounted(() => { + llmStreamCache.clear() + arrayTemp.length = 0 +}) // 监听convId变化,清理缓存 -watch(() => props.convId, () => { +watch(() => props.threadId, () => { llmStreamCache.clear() + arrayTemp.length = 0 }) // 组件卸载时清理缓存 onUnmounted(() => { llmStreamCache.clear() + arrayTemp.length = 0 }) const emit = defineEmits() @@ -367,7 +360,7 @@ const handleOnlineReport = async () => { return } - const xStreamBody = new XStreamBody('/api/reports/build-html?threadId=' + props.convId, { + const xStreamBody = new XStreamBody('/api/reports/build-html?threadId=' + props.threadId, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -406,7 +399,7 @@ const handleDownloadReport = () => { url: '/api/reports/export', method: 'POST', data: { - thread_id: props.convId, + thread_id: props.threadId, format: 'pdf' } }).then((response: any) => { diff --git a/spring-ai-alibaba-deepresearch/ui-vue3/src/router/defaultRoutes.ts b/spring-ai-alibaba-deepresearch/ui-vue3/src/router/defaultRoutes.ts index a7d9bb2164..2ca9f48e9d 100644 --- a/spring-ai-alibaba-deepresearch/ui-vue3/src/router/defaultRoutes.ts +++ b/spring-ai-alibaba-deepresearch/ui-vue3/src/router/defaultRoutes.ts @@ -48,9 +48,14 @@ export const routes: Readonly = [ { path: '/knowledge', name: 'knowledge', - component: () => import('../views/knowledge/index.vue'), + component: () => import('../views/knowledge/Management.vue'), + }, + { + path: '/knowledge/management', + name: 'knowledge-management', + component: () => import('../views/knowledge/Management.vue'), meta: { - icon: 'carbon:book', + icon: 'carbon:folder', fullscreen: true, }, }, diff --git a/spring-ai-alibaba-deepresearch/ui-vue3/src/store/ConfigStore.ts b/spring-ai-alibaba-deepresearch/ui-vue3/src/store/ConfigStore.ts index c0ff427dbd..4ee3f57326 100644 --- a/spring-ai-alibaba-deepresearch/ui-vue3/src/store/ConfigStore.ts +++ b/spring-ai-alibaba-deepresearch/ui-vue3/src/store/ConfigStore.ts @@ -21,13 +21,12 @@ import { reactive } from 'vue' type ConfigType = { form: { - enable_background_investigation: boolean auto_accepted_plan: boolean optimize_query_num: number max_plan_iterations: number max_step_num: number mcp_settings: any - search_engine: 'tavily' + search_engine: 'tavily', } } export const useConfigStore = () => @@ -35,7 +34,6 @@ export const useConfigStore = () => state(): ConfigType { return reactive({ form: { - enable_background_investigation: true, auto_accepted_plan: true, optimize_query_num: 3, max_plan_iterations: 1, diff --git a/spring-ai-alibaba-deepresearch/ui-vue3/src/store/KnowledgeStore.ts b/spring-ai-alibaba-deepresearch/ui-vue3/src/store/KnowledgeStore.ts new file mode 100644 index 0000000000..7cc738ac48 --- /dev/null +++ b/spring-ai-alibaba-deepresearch/ui-vue3/src/store/KnowledgeStore.ts @@ -0,0 +1,156 @@ +/* + * 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 + * + * http://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. + */ + +import { defineStore } from 'pinia' +import { reactive } from 'vue' +import { v4 as uuidv4 } from 'uuid' + +export interface KnowledgeFile { + id: string + name: string + size: number + uploadTime: string + status: 'uploading' | 'success' | 'error' +} + +export interface KnowledgeBase { + kb_id: string + kb_name: string + kb_description: string + category: string + files: KnowledgeFile[] + createTime: string + updateTime: string +} + +export const useKnowledgeStore = () => + defineStore('knowledgeStore', { + state() { + return reactive({ + knowledgeBases: [] as KnowledgeBase[], + }) + }, + getters: { + getKnowledgeBaseById: (state) => (id: string) => { + return state.knowledgeBases.find(kb => kb.kb_id === id) + }, + getKnowledgeBasesByCategory: (state) => (category: string) => { + return state.knowledgeBases.filter(kb => kb.category === category) + }, + }, + actions: { + addKnowledgeBase(kb: Omit) { + const newKb: KnowledgeBase = { + ...kb, + kb_id: uuidv4(), + files: [], + createTime: new Date().toISOString(), + updateTime: new Date().toISOString(), + } + this.knowledgeBases.push(newKb) + return newKb + }, + + updateKnowledgeBase(id: string, updates: Partial>) { + const index = this.knowledgeBases.findIndex(kb => kb.kb_id === id) + if (index !== -1) { + this.knowledgeBases[index] = { + ...this.knowledgeBases[index], + ...updates, + updateTime: new Date().toISOString(), + } + return this.knowledgeBases[index] + } + return null + }, + + deleteKnowledgeBase(id: string) { + const index = this.knowledgeBases.findIndex(kb => kb.kb_id === id) + if (index !== -1) { + this.knowledgeBases.splice(index, 1) + return true + } + return false + }, + + addFileToKnowledgeBase(kbId: string, file: Omit) { + const kb = this.getKnowledgeBaseById(kbId) + if (kb) { + const newFile: KnowledgeFile = { + ...file, + id: uuidv4(), + uploadTime: new Date().toISOString(), + status: 'uploading', + } + kb.files.push(newFile) + kb.updateTime = new Date().toISOString() + return newFile + } + return null + }, + + updateFileStatus(kbId: string, fileId: string, status: KnowledgeFile['status']) { + const kb = this.getKnowledgeBaseById(kbId) + if (kb) { + const file = kb.files.find(f => f.id === fileId) + if (file) { + file.status = status + kb.updateTime = new Date().toISOString() + return file + } + } + return null + }, + + removeFileFromKnowledgeBase(kbId: string, fileId: string) { + const kb = this.getKnowledgeBaseById(kbId) + if (kb) { + const index = kb.files.findIndex(f => f.id === fileId) + if (index !== -1) { + kb.files.splice(index, 1) + kb.updateTime = new Date().toISOString() + return true + } + } + return false + }, + + initSampleData() { + if (this.knowledgeBases.length === 0) { + // 添加一些示例数据 + this.addKnowledgeBase({ + kb_name: '产品文档库', + kb_description: '包含所有产品相关的文档和规格说明', + category: '产品管理', + }) + + this.addKnowledgeBase({ + kb_name: '技术文档库', + kb_description: '技术架构、API文档和开发指南', + category: '技术开发', + }) + + this.addKnowledgeBase({ + kb_name: '用户手册库', + kb_description: '用户操作指南和FAQ文档', + category: '用户支持', + }) + } + }, + }, + persist: true, + })() diff --git a/spring-ai-alibaba-deepresearch/ui-vue3/src/store/MessageStore.ts b/spring-ai-alibaba-deepresearch/ui-vue3/src/store/MessageStore.ts index b643a33d15..88eedfbc42 100644 --- a/spring-ai-alibaba-deepresearch/ui-vue3/src/store/MessageStore.ts +++ b/spring-ai-alibaba-deepresearch/ui-vue3/src/store/MessageStore.ts @@ -18,49 +18,51 @@ import { defineStore } from 'pinia' import { type MessageInfo, type SimpleType } from 'ant-design-x-vue' import { reactive } from 'vue' -import { initial } from 'lodash' -type MsgType = { - convId: string - currentState: { - [key: string]: { - // 会话 id - info: MessageInfo - // 是否候选, 是: 不显示在界面上 - candidate: boolean - // 是否勾选了 deepresearch - deepResearch: boolean - // 是否展示研究细节 - deepResearchDetail: boolean - // 记录ai内容的类型 - aiType: 'normal' | 'startDS' | 'onDS' | 'endDS' - } - } - // 记录历史 - history: { [key: string]: MessageInfo[] }, - htmlReport: { [key:string]: string[] }, - -} +import { type MessageState, type MsgType } from '@/types/message' +import { parseJsonTextStrict } from '@/utils/jsonParser' +import type { LlmStreamNode, NormalNode } from '@/types/node' export const useMessageStore = () => defineStore('messageStore', { state(): MsgType { return reactive({ convId: '', - currentState: {}, - history: {}, - htmlReport: {}, + currentState: {} as { [key: string]: MessageState }, + // { 会话id: [{ 线程id: 消息列表 }] } + history: {} as { [key: string]: MessageInfo[] }, + htmlReport: {} as { [key: string]: string[] }, + report: {} as { [key: string]: any[] }, }) }, getters: { - messages: (state): any => { + // 获取消息列表 + messages: (state): MessageInfo[] => { + const res: MessageInfo[] = [] if (state.convId) { - return state.history[state.convId] + const messages = state.history[state.convId] + const threadId = state.currentState[state.convId].threadId + if(!messages) { + return [] + } + for(const msg of messages) { + if(!msg.message) { + continue + } + const jsonArray = parseJsonTextStrict(msg.message) + jsonArray.forEach(item => { + if(item.graphId.thread_id === threadId) { + res.push(msg) + } + }) + } } - return null + return res }, - current: (state): any => { + // 获取当下消息状态 + current: (state): MessageState => { if (state.convId) { return state.currentState[state.convId] } + return {} as MessageState } }, actions: { @@ -74,8 +76,29 @@ export const useMessageStore = () => } else { this.current.aiType = 'normal' } - console.log('nextAIType', this.current.aiType) }, + addReport(report: any) { + if(!report) { + return + } + const node = JSON.parse(report) + if(!this.report[node.graphId.thread_id]) { + this.report[node.graphId.thread_id] = [] + } + this.report[node.graphId.thread_id].push(node) + }, + isEnd(threadId: string): boolean { + const report = this.report[threadId] + if(!report) { + return false + } + for(const item of report) { + if(item.nodeName === '__END__') { + return true + } + } + return false + } }, persist: true, })() diff --git a/spring-ai-alibaba-deepresearch/ui-vue3/src/types/message.ts b/spring-ai-alibaba-deepresearch/ui-vue3/src/types/message.ts new file mode 100644 index 0000000000..59c72eec00 --- /dev/null +++ b/spring-ai-alibaba-deepresearch/ui-vue3/src/types/message.ts @@ -0,0 +1,48 @@ +/* + * 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 + * + * http://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. + */ + +import { type MessageInfo } from 'ant-design-x-vue' + +/** + * 消息状态类型定义 + */ +export interface MessageState { + // 会话 id + info: MessageInfo + // 是否候选, 是: 不显示在界面上 + candidate: boolean + // 是否展示研究细节 + deepResearchDetail: boolean + // 记录ai内容的类型 + aiType: 'normal' | 'startDS' | 'onDS' | 'endDS' + // 极速模式 或者 深度模式 + deepResearch?: boolean + threadId: string +} + + +/** + * 消息存储类型定义 + */ +export interface MsgType { + convId: string + currentState: { [key: string]: MessageState } + // 记录历史 + history: { [key: string]: MessageInfo[] } + htmlReport: { [key: string]: string[] } + report: { [key: string]: any[] } +} diff --git a/spring-ai-alibaba-deepresearch/ui-vue3/src/types/node.ts b/spring-ai-alibaba-deepresearch/ui-vue3/src/types/node.ts index e0b2fa6214..bbf6d9922e 100644 --- a/spring-ai-alibaba-deepresearch/ui-vue3/src/types/node.ts +++ b/spring-ai-alibaba-deepresearch/ui-vue3/src/types/node.ts @@ -14,7 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +export interface GraphId { + session_id: string + thread_id: string +} /** * 普通类型节点定义 */ @@ -22,5 +25,23 @@ export interface NormalNode { /** 节点名称 */ nodeName: string /** 节点内容 */ - content: object + content: string | object , + graphId: GraphId + displayTitle: string, + siteInformation: SiteInformation[] +} + +export interface SiteInformation { + icon: string + weight: string + title: string + url: string + content: string +} + +export interface LlmStreamNode { + visible: boolean, + step_title: string, + finishReason: string + graphId: GraphId } diff --git a/spring-ai-alibaba-deepresearch/ui-vue3/src/utils/jsonParser.ts b/spring-ai-alibaba-deepresearch/ui-vue3/src/utils/jsonParser.ts index 7eb8744666..90f86887ff 100644 --- a/spring-ai-alibaba-deepresearch/ui-vue3/src/utils/jsonParser.ts +++ b/spring-ai-alibaba-deepresearch/ui-vue3/src/utils/jsonParser.ts @@ -4,46 +4,6 @@ * 并返回对应的JSON数组 */ -/** - * 解析类JSON文本为JSON数组 - * @param text 包含多个JSON对象的文本字符串 - * @returns 解析后的JSON对象数组 - */ -export function parseJsonText(text: string): any[] { - if (!text || typeof text !== 'string') { - return []; - } - - const result: any[] = []; - const trimmedText = text.trim(); - - if (!trimmedText) { - return result; - } - - // 使用正则表达式匹配所有的JSON对象 - // 匹配以 { 开始,以 } 结束的完整JSON对象 - const jsonRegex = /\{[^{}]*\}/g; - const matches = trimmedText.match(jsonRegex); - - if (!matches) { - return result; - } - - // 解析每个匹配到的JSON字符串 - for (const match of matches) { - try { - const jsonObj = JSON.parse(match.trim()); - result.push(jsonObj); - } catch (error) { - console.warn('解析JSON对象失败:', match, error); - // 继续处理其他JSON对象,不中断整个解析过程 - } - } - - return result; -} - /** * 解析类JSON文本为JSON数组(更严格的版本) * 支持嵌套的JSON对象和字符串内的转义字符 @@ -148,7 +108,9 @@ function findJsonObjectEnd(text: string, startIndex: number): number { return -1; // 没有找到完整的JSON对象 } + + /** * 默认导出解析函数 */ -export default parseJsonText; +export default parseJsonTextStrict; diff --git a/spring-ai-alibaba-deepresearch/ui-vue3/src/utils/request.ts b/spring-ai-alibaba-deepresearch/ui-vue3/src/utils/request.ts index b923d72dfa..381a164814 100644 --- a/spring-ai-alibaba-deepresearch/ui-vue3/src/utils/request.ts +++ b/spring-ai-alibaba-deepresearch/ui-vue3/src/utils/request.ts @@ -36,10 +36,17 @@ const request: AxiosInterceptorManager = service.int const response: AxiosInterceptorManager = service.interceptors.response request.use( config => { - config.data = JSON.stringify(config.data) - config.headers = { - 'Content-Type': 'application/json', + // 如果是FormData,不要进行JSON序列化和设置Content-Type + if (config.data instanceof FormData) { + // 删除默认的Content-Type,让浏览器自动设置multipart/form-data + delete config.headers['Content-Type'] + } else { + config.data = JSON.stringify(config.data) + config.headers = { + 'Content-Type': 'application/json', + } } + console.log('请求配置:', config) return config }, error => { @@ -49,6 +56,7 @@ request.use( response.use( response => { + console.log('响应数据:', response) if ( response.status === 200 && (response.data.code === 200 || response.data.status === 'success') diff --git a/spring-ai-alibaba-deepresearch/ui-vue3/src/views/chat/index.vue b/spring-ai-alibaba-deepresearch/ui-vue3/src/views/chat/index.vue index ab41e257aa..7733afdb81 100644 --- a/spring-ai-alibaba-deepresearch/ui-vue3/src/views/chat/index.vue +++ b/spring-ai-alibaba-deepresearch/ui-vue3/src/views/chat/index.vue @@ -60,7 +60,7 @@ > - - + --> - +
- - + @@ -149,13 +148,14 @@ import { useConversationStore } from '@/store/ConversationStore' import { useRoute, useRouter } from 'vue-router' import { useConfigStore } from '@/store/ConfigStore' import { parseJsonTextStrict } from '@/utils/jsonParser'; -import type { NormalNode } from '@/types/node'; +import type { NormalNode, SiteInformation } from '@/types/node'; import type { UploadFile } from '@/types/upload'; +import type { MessageState } from '@/types/message'; const router = useRouter() const route = useRoute() const conversationStore = useConversationStore() -// 会话ID +// TODO 是否有更好的方式,发送消息之后才启动一个新的会话 let convId = route.params.convId as string if (!convId) { const { key } = conversationStore.newOne() @@ -165,6 +165,8 @@ const uploadFileList = ref([]) const { useToken } = theme const { token } = useToken() const username = useAuthStore().token + +// 定义消息列表角色配置 const roles: BubbleListProps['roles'] = { ai: { placement: 'start', @@ -192,15 +194,18 @@ const roles: BubbleListProps['roles'] = { const messageStore = useMessageStore() const configStore = useConfigStore() + +// 设置当前会话信息 messageStore.convId = convId let current = messageStore.current if (!current) { - current = reactive({}) + current = reactive({} as MessageState) if (convId) { messageStore.currentState[convId] = current } } +// 处理人类反馈的请求 const sendResumeStream = async(message: string | undefined, onUpdate: (content: any) => void, onError: (error: any) => void): Promise => { const xStreamBody = new XStreamBody('/chat/resume', { method: 'POST', @@ -211,22 +216,25 @@ const sendResumeStream = async(message: string | undefined, onUpdate: (content body: { feed_back_content: message, feed_back: true, - thread_id: convId + session_id: convId, + thread_id: current.threadId, }, }) try { await xStreamBody.readStream((chunk: any) => { - onUpdate(chunk) + messageStore.addReport(chunk) + onUpdate(chunk) }) } catch (e: any) { - console.error(e.statusText) + console.error('sendResumeStreamError', e) onError(e.statusText) } return xStreamBody.content() } +// 处理发送消息的请求 const sendChatStream = async (message: string | undefined, onUpdate: (content: any) => void, onError: (error: any) => void): Promise => { const xStreamBody = new XStreamBody('/chat/stream', { method: 'POST', @@ -236,34 +244,31 @@ const sendChatStream = async (message: string | undefined, onUpdate: (content: a }, body: { ...configStore.chatConfig, + enable_deepresearch: current.deepResearch, query: message, - thread_id: convId + session_id: convId }, }) try { await xStreamBody.readStream((chunk: any) => { + messageStore.addReport(chunk) onUpdate(chunk) + }) } catch (e: any) { - console.error(e.statusText) + console.error('sendChatStream', e) onError(e.statusText) } return xStreamBody.content() } +// 定义 agent +const senderLoading = ref(false) const [agent] = useXAgent({ request: async ({ message }, { onSuccess, onUpdate, onError }) => { senderLoading.value = true - - if (!current.deepResearch) { - senderLoading.value = false - onSuccess(`暂未实现: ${message}`) - return - } - let content = '' - switch (current.aiType) { case 'normal': case 'startDS': { @@ -276,60 +281,6 @@ const [agent] = useXAgent({ break } - case 'onDS': { - const xStreamBody = new XStreamBody('/chat/resume', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'text/event-stream', - }, - body: { - ...configStore.chatConfig, - query: message, - feed_back: false, - }, - }) - - try { - await xStreamBody.readStream((chunk: any) => { - onUpdate(chunk) - }) - } catch (e: any) { - console.error(e.statusText) - onError(e.statusText) - } - - content = xStreamBody.content() - break - } - - case 'endDS': { - const xStreamBody = new XStreamBody('/chat/resume', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'text/event-stream', - }, - body: { - ...configStore.chatConfig, - query: message, - feed_back: true, - }, - }) - - try { - await xStreamBody.readStream((chunk: any) => { - onUpdate(chunk) - }) - } catch (e: any) { - console.error(e.statusText) - onError(e.statusText) - } - - content = xStreamBody.content() - break - } - default: { onError(new Error(`未知的 aiType: ${current.aiType}`)) senderLoading.value = false @@ -348,26 +299,13 @@ const { onRequest, messages } = useXChat({ requestPlaceholder: 'Waiting...', requestFallback: 'Failed return. Please try again later.', }) - -if (convId) { - const his_messages = messageStore.history[convId] - console.log('his_messages', his_messages) - if (his_messages) { - messages.value = [...his_messages] - } -} - +// 定义发送消息的内容 const content = ref('') -const senderLoading = ref(false) - const submitHandle = (nextContent: any) => { current.aiType = 'normal' - // 如果是深度研究,需要切换到下一个aiType - if (current.deepResearch) { - messageStore.nextAIType() - current.deepResearchDetail = true - } + messageStore.nextAIType() + // 自动接受,需要再转为下一个状态 if(configStore.chatConfig.auto_accepted_plan){ messageStore.nextAIType() @@ -383,11 +321,13 @@ function startDeepResearch() { onRequest('开始研究') } -function openDeepResearch() { +function openDeepResearch(threadId: string) { + current.threadId = threadId current.deepResearchDetail = !current.deepResearchDetail } -function buildPendingNodeThoughtChain() : any { +// 构建待处理节点的思考链 +function buildPendingNodeThoughtChain(jsonArray: any[]) : any { const items: ThoughtChainProps['items'] = [ { title: '请稍后...', @@ -404,14 +344,14 @@ function buildPendingNodeThoughtChain() : any { ) } +const collapsible = ref(['backgroundInvestigator']) +const onExpand = (keys: string[]) => { + collapsible.value = keys; +}; -let tempJsonArray: any[] = [] +// 构建开始研究的思考链 function buildStartDSThoughtChain(jsonArray: any[]) : any { - // 重置数组 - if(tempJsonArray.length > 0) { - tempJsonArray = [] - } - const { Paragraph, Text } = Typography + const { Paragraph } = Typography // 获取背景调查节点 const backgroundInvestigatorNode = jsonArray.filter((item) => item.nodeName === 'background_investigator')[0] const results = backgroundInvestigatorNode.siteInformation @@ -447,34 +387,30 @@ function buildStartDSThoughtChain(jsonArray: any[]) : any { description: 只需要几分钟就可以准备好, footer: ( - {/* */} - + { messageStore.isEnd(backgroundInvestigatorNode.graphId.thread_id) ? + : } ), extra: '', }, ] - tempJsonArray = jsonArray return ( <> 这是该主题的研究方案。如果你需要进行更新,请告诉我。 +

- {/*

{{ msg }}

*/} - + ) } - -function buildOnDSThoughtChain() : any { - if(tempJsonArray.length === 0){ - return - } - const { Paragraph, Text } = Typography +// 构建正在分析结果的思考链 +function buildOnDSThoughtChain(jsonArray: any[]) : any { + const { Paragraph } = Typography // 获取背景调查节点 - const backgroundInvestigatorNode = tempJsonArray.filter((item) => item.nodeName === 'background_investigator')[0] - const results = backgroundInvestigatorNode.siteInformation - const markdownContent = results.map((result: any, index: number) => { + const backgroundInvestigatorNode = jsonArray.filter((item) => item.nodeName === 'background_investigator')[0] + const results: SiteInformation[] = backgroundInvestigatorNode.siteInformation + const markdownContent = results.map((result: SiteInformation, index: number) => { return `${index + 1}. [${result.title}](${result.url})\n\n` }).join('\n') const items: ThoughtChainProps['items'] = [ @@ -503,23 +439,20 @@ function buildOnDSThoughtChain() : any { return ( <> 这是该主题的研究方案。正在分析结果中... +

{/*

{{ msg }}

*/} - + ) } -const collapsible = ref({}) -function buildEndDSThoughtChain(jsonArray: any[]): JSX.Element | undefined { - if(tempJsonArray.length === 0 && jsonArray.length === 0){ - return undefined - } - const curJsonArray = tempJsonArray.length > 0 ? tempJsonArray : jsonArray - const { Paragraph, Text } = Typography +// 构建分析完成的思考链 +function buildEndDSThoughtChain(jsonArray: NormalNode[]): JSX.Element | undefined { + const { Paragraph } = Typography const items: ThoughtChainProps['items'] = [] // 获取背景调查节点 - const backgroundInvestigatorNode = curJsonArray.filter((item) => item.nodeName === 'background_investigator')[0] + const backgroundInvestigatorNode = jsonArray.filter((item) => item.nodeName === 'background_investigator')[0] if(backgroundInvestigatorNode && backgroundInvestigatorNode.siteInformation){ const results = backgroundInvestigatorNode.siteInformation const markdownContent = results.map((result: any, index: number) => { @@ -540,19 +473,27 @@ function buildEndDSThoughtChain(jsonArray: any[]): JSX.Element | undefined { ), } items.push(item) - collapsible.value = { expandedKeys: ['backgroundInvestigator'] } } - const completeItem: ThoughtChainItem = { - status: 'success', - title: '分析结果', - icon: , - footer: ( - - - - ), + // 分析结果节点 + const startNode = jsonArray.filter((item) => item.nodeName === '__START__')[0] + const humanFeedbackNode = jsonArray.filter((item) => item.nodeName === 'human_feedback')[0] + if(startNode || humanFeedbackNode) { + const threadId = startNode ? startNode.graphId.thread_id : humanFeedbackNode.graphId.thread_id + const completeItem: ThoughtChainItem = { + status: 'success', + title: '分析结果', + icon: , + footer: ( + + + + ), + } + items.push(completeItem) } - items.push(completeItem) + // 完成节点 const endItem: ThoughtChainItem = { title: '完成', icon: h(CheckCircleOutlined), @@ -563,116 +504,118 @@ function buildEndDSThoughtChain(jsonArray: any[]): JSX.Element | undefined { return ( <> 这是该主题的研究方案已完成,可以点击下载报告 +

- {/*

{{ msg }}

*/} - + ) } -function parseLoadingMessage(): any{ - if(current.deepResearch){ - // 准备开始研究 - if(current.aiType === 'startDS') { - return buildPendingNodeThoughtChain() - } - if(current.aiType === 'onDS' && configStore.chatConfig.auto_accepted_plan) { - return buildPendingNodeThoughtChain() - } - // 正在研究中 - if(current.aiType === 'onDS') { - return buildOnDSThoughtChain() - } +// 解析 status = loading 的消息 +function parseLoadingMessage(msg: string): any{ + // 准备开始研究 + const jsonArray = parseJsonTextStrict(msg) + if(!jsonArray || jsonArray.length === 0){ + return buildPendingNodeThoughtChain(jsonArray) + } + // 深度研究模式, 需要设置 threadId 并且 打开 report 面板 + const coordinatorNode = jsonArray.filter((item) => item.nodeName === 'coordinator')[0]; + if(coordinatorNode && coordinatorNode.content) { + current.threadId = coordinatorNode.graphId.thread_id + current.deepResearchDetail = true + } + const report = messageStore.report[jsonArray[0].graphId.thread_id] + if(!report) { + return buildPendingNodeThoughtChain(jsonArray) + } + // 如果已经有数,则渲染思维链 + const backgroundInvestigatorNode = report.filter((item) => item.nodeName === 'background_investigator')[0] + if(backgroundInvestigatorNode) { + return buildOnDSThoughtChain(report) } - return buildPendingNodeThoughtChain() + + return buildPendingNodeThoughtChain(jsonArray) } -function parseSuccessMessage(msg: any, isCurrent: boolean) { +// 解析 status = success 的消息 +function parseSuccessMessage(msg: string) { // 解析完整数据 const jsonArray: NormalNode[] = parseJsonTextStrict(msg) - // 历史数据渲染 - if (current.deepResearch && !isCurrent) { - // 研究网站、分析结果、生成报告 - return - } - - if (current.deepResearch && isCurrent) { - // 不启用研究模式,闲聊模式 - if(jsonArray.filter((item) => item.nodeName === 'coordinator').length > 0) { - const coordinatorNode = jsonArray.filter((item) => item.nodeName === 'coordinator')[0]; - if(!coordinatorNode.content) { - return (jsonArray.filter((item) => item.nodeName === '__END__')[0].content as any).output - } - } - if (current.aiType === 'startDS') { - // 如果不包含背景调查,则提示用户重新输入 - if(jsonArray.filter((item) => item.nodeName === 'background_investigator').length === 0) { - return - } - return buildStartDSThoughtChain(jsonArray) - } - // 研究完成,TODO 这里应该流为endDS状态 - if (current.aiType === 'onDS') { - return buildEndDSThoughtChain(jsonArray) + // 闲聊模式 + if(jsonArray.filter((item) => item.nodeName === 'coordinator').length > 0) { + const coordinatorNode = jsonArray.filter((item) => item.nodeName === 'coordinator')[0]; + if(coordinatorNode && !coordinatorNode.content) { + return (jsonArray.filter((item) => item.nodeName === '__END__')[0].content as any).output } } + // 人类中断模式 + if(jsonArray.filter((item) => item.nodeName === '__END__').length === 0) { + return buildStartDSThoughtChain(jsonArray) + } + // 人类恢复模式 或者 直接 end 模式 + if(jsonArray.filter((item) => item.nodeName === 'human_feedback').length > 0 || jsonArray.filter((item) => item.nodeName === '__END__').length) { + return buildEndDSThoughtChain(jsonArray) + } } -function getTargetNode(jsonArray: NormalNode[]): NormalNode | undefined { - // TODO: 实现获取目标节点的逻辑 - return undefined -} // 解析消息记录 // status === local 表示人类 loading表示stream流正在返回 success表示steram完成返回 // msg 当status === loading的时候,返回stream流的chunk 当status === success的时候,返回所有chunk的拼接字符串 -// isCurrent true表示当前消息是最新的,false表示历史消息 -function parseMessage(status: MessageStatus, msg: any, isCurrent: boolean): any { +function parseMessage(status: MessageStatus, msg: string): any { switch (status) { // 人类信息 case 'local': return msg case 'loading': - return parseLoadingMessage() + return parseLoadingMessage(msg) case 'success': - return parseSuccessMessage(msg, isCurrent) + return parseSuccessMessage(msg) case 'error': return msg default: return '' } } - -function parseFooter(status: MessageStatus, isCurrent: boolean): any { +// TODO 分享、拷贝、更多操作 +function parseFooter(status: MessageStatus): any { switch (status) { case 'success': - // return ( - // - // ) return '' default: return '' } } - +// 初始化消息 +if (convId) { + const his_messages = messageStore.history[convId] + if (his_messages) { + messages.value = his_messages + } +} +// 消息列表 const bubbleList = computed(() => { - const len = messages.value.length + let isError = false + for(const item of messages.value) { + if(item.status === 'error') { + isError = true + } + } + // 避免异常,导致整个消息列表被覆盖 + if(isError) { + return [] + } + messageStore.history[convId] = messages.value - // 当状态是loading的时候,是每个chunk,然后succes,把之前所有的chunk 全部返回 - const list = messages.value.map(({ id, message, status }, idx) => ({ - key: id, - role: status === 'local' ? 'local' : 'ai', - content: parseMessage(status, message, idx === len - 1), - footer: parseFooter(status, idx === len - 1), - })) - return list; + return messages.value.map(({id, status, message}, idx) => { + return { + key: idx, + role: status === 'local' ? 'local' : 'ai', + content: parseMessage(status, message), + footer: parseFooter(status), + } + }) }) const scrollContainer = ref(null) @@ -771,9 +714,6 @@ watch( .tag-deep-research { cursor: pointer; - &checked { - } - &unchecked { background: #fff; } diff --git a/spring-ai-alibaba-deepresearch/ui-vue3/src/views/config/index.vue b/spring-ai-alibaba-deepresearch/ui-vue3/src/views/config/index.vue index 5c39e3aad6..c6b857cabc 100644 --- a/spring-ai-alibaba-deepresearch/ui-vue3/src/views/config/index.vue +++ b/spring-ai-alibaba-deepresearch/ui-vue3/src/views/config/index.vue @@ -27,21 +27,10 @@ 一般配置 - - - - - @@ -68,7 +57,6 @@ + + diff --git a/spring-ai-alibaba-deepresearch/ui-vue3/src/views/knowledge/index.vue b/spring-ai-alibaba-deepresearch/ui-vue3/src/views/knowledge/index.vue index 1c9d6b6839..79b9a45e9a 100644 --- a/spring-ai-alibaba-deepresearch/ui-vue3/src/views/knowledge/index.vue +++ b/spring-ai-alibaba-deepresearch/ui-vue3/src/views/knowledge/index.vue @@ -27,7 +27,7 @@

管理您的知识库文档,组织和分类文档内容

- 进入管理 + 进入管理
@@ -35,7 +35,7 @@

上传新的文档到知识库,支持多种文件格式

- 上传文档 + 上传文档
@@ -43,7 +43,7 @@

在知识库中搜索相关信息和文档内容

- 开始搜索 + 开始搜索
@@ -78,12 +78,17 @@