Commit 50b5aeb4428643f543573083ccfe7a8cf7942dfa

Authored by YunaiV
1 parent 369ca68a

feat:【ai 大模型】支持 Chat Role 的 mcp 配置

yudao-module-ai/pom.xml
@@ -197,6 +197,7 @@ @@ -197,6 +197,7 @@
197 <groupId>org.springframework.ai</groupId> 197 <groupId>org.springframework.ai</groupId>
198 <artifactId>spring-ai-starter-mcp-client-webflux</artifactId> 198 <artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
199 <version>${spring-ai.version}</version> 199 <version>${spring-ai.version}</version>
  200 + <optional>true</optional>
200 </dependency> 201 </dependency>
201 202
202 <!-- TinyFlow:AI 工作流 --> 203 <!-- TinyFlow:AI 工作流 -->
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java
@@ -52,6 +52,9 @@ public class AiChatRoleRespVO implements VO { @@ -52,6 +52,9 @@ public class AiChatRoleRespVO implements VO {
52 @Schema(description = "引用的工具编号列表", example = "1,2,3") 52 @Schema(description = "引用的工具编号列表", example = "1,2,3")
53 private List<Long> toolIds; 53 private List<Long> toolIds;
54 54
  55 + @Schema(description = "引用的 MCP Client 名字列表", example = "filesystem")
  56 + private List<String> mcpClientNames;
  57 +
55 @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") 58 @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
56 private Boolean publicStatus; 59 private Boolean publicStatus;
57 60
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java
@@ -37,4 +37,7 @@ public class AiChatRoleSaveMyReqVO { @@ -37,4 +37,7 @@ public class AiChatRoleSaveMyReqVO {
37 @Schema(description = "引用的工具编号列表", example = "1,2,3") 37 @Schema(description = "引用的工具编号列表", example = "1,2,3")
38 private List<Long> toolIds; 38 private List<Long> toolIds;
39 39
  40 + @Schema(description = "引用的 MCP Client 名字列表", example = "filesystem")
  41 + private List<String> mcpClientNames;
  42 +
40 } 43 }
41 \ No newline at end of file 44 \ No newline at end of file
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java
@@ -50,6 +50,9 @@ public class AiChatRoleSaveReqVO { @@ -50,6 +50,9 @@ public class AiChatRoleSaveReqVO {
50 @Schema(description = "引用的工具编号列表", example = "1,2,3") 50 @Schema(description = "引用的工具编号列表", example = "1,2,3")
51 private List<Long> toolIds; 51 private List<Long> toolIds;
52 52
  53 + @Schema(description = "引用的 MCP Client 名字列表", example = "filesystem")
  54 + private List<String> mcpClientNames;
  55 +
53 @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") 56 @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
54 @NotNull(message = "是否公开不能为空") 57 @NotNull(message = "是否公开不能为空")
55 private Boolean publicStatus; 58 private Boolean publicStatus;
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java
@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.model; @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.model;
3 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 3 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; 4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
5 import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; 5 import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
  6 +import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler;
6 import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; 7 import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
7 import com.baomidou.mybatisplus.annotation.KeySequence; 8 import com.baomidou.mybatisplus.annotation.KeySequence;
8 import com.baomidou.mybatisplus.annotation.TableField; 9 import com.baomidou.mybatisplus.annotation.TableField;
@@ -80,6 +81,13 @@ public class AiChatRoleDO extends BaseDO { @@ -80,6 +81,13 @@ public class AiChatRoleDO extends BaseDO {
80 */ 81 */
81 @TableField(typeHandler = LongListTypeHandler.class) 82 @TableField(typeHandler = LongListTypeHandler.class)
82 private List<Long> toolIds; 83 private List<Long> toolIds;
  84 + /**
  85 + * 引用的 MCP Client 名字列表
  86 + *
  87 + * 关联 spring.ai.mcp.client 下的名字
  88 + */
  89 + @TableField(typeHandler = StringListTypeHandler.class)
  90 + private List<String> mcpClientNames;
83 91
84 /** 92 /**
85 * 是否公开 93 * 是否公开
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java
@@ -36,6 +36,7 @@ import cn.iocoder.yudao.module.ai.service.model.AiToolService; @@ -36,6 +36,7 @@ import cn.iocoder.yudao.module.ai.service.model.AiToolService;
36 import cn.iocoder.yudao.module.ai.util.AiUtils; 36 import cn.iocoder.yudao.module.ai.util.AiUtils;
37 import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils; 37 import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils;
38 import com.google.common.collect.Maps; 38 import com.google.common.collect.Maps;
  39 +import io.modelcontextprotocol.client.McpSyncClient;
39 import jakarta.annotation.Resource; 40 import jakarta.annotation.Resource;
40 import lombok.extern.slf4j.Slf4j; 41 import lombok.extern.slf4j.Slf4j;
41 import org.springframework.ai.chat.messages.Message; 42 import org.springframework.ai.chat.messages.Message;
@@ -47,6 +48,10 @@ import org.springframework.ai.chat.model.ChatResponse; @@ -47,6 +48,10 @@ import org.springframework.ai.chat.model.ChatResponse;
47 import org.springframework.ai.chat.model.StreamingChatModel; 48 import org.springframework.ai.chat.model.StreamingChatModel;
48 import org.springframework.ai.chat.prompt.ChatOptions; 49 import org.springframework.ai.chat.prompt.ChatOptions;
49 import org.springframework.ai.chat.prompt.Prompt; 50 import org.springframework.ai.chat.prompt.Prompt;
  51 +import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
  52 +import org.springframework.ai.mcp.client.autoconfigure.properties.McpClientCommonProperties;
  53 +import org.springframework.ai.tool.ToolCallback;
  54 +import org.springframework.ai.tool.resolution.ToolCallbackResolver;
50 import org.springframework.beans.factory.annotation.Autowired; 55 import org.springframework.beans.factory.annotation.Autowired;
51 import org.springframework.stereotype.Service; 56 import org.springframework.stereotype.Service;
52 import org.springframework.transaction.annotation.Transactional; 57 import org.springframework.transaction.annotation.Transactional;
@@ -119,6 +124,17 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { @@ -119,6 +124,17 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
119 @Autowired(required = false) // 由于 yudao.ai.web-search.enable 配置项,可以关闭 AiWebSearchClient 的功能,所以这里只能不强制注入 124 @Autowired(required = false) // 由于 yudao.ai.web-search.enable 配置项,可以关闭 AiWebSearchClient 的功能,所以这里只能不强制注入
120 private AiWebSearchClient webSearchClient; 125 private AiWebSearchClient webSearchClient;
121 126
  127 + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
  128 + @Autowired(required = false) // 由于 yudao.ai.mcp.client.enable 配置项,可以关闭 McpSyncClient 的功能,所以这里只能不强制注入
  129 + private List<McpSyncClient> mcpClients;
  130 +
  131 + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
  132 + @Autowired(required = false) // 由于 yudao.ai.mcp.client.enable 配置项,可以关闭 McpSyncClient 的功能,所以这里只能不强制注入
  133 + private McpClientCommonProperties mcpClientCommonProperties;
  134 +
  135 + @Resource
  136 + private ToolCallbackResolver toolCallbackResolver;
  137 +
122 @Transactional(rollbackFor = Exception.class) 138 @Transactional(rollbackFor = Exception.class)
123 public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) { 139 public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) {
124 // 1.1 校验对话存在 140 // 1.1 校验对话存在
@@ -334,23 +350,54 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { @@ -334,23 +350,54 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
334 } 350 }
335 351
336 // 2.1 查询 tool 工具 352 // 2.1 查询 tool 工具
337 - Set<String> toolNames = null;  
338 - Map<String,Object> toolContext = Map.of();  
339 - if (conversation.getRoleId() != null) {  
340 - AiChatRoleDO chatRole = chatRoleService.getChatRole(conversation.getRoleId());  
341 - if (chatRole != null && CollUtil.isNotEmpty(chatRole.getToolIds())) {  
342 - toolNames = convertSet(toolService.getToolList(chatRole.getToolIds()), AiToolDO::getName);  
343 - toolContext = AiUtils.buildCommonToolContext();  
344 - }  
345 - } 353 + List<ToolCallback> toolCallbacks = getToolCallbackListByRoleId(conversation.getRoleId());
  354 + Map<String,Object> toolContext = CollUtil.isNotEmpty(toolCallbacks) ? AiUtils.buildCommonToolContext()
  355 + : Map.of();
346 // 2.2 构建 ChatOptions 对象 356 // 2.2 构建 ChatOptions 对象
347 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); 357 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform());
348 ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(), 358 ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(),
349 conversation.getTemperature(), conversation.getMaxTokens(), 359 conversation.getTemperature(), conversation.getMaxTokens(),
350 - toolNames, toolContext); 360 + toolCallbacks, toolContext);
351 return new Prompt(chatMessages, chatOptions); 361 return new Prompt(chatMessages, chatOptions);
352 } 362 }
353 363
  364 + private List<ToolCallback> getToolCallbackListByRoleId(Long roleId) {
  365 + if (roleId == null) {
  366 + return null;
  367 + }
  368 + AiChatRoleDO chatRole = chatRoleService.getChatRole(roleId);
  369 + if (chatRole == null) {
  370 + return null;
  371 + }
  372 + List<ToolCallback> toolCallbacks = new ArrayList<>();
  373 + // 1. 通过 toolIds
  374 + if (CollUtil.isNotEmpty(chatRole.getToolIds())) {
  375 + Set<String> toolNames = convertSet(toolService.getToolList(chatRole.getToolIds()), AiToolDO::getName);
  376 + toolNames.forEach(toolName -> {
  377 + ToolCallback toolCallback = toolCallbackResolver.resolve(toolName);
  378 + if (toolCallback != null) {
  379 + toolCallbacks.add(toolCallback);
  380 + }
  381 + });
  382 + }
  383 + // 2. 通过 mcpClients
  384 + if (CollUtil.isNotEmpty(mcpClients) && CollUtil.isNotEmpty(chatRole.getMcpClientNames())) {
  385 + chatRole.getMcpClientNames().forEach(mcpClientName -> {
  386 + // 2.1 标准化名字,参考 McpClientAutoConfiguration 的 connectedClientName 方法
  387 + String finalMcpClientName = mcpClientCommonProperties.getName() + " - " + mcpClientName;
  388 + // 2.2 匹配对应的 McpSyncClient
  389 + mcpClients.forEach(mcpClient -> {
  390 + if (ObjUtil.notEqual(mcpClient.getClientInfo().name(), finalMcpClientName)) {
  391 + return;
  392 + }
  393 + ToolCallback[] mcpToolCallBacks = new SyncMcpToolCallbackProvider(mcpClient).getToolCallbacks();
  394 + CollUtil.addAll(toolCallbacks, mcpToolCallBacks);
  395 + });
  396 + });
  397 + }
  398 + return toolCallbacks;
  399 + }
  400 +
354 /** 401 /**
355 * 从历史消息中,获得倒序的 n 组消息作为消息上下文 402 * 从历史消息中,获得倒序的 n 组消息作为消息上下文
356 * <p> 403 * <p>
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java
@@ -18,12 +18,10 @@ import org.springframework.ai.deepseek.DeepSeekChatOptions; @@ -18,12 +18,10 @@ import org.springframework.ai.deepseek.DeepSeekChatOptions;
18 import org.springframework.ai.minimax.MiniMaxChatOptions; 18 import org.springframework.ai.minimax.MiniMaxChatOptions;
19 import org.springframework.ai.ollama.api.OllamaOptions; 19 import org.springframework.ai.ollama.api.OllamaOptions;
20 import org.springframework.ai.openai.OpenAiChatOptions; 20 import org.springframework.ai.openai.OpenAiChatOptions;
  21 +import org.springframework.ai.tool.ToolCallback;
21 import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; 22 import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;
22 23
23 -import java.util.Collections;  
24 -import java.util.HashMap;  
25 -import java.util.Map;  
26 -import java.util.Set; 24 +import java.util.*;
27 25
28 /** 26 /**
29 * Spring AI 工具类 27 * Spring AI 工具类
@@ -40,15 +38,15 @@ public class AiUtils { @@ -40,15 +38,15 @@ public class AiUtils {
40 } 38 }
41 39
42 public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens, 40 public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens,
43 - Set<String> toolNames, Map<String, Object> toolContext) {  
44 - toolNames = ObjUtil.defaultIfNull(toolNames, Collections.emptySet()); 41 + List<ToolCallback> toolCallbacks, Map<String, Object> toolContext) {
  42 + toolCallbacks = ObjUtil.defaultIfNull(toolCallbacks, Collections.emptyList());
45 toolContext = ObjUtil.defaultIfNull(toolContext, Collections.emptyMap()); 43 toolContext = ObjUtil.defaultIfNull(toolContext, Collections.emptyMap());
46 // noinspection EnhancedSwitchMigration 44 // noinspection EnhancedSwitchMigration
47 switch (platform) { 45 switch (platform) {
48 case TONG_YI: 46 case TONG_YI:
49 return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens) 47 return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens)
50 .withEnableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置 48 .withEnableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置
51 - .withToolNames(toolNames).withToolContext(toolContext).build(); 49 + .withToolCallbacks(toolCallbacks).withToolContext(toolContext).build();
52 case YI_YAN: 50 case YI_YAN:
53 return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); 51 return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
54 case DEEP_SEEK: 52 case DEEP_SEEK:
@@ -57,30 +55,30 @@ public class AiUtils { @@ -57,30 +55,30 @@ public class AiUtils {
57 case SILICON_FLOW: // 复用 DeepSeek 客户端 55 case SILICON_FLOW: // 复用 DeepSeek 客户端
58 case XING_HUO: // 复用 DeepSeek 客户端 56 case XING_HUO: // 复用 DeepSeek 客户端
59 return DeepSeekChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) 57 return DeepSeekChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
60 - .toolNames(toolNames).toolContext(toolContext).build(); 58 + .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
61 case ZHI_PU: 59 case ZHI_PU:
62 return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) 60 return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
63 - .toolNames(toolNames).toolContext(toolContext).build(); 61 + .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
64 case MINI_MAX: 62 case MINI_MAX:
65 return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) 63 return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
66 - .toolNames(toolNames).toolContext(toolContext).build(); 64 + .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
67 case MOONSHOT: 65 case MOONSHOT:
68 return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) 66 return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
69 - .toolNames(toolNames).toolContext(toolContext).build(); 67 + .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
70 case OPENAI: 68 case OPENAI:
71 case GEMINI: // 复用 OpenAI 客户端 69 case GEMINI: // 复用 OpenAI 客户端
72 case BAI_CHUAN: // 复用 OpenAI 客户端 70 case BAI_CHUAN: // 复用 OpenAI 客户端
73 return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) 71 return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
74 - .toolNames(toolNames).toolContext(toolContext).build(); 72 + .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
75 case AZURE_OPENAI: 73 case AZURE_OPENAI:
76 return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens) 74 return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens)
77 - .toolNames(toolNames).toolContext(toolContext).build(); 75 + .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
78 case ANTHROPIC: 76 case ANTHROPIC:
79 return AnthropicChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) 77 return AnthropicChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
80 - .toolNames(toolNames).toolContext(toolContext).build(); 78 + .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
81 case OLLAMA: 79 case OLLAMA:
82 return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens) 80 return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens)
83 - .toolNames(toolNames).toolContext(toolContext).build(); 81 + .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
84 default: 82 default:
85 throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); 83 throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
86 } 84 }
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/mcp/DouBaoMcpTests.java
@@ -36,44 +36,47 @@ public class DouBaoMcpTests { @@ -36,44 +36,47 @@ public class DouBaoMcpTests {
36 36
37 @Test 37 @Test
38 public void testMcpGetUserInfo() { 38 public void testMcpGetUserInfo() {
39 -  
40 // 打印结果 39 // 打印结果
41 System.out.println(chatClient.prompt() 40 System.out.println(chatClient.prompt()
42 .user("目前有哪些工具可以使用") 41 .user("目前有哪些工具可以使用")
43 .call() 42 .call()
44 .content()); 43 .content());
45 System.out.println("===================================="); 44 System.out.println("====================================");
  45 +
46 // 打印结果 46 // 打印结果
47 System.out.println(chatClient.prompt() 47 System.out.println(chatClient.prompt()
48 .user("小新的年龄是多少") 48 .user("小新的年龄是多少")
49 .call() 49 .call()
50 .content()); 50 .content());
51 System.out.println("===================================="); 51 System.out.println("====================================");
  52 +
52 // 打印结果 53 // 打印结果
53 System.out.println(chatClient.prompt() 54 System.out.println(chatClient.prompt()
54 .user("获取小新的基本信息") 55 .user("获取小新的基本信息")
55 .call() 56 .call()
56 .content()); 57 .content());
57 System.out.println("===================================="); 58 System.out.println("====================================");
  59 +
58 // 打印结果 60 // 打印结果
59 System.out.println(chatClient.prompt() 61 System.out.println(chatClient.prompt()
60 .user("小新是什么职业的") 62 .user("小新是什么职业的")
61 .call() 63 .call()
62 .content()); 64 .content());
63 System.out.println("===================================="); 65 System.out.println("====================================");
  66 +
64 // 打印结果 67 // 打印结果
65 System.out.println(chatClient.prompt() 68 System.out.println(chatClient.prompt()
66 .user("小新的教育背景") 69 .user("小新的教育背景")
67 .call() 70 .call()
68 .content()); 71 .content());
69 System.out.println("===================================="); 72 System.out.println("====================================");
  73 +
70 // 打印结果 74 // 打印结果
71 System.out.println(chatClient.prompt() 75 System.out.println(chatClient.prompt()
72 .user("小新的兴趣爱好是什么") 76 .user("小新的兴趣爱好是什么")
73 .call() 77 .call()
74 .content()); 78 .content());
75 System.out.println("===================================="); 79 System.out.println("====================================");
76 -  
77 } 80 }
78 81
79 82