Commit 50b5aeb4428643f543573083ccfe7a8cf7942dfa

Authored by YunaiV
1 parent 369ca68a

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

yudao-module-ai/pom.xml
... ... @@ -197,6 +197,7 @@
197 197 <groupId>org.springframework.ai</groupId>
198 198 <artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
199 199 <version>${spring-ai.version}</version>
  200 + <optional>true</optional>
200 201 </dependency>
201 202  
202 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 52 @Schema(description = "引用的工具编号列表", example = "1,2,3")
53 53 private List<Long> toolIds;
54 54  
  55 + @Schema(description = "引用的 MCP Client 名字列表", example = "filesystem")
  56 + private List<String> mcpClientNames;
  57 +
55 58 @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
56 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 37 @Schema(description = "引用的工具编号列表", example = "1,2,3")
38 38 private List<Long> toolIds;
39 39  
  40 + @Schema(description = "引用的 MCP Client 名字列表", example = "filesystem")
  41 + private List<String> mcpClientNames;
  42 +
40 43 }
41 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 50 @Schema(description = "引用的工具编号列表", example = "1,2,3")
51 51 private List<Long> toolIds;
52 52  
  53 + @Schema(description = "引用的 MCP Client 名字列表", example = "filesystem")
  54 + private List<String> mcpClientNames;
  55 +
53 56 @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
54 57 @NotNull(message = "是否公开不能为空")
55 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 3 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
5 5 import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
  6 +import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler;
6 7 import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
7 8 import com.baomidou.mybatisplus.annotation.KeySequence;
8 9 import com.baomidou.mybatisplus.annotation.TableField;
... ... @@ -80,6 +81,13 @@ public class AiChatRoleDO extends BaseDO {
80 81 */
81 82 @TableField(typeHandler = LongListTypeHandler.class)
82 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 36 import cn.iocoder.yudao.module.ai.util.AiUtils;
37 37 import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils;
38 38 import com.google.common.collect.Maps;
  39 +import io.modelcontextprotocol.client.McpSyncClient;
39 40 import jakarta.annotation.Resource;
40 41 import lombok.extern.slf4j.Slf4j;
41 42 import org.springframework.ai.chat.messages.Message;
... ... @@ -47,6 +48,10 @@ import org.springframework.ai.chat.model.ChatResponse;
47 48 import org.springframework.ai.chat.model.StreamingChatModel;
48 49 import org.springframework.ai.chat.prompt.ChatOptions;
49 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 55 import org.springframework.beans.factory.annotation.Autowired;
51 56 import org.springframework.stereotype.Service;
52 57 import org.springframework.transaction.annotation.Transactional;
... ... @@ -119,6 +124,17 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
119 124 @Autowired(required = false) // 由于 yudao.ai.web-search.enable 配置项,可以关闭 AiWebSearchClient 的功能,所以这里只能不强制注入
120 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 138 @Transactional(rollbackFor = Exception.class)
123 139 public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) {
124 140 // 1.1 校验对话存在
... ... @@ -334,23 +350,54 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
334 350 }
335 351  
336 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 356 // 2.2 构建 ChatOptions 对象
347 357 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform());
348 358 ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(),
349 359 conversation.getTemperature(), conversation.getMaxTokens(),
350   - toolNames, toolContext);
  360 + toolCallbacks, toolContext);
351 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 402 * 从历史消息中,获得倒序的 n 组消息作为消息上下文
356 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 18 import org.springframework.ai.minimax.MiniMaxChatOptions;
19 19 import org.springframework.ai.ollama.api.OllamaOptions;
20 20 import org.springframework.ai.openai.OpenAiChatOptions;
  21 +import org.springframework.ai.tool.ToolCallback;
21 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 27 * Spring AI 工具类
... ... @@ -40,15 +38,15 @@ public class AiUtils {
40 38 }
41 39  
42 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 43 toolContext = ObjUtil.defaultIfNull(toolContext, Collections.emptyMap());
46 44 // noinspection EnhancedSwitchMigration
47 45 switch (platform) {
48 46 case TONG_YI:
49 47 return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens)
50 48 .withEnableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置
51   - .withToolNames(toolNames).withToolContext(toolContext).build();
  49 + .withToolCallbacks(toolCallbacks).withToolContext(toolContext).build();
52 50 case YI_YAN:
53 51 return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
54 52 case DEEP_SEEK:
... ... @@ -57,30 +55,30 @@ public class AiUtils {
57 55 case SILICON_FLOW: // 复用 DeepSeek 客户端
58 56 case XING_HUO: // 复用 DeepSeek 客户端
59 57 return DeepSeekChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
60   - .toolNames(toolNames).toolContext(toolContext).build();
  58 + .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
61 59 case ZHI_PU:
62 60 return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
63   - .toolNames(toolNames).toolContext(toolContext).build();
  61 + .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
64 62 case MINI_MAX:
65 63 return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
66   - .toolNames(toolNames).toolContext(toolContext).build();
  64 + .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
67 65 case MOONSHOT:
68 66 return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
69   - .toolNames(toolNames).toolContext(toolContext).build();
  67 + .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
70 68 case OPENAI:
71 69 case GEMINI: // 复用 OpenAI 客户端
72 70 case BAI_CHUAN: // 复用 OpenAI 客户端
73 71 return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
74   - .toolNames(toolNames).toolContext(toolContext).build();
  72 + .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
75 73 case AZURE_OPENAI:
76 74 return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens)
77   - .toolNames(toolNames).toolContext(toolContext).build();
  75 + .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
78 76 case ANTHROPIC:
79 77 return AnthropicChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
80   - .toolNames(toolNames).toolContext(toolContext).build();
  78 + .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
81 79 case OLLAMA:
82 80 return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens)
83   - .toolNames(toolNames).toolContext(toolContext).build();
  81 + .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
84 82 default:
85 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 36  
37 37 @Test
38 38 public void testMcpGetUserInfo() {
39   -
40 39 // 打印结果
41 40 System.out.println(chatClient.prompt()
42 41 .user("目前有哪些工具可以使用")
43 42 .call()
44 43 .content());
45 44 System.out.println("====================================");
  45 +
46 46 // 打印结果
47 47 System.out.println(chatClient.prompt()
48 48 .user("小新的年龄是多少")
49 49 .call()
50 50 .content());
51 51 System.out.println("====================================");
  52 +
52 53 // 打印结果
53 54 System.out.println(chatClient.prompt()
54 55 .user("获取小新的基本信息")
55 56 .call()
56 57 .content());
57 58 System.out.println("====================================");
  59 +
58 60 // 打印结果
59 61 System.out.println(chatClient.prompt()
60 62 .user("小新是什么职业的")
61 63 .call()
62 64 .content());
63 65 System.out.println("====================================");
  66 +
64 67 // 打印结果
65 68 System.out.println(chatClient.prompt()
66 69 .user("小新的教育背景")
67 70 .call()
68 71 .content());
69 72 System.out.println("====================================");
  73 +
70 74 // 打印结果
71 75 System.out.println(chatClient.prompt()
72 76 .user("小新的兴趣爱好是什么")
73 77 .call()
74 78 .content());
75 79 System.out.println("====================================");
76   -
77 80 }
78 81  
79 82  
... ...