From 523d90233cb366cbaaca2775567f4df44eb4a264 Mon Sep 17 00:00:00 2001 From: song <271667068@qq.com> Date: Thu, 10 Jul 2025 22:32:00 +0800 Subject: [PATCH 1/3] [app-builder] parse MCP config and query tool list from MCP service --- app-builder/jane/plugins/aipp-plugin/pom.xml | 4 + .../aipp/common/exception/AippErrCode.java | 5 + .../fit/jober/aipp/fitable/LlmComponent.java | 74 ++++++++- .../fit/jober/aipp/util/McpUtils.java | 45 ++++++ .../resources/i18n/messages_en.properties | 1 + .../resources/i18n/messages_zh.properties | 1 + .../jober/aipp/fitable/LlmComponentTest.java | 141 ++++++++++++++++-- .../fit/jober/aipp/constants/AippConst.java | 41 +++++ .../aipp/common/exception/AippErrCode.java | 5 + .../pages/appDetail/overview/common/config.ts | 4 + common/dependency/pom.xml | 5 + .../main/resources/i18n/aipp_en.properties | 1 + .../main/resources/i18n/aipp_zh.properties | 1 + 13 files changed, 304 insertions(+), 24 deletions(-) create mode 100644 app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/McpUtils.java diff --git a/app-builder/jane/plugins/aipp-plugin/pom.xml b/app-builder/jane/plugins/aipp-plugin/pom.xml index 7722071770..7e82ceca76 100644 --- a/app-builder/jane/plugins/aipp-plugin/pom.xml +++ b/app-builder/jane/plugins/aipp-plugin/pom.xml @@ -134,6 +134,10 @@ modelengine.fit.jade.waterflow waterflow-graph-service + + org.fitframework.fel + tool-mcp-client-service + diff --git a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/common/exception/AippErrCode.java b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/common/exception/AippErrCode.java index 2c03e79b57..ecd511011d 100644 --- a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/common/exception/AippErrCode.java +++ b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/common/exception/AippErrCode.java @@ -137,6 +137,11 @@ public enum AippErrCode implements ErrorCode, RetCode { */ INVALID_FILE_PATH(90002003, "无效文件路径。"), + /** + * 调用 MCP 服务失败。 + */ + CALL_MCP_SERVER_FAILED(90002004, "调用 MCP 服务失败,原因:{0}。"), + /** * json解析失败 */ diff --git a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java index a7f0b611f4..86d28862f9 100644 --- a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java +++ b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java @@ -19,7 +19,12 @@ import modelengine.fel.engine.flows.AiProcessFlow; import modelengine.fel.engine.operators.patterns.AbstractAgent; import modelengine.fel.engine.operators.prompts.Prompts; +import modelengine.fel.tool.mcp.client.McpClient; +import modelengine.fel.tool.mcp.client.McpClientFactory; +import modelengine.fel.tool.mcp.entity.Tool; import modelengine.fel.tool.model.transfer.ToolData; +import modelengine.fit.jober.aipp.util.McpUtils; +import modelengine.fitframework.inspection.Validation; import modelengine.jade.store.service.ToolService; import modelengine.fit.jade.aipp.model.dto.ModelAccessInfo; import modelengine.fit.jade.aipp.model.service.AippModelCenter; @@ -60,6 +65,7 @@ import modelengine.fitframework.util.StringUtils; import modelengine.fitframework.util.UuidUtils; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -69,6 +75,7 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * LLM 组件实现 @@ -101,6 +108,7 @@ public class LlmComponent implements FlowableService { private final AippModelCenter aippModelCenter; private final PromptBuilderChain promptBuilderChain; private final AppTaskInstanceService appTaskInstanceService; + private final McpClientFactory mcpClientFactory; /** * 大模型节点构造器,内部通过提供的 agent 和 tool 构建智能体工作流。 @@ -114,6 +122,7 @@ public class LlmComponent implements FlowableService { * @param aippModelCenter 表示模型中心的 {@link AippModelCenter}。 * @param promptBuilderChain 表示提示器构造器职责链的 {@link PromptBuilderChain}。 * @param appTaskInstanceService 表示任务实例服务的 {@link AppTaskInstanceService}。 + * @param mcpClientFactory 表示大模型上下文客户端工厂的 {@link McpClientFactory}。 */ public LlmComponent(FlowInstanceService flowInstanceService, @Fit ToolService toolService, @@ -123,7 +132,8 @@ public LlmComponent(FlowInstanceService flowInstanceService, @Fit(alias = "json") ObjectSerializer serializer, AippModelCenter aippModelCenter, PromptBuilderChain promptBuilderChain, - AppTaskInstanceService appTaskInstanceService) { + AppTaskInstanceService appTaskInstanceService, + McpClientFactory mcpClientFactory) { this.flowInstanceService = flowInstanceService; this.toolService = toolService; this.aippLogService = aippLogService; @@ -139,6 +149,7 @@ public LlmComponent(FlowInstanceService flowInstanceService, .close(); this.promptBuilderChain = promptBuilderChain; this.appTaskInstanceService = appTaskInstanceService; + this.mcpClientFactory = notNull(mcpClientFactory, "The mcp client factory cannot be null."); } /** @@ -177,6 +188,7 @@ public List> handleTask(List> flowData) StreamMsgSender streamMsgSender = new StreamMsgSender(this.aippLogStreamService, this.serializer, path, msgId, instId); streamMsgSender.sendKnowledge(promptMessage.getMetadata(), businessData); + ChatOption chatOption = buildChatOptions(businessData); agentFlow.converse() .bind((acc, chunk) -> { if (firstTokenFlag[0]) { @@ -195,7 +207,8 @@ public List> handleTask(List> flowData) .doOnConsume(msg -> llmOutputConsumer(llmMeta, msg, promptMessage.getMetadata())) .doOnError(throwable -> doOnAgentError(llmMeta, throwable.getCause() == null ? throwable.getMessage() : throwable.getCause().getMessage())) - .bind(buildChatOptions(businessData)) + .bind(chatOption) + .bind(AippConst.TOOLS_KEY, chatOption.tools()) .offer(Tip.fromArray(promptMessage.getSystemMessage(), promptMessage.getHumanMessage())); log.info("[perf] [{}] handleTask end, instId={}", System.currentTimeMillis(), instId); return flowData; @@ -393,10 +406,6 @@ private String getFilePath(Map businessData) { * @return 返回表示自定义参数。 */ private ChatOption buildChatOptions(Map businessData) { - List skillNameList = new ArrayList<>(ObjectUtils.cast(businessData.get("tools"))); - if (businessData.containsKey("workflows")) { - skillNameList.addAll(ObjectUtils.cast(businessData.get("workflows"))); - } String model = ObjectUtils.cast(businessData.get("model")); Map accessInfo = ObjectUtils.nullIf(ObjectUtils.cast(businessData.get("accessInfo")), MapBuilder.get().put("serviceName", model).put("tag", "INTERNAL").build()); @@ -413,10 +422,50 @@ private ChatOption buildChatOptions(Map businessData) { .secureConfig(modelAccessInfo.isSystemModel() ? null : SecureConfig.custom().ignoreTrust(true).build()) .apiKey(modelAccessInfo.getAccessKey()) .temperature(ObjectUtils.cast(businessData.get("temperature"))) - .tools(this.buildToolInfos(skillNameList)) + .tools(this.buildToolInfos(businessData)) .build(); } + private List buildToolInfos(Map businessData) { + List skillNameList = new ArrayList<>(ObjectUtils.cast(businessData.get("tools"))); + if (businessData.containsKey("workflows")) { + skillNameList.addAll(ObjectUtils.cast(businessData.get("workflows"))); + } + Map mcpServersConfig = ObjectUtils.cast(businessData.get(AippConst.MCP_SERVERS_KEY)); + + return Stream.concat(this.buildToolInfos(skillNameList).stream(), + this.buildMcpToolInfos(mcpServersConfig).stream()).collect(Collectors.toList()); + } + + private List buildMcpToolInfos(Map mcpServersConfig) { + List result = new ArrayList<>(); + ObjectUtils.nullIf(mcpServersConfig, new HashMap()).forEach((serverName, value) -> { + Map serverConfig = ObjectUtils.cast(value); + String url = Validation.notBlank(ObjectUtils.cast(serverConfig.get(AippConst.MCP_SERVER_URL_KEY)), + "The mcp url should not be empty."); + + try (McpClient mcpClient = this.mcpClientFactory.create(McpUtils.getBaseUrl(url), + McpUtils.getSseEndpoint(url))) { + mcpClient.initialize(); + List tools = mcpClient.getTools(); + result.addAll(tools.stream() + .map(tool -> ToolInfo.custom() + .name(buildUniqueToolName(AippConst.MCP_SERVER_TYPE, serverName, tool.getName())) + .description(tool.getDescription()) + .parameters(tool.getInputSchema()) + .extensions(MapBuilder.get() + .put(AippConst.MCP_SERVER_KEY, serverConfig) + .put(AippConst.TOOL_REAL_NAME, tool.getName()) + .build()) + .build()) + .toList()); + } catch (IOException exception) { + throw new AippException(AippErrCode.CALL_MCP_SERVER_FAILED, exception.getMessage()); + } + }); + return result; + } + private List buildToolInfos(List skillNameList) { return skillNameList.stream() .map(this.toolService::getTool) @@ -427,12 +476,21 @@ private List buildToolInfos(List skillNameList) { private ToolInfo buildToolInfo(ToolData toolData) { return ToolInfo.custom() - .name(toolData.getUniqueName()) + .name(buildUniqueToolName(AippConst.STORE_SERVER_TYPE, + AippConst.STORE_SERVER_NAME, + toolData.getUniqueName())) .description(toolData.getDescription()) .parameters(new HashMap<>(toolData.getSchema())) + .extensions(MapBuilder.get() + .put(AippConst.TOOL_REAL_NAME, toolData.getUniqueName()) + .build()) .build(); } + private static String buildUniqueToolName(String type, String serverName, String toolName) { + return String.format("%s_%s_%s", type, serverName, toolName); + } + public static boolean checkEnableLog(Map businessData) { Object value = businessData.get(AippConst.BS_LLM_ENABLE_LOG); if (value == null) { diff --git a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/McpUtils.java b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/McpUtils.java new file mode 100644 index 0000000000..dac8353e1a --- /dev/null +++ b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/McpUtils.java @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fit.jober.aipp.util; + +import modelengine.fitframework.inspection.Validation; + +/** + * 大模型上下文协议相关工具方法。 + * + * @author 宋永坦 + * @since 2025-07-11 + */ +public class McpUtils { + private static final String SSE_ENDPOINT_SPLIT_DELIMITER = "/"; + + /** + * 获取 {@code baseUrl} 部分。 + * + * @param url 目标地址。 + * @return {@code baseUrl} 部分。 + * @throws IllegalArgumentException 当目标地址不包含 {@code sseEndpoint} 时。 + */ + public static String getBaseUrl(String url) { + String[] splits = url.split(SSE_ENDPOINT_SPLIT_DELIMITER); + Validation.greaterThan(splits.length, 3, "The url is wrong. [url={0}]", url); + return url.substring(0, url.length() - splits[splits.length - 1].length() - 1); + } + + /** + * 获取 {@code sseEndpoint} 部分。 + * + * @param url 目标地址。 + * @return {@code sseEndpoint} 部分。 + * @throws IllegalArgumentException 当目标地址不包含 {@code sseEndpoint} 时。 + */ + public static String getSseEndpoint(String url) { + String[] splits = url.split(SSE_ENDPOINT_SPLIT_DELIMITER); + Validation.greaterThan(splits.length, 3, "The url is wrong. [url={0}]", url); + return SSE_ENDPOINT_SPLIT_DELIMITER + splits[splits.length - 1]; + } +} diff --git a/app-builder/jane/plugins/aipp-plugin/src/main/resources/i18n/messages_en.properties b/app-builder/jane/plugins/aipp-plugin/src/main/resources/i18n/messages_en.properties index 51568f6565..47e9ddb33e 100644 --- a/app-builder/jane/plugins/aipp-plugin/src/main/resources/i18n/messages_en.properties +++ b/app-builder/jane/plugins/aipp-plugin/src/main/resources/i18n/messages_en.properties @@ -20,6 +20,7 @@ 90002001=The file has expired or is damaged. 90002002=Failed to parse the file. 90002003=Invalid file path. +90002004=Failed to call MCP service. reason: {0}. 90002900=JSON parsing failed. Cause: {0}. 90002901=JSON encoding failed. Cause: {0}. 90002902=Failed to obtain historical records. diff --git a/app-builder/jane/plugins/aipp-plugin/src/main/resources/i18n/messages_zh.properties b/app-builder/jane/plugins/aipp-plugin/src/main/resources/i18n/messages_zh.properties index 18113442a6..3c51155780 100644 --- a/app-builder/jane/plugins/aipp-plugin/src/main/resources/i18n/messages_zh.properties +++ b/app-builder/jane/plugins/aipp-plugin/src/main/resources/i18n/messages_zh.properties @@ -20,6 +20,7 @@ 90002001=文件过期或损坏。 90002002=解析文件内容失败。 90002003=无效文件路径。 +90002004=调用 MCP 服务失败,原因:{0}。 90002900=json解码失败,原因:{0}。 90002901=json编码失败,原因:{0}。 90002902=获取历史记录失败。 diff --git a/app-builder/jane/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java b/app-builder/jane/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java index f16f800a00..e2c29a3a76 100644 --- a/app-builder/jane/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java +++ b/app-builder/jane/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java @@ -10,10 +10,18 @@ import static modelengine.fit.jober.aipp.TestUtils.mockResumeFlow; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.times; import static org.mockito.Mockito.when; +import modelengine.fel.core.chat.ChatOption; +import modelengine.fel.core.tool.ToolInfo; import modelengine.fel.engine.operators.models.ChatFlowModel; +import modelengine.fel.tool.ToolSchema; +import modelengine.fel.tool.mcp.client.McpClient; +import modelengine.fel.tool.mcp.client.McpClientFactory; +import modelengine.fel.tool.mcp.entity.Tool; +import modelengine.fel.tool.model.transfer.ToolData; import modelengine.fit.jade.aipp.model.dto.ModelListDto; import modelengine.fit.jade.aipp.model.service.AippModelCenter; import modelengine.fit.jade.aipp.prompt.PromptBuilder; @@ -26,7 +34,6 @@ import modelengine.fit.jade.waterflow.FlowInstanceService; import modelengine.fit.jober.aipp.TestUtils; import modelengine.fit.jober.aipp.constants.AippConst; -import modelengine.fit.jober.aipp.domains.taskinstance.AppTaskInstance; import modelengine.fit.jober.aipp.domains.taskinstance.service.AppTaskInstanceService; import modelengine.fit.jober.aipp.fel.WaterFlowAgent; import modelengine.fit.jober.aipp.service.AippLogService; @@ -48,19 +55,21 @@ import modelengine.fit.serialization.json.jackson.JacksonObjectSerializer; import modelengine.fitframework.flowable.Choir; import modelengine.fitframework.serialization.ObjectSerializer; +import modelengine.fitframework.util.CollectionUtils; import modelengine.fitframework.util.MapBuilder; import modelengine.fitframework.util.ObjectUtils; +import modelengine.fitframework.util.StringUtils; import modelengine.jade.store.service.ToolService; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.stubbing.Answer; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -94,6 +103,8 @@ public class LlmComponentTest { private final ObjectSerializer serializer = new JacksonObjectSerializer(null, null, null, true); @Mock private AippModelCenter aippModelCenter; + @Mock + private McpClientFactory mcpClientFactory; static class PromptBuilderStub implements PromptBuilder { @Override @@ -220,15 +231,16 @@ void shouldFailedWhenNoTool() throws InterruptedException { throw new RuntimeException("test"); }).map(m -> ObjectUtils.cast(new AiMessage("bad"))).close(); AbstractAgent agent = this.buildStubAgent(testAgent); - LlmComponent llmComponent = new LlmComponent(flowInstanceService, + LlmComponent llmComponent = new LlmComponent(this.flowInstanceService, this.toolService, agent, - aippLogService, + this.aippLogService, null, - serializer, - aippModelCenter, - promptBuilderChain, - this.appTaskInstanceService); + this.serializer, + this.aippModelCenter, + this.promptBuilderChain, + this.appTaskInstanceService, + this.mcpClientFactory); // mock CountDownLatch countDownLatch = mockFailAsyncJob(flowInstanceService); @@ -289,16 +301,113 @@ void shouldFailWhenDebugAndLlmNotAvailable() throws InterruptedException { countDownLatch.await(); } + @Test + void shouldGetStoreToolsWhenHandleTaskGivenStoreToolsConfig() throws InterruptedException { + // given + this.prepareModel(); + ChatModel chatModel = Mockito.mock(ChatModel.class); + doAnswer(invocation -> Choir.create(emitter -> { + for (int i = 0; i < 4; i++) { + emitter.emit(new AiMessage(String.valueOf(i))); + } + emitter.complete(); + })).when(chatModel).generate(any(), any()); + + AbstractAgent agent = this.getWaterFlowAgent(chatModel); + LlmComponent llmComponent = getLlmComponent(agent); + + CountDownLatch countDownLatch = mockResumeFlow(flowInstanceService); + Map businessData = buildLlmTestData(); + String toolUniqueName = "tool1"; + businessData.put(AippConst.TOOLS_KEY, Arrays.asList(toolUniqueName)); + ToolData tool = new ToolData(); + tool.setName(toolUniqueName); + tool.setUniqueName(toolUniqueName); + tool.setDescription("desc"); + tool.setSchema(MapBuilder.get() + .put(ToolSchema.PARAMETERS, MapBuilder.get().put("name", toolUniqueName).build()) + .build()); + when(this.toolService.getTool(toolUniqueName)).thenReturn(tool); + + // when + llmComponent.handleTask(TestUtils.buildFlowDataWithExtraConfig(businessData, null)); + + // then + countDownLatch.await(); + ArgumentCaptor chatOptionCaptor = ArgumentCaptor.forClass(ChatOption.class); + Mockito.verify(chatModel).generate(any(), chatOptionCaptor.capture()); + ChatOption capturedChatOptions = chatOptionCaptor.getValue(); + Assertions.assertTrue(CollectionUtils.isNotEmpty(capturedChatOptions.tools())); + Assertions.assertEquals(1, capturedChatOptions.tools().size()); + ToolInfo toolInfo = capturedChatOptions.tools().get(0); + Assertions.assertEquals(AippConst.STORE_SERVER_TYPE + "_" + AippConst.STORE_SERVER_NAME + "_" + tool.getName(), + toolInfo.name()); + Assertions.assertEquals(tool.getDescription(), toolInfo.description()); + Assertions.assertEquals(tool.getSchema(), toolInfo.parameters()); + } + + @Test + void shouldGetMcpToolsWhenHandleTaskGivenMcpServersConfig() throws InterruptedException { + // given + this.prepareModel(); + ChatModel chatModel = Mockito.mock(ChatModel.class); + doAnswer(invocation -> Choir.create(emitter -> { + for (int i = 0; i < 4; i++) { + emitter.emit(new AiMessage(String.valueOf(i))); + } + emitter.complete(); + })).when(chatModel).generate(any(), any()); + + AbstractAgent agent = this.getWaterFlowAgent(chatModel); + LlmComponent llmComponent = getLlmComponent(agent); + + CountDownLatch countDownLatch = mockResumeFlow(flowInstanceService); + Map businessData = buildLlmTestData(); + String baseUrl = "http://127.0.0.1"; + String sseEndpoint = "/sse"; + String url = baseUrl + sseEndpoint; + Map mcpServerInfo = MapBuilder.get().put("url", url).build(); + String serverName = "server1"; + businessData.put(AippConst.MCP_SERVERS_KEY, MapBuilder.get().put(serverName, mcpServerInfo).build()); + McpClient mcpCLient = Mockito.mock(McpClient.class); + doNothing().when(mcpCLient).initialize(); + Tool tool = new Tool(); + tool.setName("tool1"); + tool.setDescription("desc"); + tool.setInputSchema(new HashMap<>()); + when(mcpCLient.getTools()).thenReturn(Arrays.asList(tool)); + when(this.mcpClientFactory.create(baseUrl, sseEndpoint)).thenReturn(mcpCLient); + + // when + llmComponent.handleTask(TestUtils.buildFlowDataWithExtraConfig(businessData, null)); + + // then + countDownLatch.await(); + ArgumentCaptor chatOptionCaptor = ArgumentCaptor.forClass(ChatOption.class); + Mockito.verify(chatModel).generate(any(), chatOptionCaptor.capture()); + ChatOption capturedChatOptions = chatOptionCaptor.getValue(); + Assertions.assertTrue(CollectionUtils.isNotEmpty(capturedChatOptions.tools())); + Assertions.assertEquals(1, capturedChatOptions.tools().size()); + ToolInfo toolInfo = capturedChatOptions.tools().get(0); + Assertions.assertEquals("mcp_" + serverName + "_" + tool.getName(), toolInfo.name()); + Assertions.assertEquals(tool.getDescription(), toolInfo.description()); + Assertions.assertEquals(tool.getInputSchema(), toolInfo.parameters()); + Assertions.assertEquals(mcpServerInfo, toolInfo.extensions().get(AippConst.MCP_SERVER_KEY)); + } + + + private LlmComponent getLlmComponent(final AbstractAgent agent) { - return new LlmComponent(flowInstanceService, + return new LlmComponent(this.flowInstanceService, this.toolService, agent, - aippLogService, - aippLogStreamService, - serializer, - aippModelCenter, - promptBuilderChain, - this.appTaskInstanceService); + this.aippLogService, + this.aippLogStreamService, + this.serializer, + this.aippModelCenter, + this.promptBuilderChain, + this.appTaskInstanceService, + this.mcpClientFactory); } private void prepareModel() { diff --git a/app-builder/jane/services/aipp-genericable/src/main/java/modelengine/fit/jober/aipp/constants/AippConst.java b/app-builder/jane/services/aipp-genericable/src/main/java/modelengine/fit/jober/aipp/constants/AippConst.java index be4b4e17bc..3224b16389 100644 --- a/app-builder/jane/services/aipp-genericable/src/main/java/modelengine/fit/jober/aipp/constants/AippConst.java +++ b/app-builder/jane/services/aipp-genericable/src/main/java/modelengine/fit/jober/aipp/constants/AippConst.java @@ -769,6 +769,47 @@ public class AippConst { */ public static final String BUSINESS_INFOS_KEY = "infos"; + /** + * mcp server key + */ + public static final String MCP_SERVER_KEY = "mcpServer"; + + /** + * mcp servers key + */ + public static final String MCP_SERVERS_KEY = "mcpServers"; + + /** + * mcp server url key + */ + public static final String MCP_SERVER_URL_KEY = "url"; + + + /** + * mcp server type + */ + public static final String MCP_SERVER_TYPE = "mcp"; + + /** + * store server type + */ + public static final String STORE_SERVER_TYPE = "store"; + + /** + * store server name + */ + public static final String STORE_SERVER_NAME = "store"; + + /** + * tool real name + */ + public static final String TOOL_REAL_NAME = "toolRealName"; + + /** + * tools key + */ + public static final String TOOLS_KEY = "tools"; + // *** aipp initial static meta items *** /** * aipp initial static meta items diff --git a/app-builder/jane/services/aipp-service/src/main/java/modelengine/fit/jober/aipp/common/exception/AippErrCode.java b/app-builder/jane/services/aipp-service/src/main/java/modelengine/fit/jober/aipp/common/exception/AippErrCode.java index a4e9ea520e..4a1e64cc09 100644 --- a/app-builder/jane/services/aipp-service/src/main/java/modelengine/fit/jober/aipp/common/exception/AippErrCode.java +++ b/app-builder/jane/services/aipp-service/src/main/java/modelengine/fit/jober/aipp/common/exception/AippErrCode.java @@ -137,6 +137,11 @@ public enum AippErrCode implements ErrorCode, RetCode { */ INVALID_FILE_PATH(90002003, "无效文件路径。"), + /** + * 调用 MCP 服务失败。 + */ + CALL_MCP_SERVER_FAILED(90002004, "调用 MCP 服务失败,原因:{0}。"), + /** * json解析失败 */ diff --git a/app-engine/frontend/src/pages/appDetail/overview/common/config.ts b/app-engine/frontend/src/pages/appDetail/overview/common/config.ts index f92d4ef47f..f0a8c5b48f 100644 --- a/app-engine/frontend/src/pages/appDetail/overview/common/config.ts +++ b/app-engine/frontend/src/pages/appDetail/overview/common/config.ts @@ -1706,6 +1706,10 @@ export const errorCodeData = [ code: 90002003, message: '无效文件路径。', }, + { + code: 90002004, + message: '调用 MCP 服务失败,原因:{错误信息}。', + }, { code: 90002900, message: 'Json解析失败,原因:{错误信息}。', diff --git a/common/dependency/pom.xml b/common/dependency/pom.xml index 457b002792..1ca55c7611 100644 --- a/common/dependency/pom.xml +++ b/common/dependency/pom.xml @@ -227,6 +227,11 @@ fel-flow ${fel.version} + + org.fitframework.fel + tool-mcp-client-service + ${fel.version} + modelengine.fit.jade.service diff --git a/common/plugins/http-interceptor/src/main/resources/i18n/aipp_en.properties b/common/plugins/http-interceptor/src/main/resources/i18n/aipp_en.properties index adcb2ff4fb..ceb9bdff5c 100644 --- a/common/plugins/http-interceptor/src/main/resources/i18n/aipp_en.properties +++ b/common/plugins/http-interceptor/src/main/resources/i18n/aipp_en.properties @@ -20,6 +20,7 @@ 90002001=The file has expired or is damaged. 90002002=Failed to parse the file. 90002003=Invalid file path. +90002004=Failed to call MCP service. reason: {0}. 90002900=JSON parsing failed. Cause: {0}. 90002901=JSON encoding failed. Cause: {0}. 90002902=Failed to obtain historical records. diff --git a/common/plugins/http-interceptor/src/main/resources/i18n/aipp_zh.properties b/common/plugins/http-interceptor/src/main/resources/i18n/aipp_zh.properties index 875ac29637..c44f6ebca6 100644 --- a/common/plugins/http-interceptor/src/main/resources/i18n/aipp_zh.properties +++ b/common/plugins/http-interceptor/src/main/resources/i18n/aipp_zh.properties @@ -20,6 +20,7 @@ 90002001=文件过期或损坏。 90002002=解析文件内容失败。 90002003=无效文件路径。 +90002004=调用 MCP 服务失败,原因:{0}。 90002900=json解码失败,原因:{0}。 90002901=json编码失败,原因:{0}。 90002902=获取历史记录失败。 From b039d559e15146ebb42d8106bf7b61f0c018cc73 Mon Sep 17 00:00:00 2001 From: song <271667068@qq.com> Date: Mon, 14 Jul 2025 16:25:24 +0800 Subject: [PATCH 2/3] [app-builder] support MCP tool invocation in LLM nodes --- .../jober/aipp/fel/FelComponentConfig.java | 6 +- .../fit/jober/aipp/fel/WaterFlowAgent.java | 79 ++++++-- .../jober/aipp/fel/WaterFlowAgentTest.java | 183 ++++++++++++++++++ .../jober/aipp/fitable/LlmComponentTest.java | 2 +- 4 files changed, 252 insertions(+), 18 deletions(-) create mode 100644 app-builder/jane/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fel/WaterFlowAgentTest.java diff --git a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/FelComponentConfig.java b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/FelComponentConfig.java index e9671c9d31..ab9296f730 100644 --- a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/FelComponentConfig.java +++ b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/FelComponentConfig.java @@ -9,6 +9,7 @@ import modelengine.fel.core.chat.ChatModel; import modelengine.fel.core.chat.Prompt; import modelengine.fel.engine.operators.patterns.AbstractAgent; +import modelengine.fel.tool.mcp.client.McpClientFactory; import modelengine.fit.jade.tool.SyncToolCall; import modelengine.fit.jober.aipp.constants.AippConst; import modelengine.fitframework.annotation.Bean; @@ -28,11 +29,12 @@ public class FelComponentConfig { * * @param syncToolCall 表示同步工具调用服务的 {@link SyncToolCall}。 * @param chatModel 表示模型流式服务的 {@link ChatModel}。 + * @param mcpClientFactory 表示大模型上下文客户端工厂的 {@link McpClientFactory}。 * @return 返回 WaterFlow 场景的 Agent 服务的 {@link AbstractAgent}{@code <}{@link Prompt}{@code , * }{@link Prompt}{@code >}。 */ @Bean(AippConst.WATER_FLOW_AGENT_BEAN) - public AbstractAgent getWaterFlowAgent(@Fit SyncToolCall syncToolCall, ChatModel chatModel) { - return new WaterFlowAgent(syncToolCall, chatModel); + public AbstractAgent getWaterFlowAgent(@Fit SyncToolCall syncToolCall, ChatModel chatModel, McpClientFactory mcpClientFactory) { + return new WaterFlowAgent(syncToolCall, chatModel, mcpClientFactory); } } \ No newline at end of file diff --git a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/WaterFlowAgent.java b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/WaterFlowAgent.java index 72b95ff911..4a8a822608 100644 --- a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/WaterFlowAgent.java +++ b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fel/WaterFlowAgent.java @@ -6,6 +6,9 @@ package modelengine.fit.jober.aipp.fel; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; + import modelengine.fel.core.chat.ChatMessage; import modelengine.fel.core.chat.ChatModel; import modelengine.fel.core.chat.Prompt; @@ -13,21 +16,30 @@ import modelengine.fel.core.chat.support.FlatChatMessage; import modelengine.fel.core.chat.support.ToolMessage; import modelengine.fel.core.tool.ToolCall; +import modelengine.fel.core.tool.ToolInfo; import modelengine.fel.engine.flows.AiFlows; import modelengine.fel.engine.flows.AiProcessFlow; import modelengine.fel.engine.operators.models.ChatChunk; import modelengine.fel.engine.operators.models.ChatFlowModel; import modelengine.fel.engine.operators.patterns.AbstractAgent; +import modelengine.fel.tool.mcp.client.McpClient; +import modelengine.fel.tool.mcp.client.McpClientFactory; import modelengine.fit.jade.tool.SyncToolCall; +import modelengine.fit.jober.aipp.common.exception.AippErrCode; +import modelengine.fit.jober.aipp.common.exception.AippException; import modelengine.fit.jober.aipp.constants.AippConst; +import modelengine.fit.jober.aipp.util.McpUtils; import modelengine.fit.waterflow.domain.context.StateContext; import modelengine.fitframework.annotation.Fit; import modelengine.fitframework.inspection.Validation; +import modelengine.fitframework.util.CollectionUtils; import modelengine.fitframework.util.ObjectUtils; +import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.stream.Collectors; /** @@ -42,28 +54,30 @@ public class WaterFlowAgent extends AbstractAgent { private final String agentMsgKey; private final SyncToolCall syncToolCall; + private final McpClientFactory mcpClientFactory; /** * {@link WaterFlowAgent} 的构造方法。 * * @param syncToolCall 表示工具调用服务的 {@link SyncToolCall}。 * @param chatStreamModel 表示流式对话大模型的 {@link ChatModel}。 + * @param mcpClientFactory 表示大模型上下文客户端工厂的 {@link McpClientFactory}。 */ - public WaterFlowAgent(@Fit SyncToolCall syncToolCall, ChatModel chatStreamModel) { + public WaterFlowAgent(@Fit SyncToolCall syncToolCall, ChatModel chatStreamModel, + McpClientFactory mcpClientFactory) { super(new ChatFlowModel(chatStreamModel, null)); - this.syncToolCall = Validation.notNull(syncToolCall, "The tool sync tool call cannot be null."); + this.syncToolCall = Validation.notNull(syncToolCall, "The tool sync tool call cannot be null."); + this.mcpClientFactory = Validation.notNull(mcpClientFactory, "The mcp client factory cannot be null."); this.agentMsgKey = AGENT_MSG_KEY; } @Override protected Prompt doToolCall(List toolCalls, StateContext ctx) { Validation.notNull(ctx, "The state context cannot be null."); - Map toolContext = ObjectUtils.getIfNull(ctx.getState(AippConst.TOOL_CONTEXT_KEY), - Collections::emptyMap); - return toolCalls.stream() - .map(toolCall -> (ChatMessage) new ToolMessage(toolCall.id(), - this.syncToolCall.call(toolCall.name(), toolCall.arguments(), toolContext))) - .collect(Collectors.collectingAndThen(Collectors.toList(), ChatMessages::from)); + return ChatMessages.from(this.callTools(toolCalls, ctx) + .stream() + .map(message -> (ChatMessage) FlatChatMessage.from(message)) + .collect(Collectors.toList())); } @Override @@ -87,18 +101,53 @@ public AiProcessFlow buildFlow() { private ChatMessage handleTool(ChatMessage input, StateContext ctx) { Validation.notNull(ctx, "The state context cannot be null."); Validation.notNull(input, "The input message cannot be null."); - - Map toolContext = ObjectUtils.getIfNull(ctx.getState(AippConst.TOOL_CONTEXT_KEY), - Collections::emptyMap); ChatMessages lastRequest = ctx.getState(this.agentMsgKey); lastRequest.add(input); - input.toolCalls().forEach(toolCall -> { - lastRequest.add(FlatChatMessage.from(new ToolMessage(toolCall.id(), - this.syncToolCall.call(toolCall.name(), toolCall.arguments(), toolContext)))); - }); + lastRequest.addAll(this.callTools(input.toolCalls(), ctx)); return input; } + private List callTools(List toolCalls, StateContext ctx) { + if (CollectionUtils.isEmpty(toolCalls)) { + return Collections.emptyList(); + } + List tools = ctx.getState(AippConst.TOOLS_KEY); + Validation.notEmpty(tools, "Missing tool detected during call."); + Map toolsMap = tools.stream().collect(Collectors.toMap(ToolInfo::name, Function.identity())); + Map toolContext = + ObjectUtils.getIfNull(ctx.getState(AippConst.TOOL_CONTEXT_KEY), Collections::emptyMap); + return toolCalls.stream() + .map(toolCall -> this.callTool(toolCall, toolsMap, toolContext)) + .collect(Collectors.toList()); + } + + private ChatMessage callTool(ToolCall toolCall, Map toolsMap, Map toolContext) { + ToolInfo toolInfo = toolsMap.get(toolCall.name()); + if (toolInfo == null) { + throw new IllegalStateException(String.format("The tool call's tool is not exist. [toolName=%s]", + toolCall.name())); + } + Map extensions = Validation.notNull(toolInfo.extensions(), + "The tool call's extension is not exist. [toolName={0}]", toolCall.name()); + String toolRealName = Validation.notBlank(ObjectUtils.cast(extensions.get(AippConst.TOOL_REAL_NAME)), + "Can not find the tool real name. [toolName={0}]", + toolCall.name()); + Map mcpServerConfig = ObjectUtils.cast(extensions.get(AippConst.MCP_SERVER_KEY)); + if (mcpServerConfig != null) { + String url = Validation.notBlank(ObjectUtils.cast(mcpServerConfig.get(AippConst.MCP_SERVER_URL_KEY)), + "The mcp url should not be empty."); + try (McpClient mcpClient = this.mcpClientFactory.create(McpUtils.getBaseUrl(url), + McpUtils.getSseEndpoint(url))) { + mcpClient.initialize(); + Object result = mcpClient.callTool(toolRealName, JSONObject.parseObject(toolCall.arguments())); + return new ToolMessage(toolCall.id(), JSON.toJSONString(result)); + } catch (IOException exception) { + throw new AippException(AippErrCode.CALL_MCP_SERVER_FAILED, exception.getMessage()); + } + } + return new ToolMessage(toolCall.id(), this.syncToolCall.call(toolRealName, toolCall.arguments(), toolContext)); + } + private ChatMessages getAgentMsg(ChatMessage input, StateContext ctx) { Validation.notNull(ctx, "The state context cannot be null."); return ctx.getState(this.agentMsgKey); diff --git a/app-builder/jane/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fel/WaterFlowAgentTest.java b/app-builder/jane/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fel/WaterFlowAgentTest.java new file mode 100644 index 0000000000..ff4507c2a4 --- /dev/null +++ b/app-builder/jane/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fel/WaterFlowAgentTest.java @@ -0,0 +1,183 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fit.jober.aipp.fel; + +import modelengine.fel.core.chat.ChatMessage; +import modelengine.fel.core.chat.ChatModel; +import modelengine.fel.core.chat.ChatOption; +import modelengine.fel.core.chat.Prompt; +import modelengine.fel.core.chat.support.AiMessage; +import modelengine.fel.core.chat.support.ChatMessages; +import modelengine.fel.core.chat.support.HumanMessage; +import modelengine.fel.core.tool.ToolCall; +import modelengine.fel.core.tool.ToolInfo; +import modelengine.fel.engine.flows.AiProcessFlow; +import modelengine.fel.tool.mcp.client.McpClient; +import modelengine.fel.tool.mcp.client.McpClientFactory; +import modelengine.fit.jade.tool.SyncToolCall; +import modelengine.fit.jober.aipp.constants.AippConst; +import modelengine.fitframework.flowable.Choir; +import modelengine.fitframework.util.MapBuilder; + +import org.apache.commons.collections.CollectionUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * {@link WaterFlowAgent} 的测试。 + */ +@ExtendWith(MockitoExtension.class) +class WaterFlowAgentTest { + private static final String TEXT_STEP = "textStep"; + private static final String TOOL_CALL_STEP = "toolCallStep"; + + @Mock + private SyncToolCall syncToolCall; + @Mock + private ChatModel chatModel; + @Mock + private McpClientFactory mcpClientFactory; + + @Test + void shouldGetResultWhenRunFlowGivenNoToolCall() { + WaterFlowAgent waterFlowAgent = new WaterFlowAgent(this.syncToolCall, this.chatModel, this.mcpClientFactory); + + String expectResult = "0123"; + doAnswer(invocation -> Choir.create(emitter -> { + for (int i = 0; i < 4; i++) { + emitter.emit(new AiMessage(String.valueOf(i))); + } + emitter.complete(); + })).when(chatModel).generate(any(), any()); + + AiProcessFlow flow = waterFlowAgent.buildFlow(); + ChatMessage result = flow.converse() + .bind(ChatOption.custom().build()) + .offer(ChatMessages.from(new HumanMessage("hi"))).await(); + + assertEquals(expectResult, result.text()); + } + + @Test + void shouldGetResultWhenRunFlowGivenStoreToolCall() { + WaterFlowAgent waterFlowAgent = new WaterFlowAgent(this.syncToolCall, this.chatModel, this.mcpClientFactory); + + String expectResult = "tool result:0123"; + String realName = "realName"; + ToolInfo toolInfo = buildToolInfo(realName); + ToolCall toolCall = ToolCall.custom().id("id").name(toolInfo.name()).arguments("{}").build(); + List toolCalls = Collections.singletonList(toolCall); + AtomicReference step = new AtomicReference<>(TOOL_CALL_STEP); + doAnswer(invocation -> { + Prompt prompt = invocation.getArgument(0); + Choir result = mockGenerateResult(step.get(), toolCalls, prompt); + step.set(TEXT_STEP); + return result; + }).when(chatModel).generate(any(), any()); + Map toolContext = MapBuilder.get().put("key", "value").build(); + when(this.syncToolCall.call(realName, toolCall.arguments(), toolContext)).thenReturn("tool result:"); + + AiProcessFlow flow = waterFlowAgent.buildFlow(); + ChatMessage result = flow.converse() + .bind(ChatOption.custom().build()) + .bind(AippConst.TOOL_CONTEXT_KEY, toolContext) + .bind(AippConst.TOOLS_KEY, Collections.singletonList(toolInfo)) + .offer(ChatMessages.from(new HumanMessage("hi"))).await(); + + verify(this.mcpClientFactory, times(0)).create(any(), any()); + assertEquals(expectResult, result.text()); + } + + @Test + void shouldGetResultWhenRunFlowGivenMcpToolCall() { + WaterFlowAgent waterFlowAgent = new WaterFlowAgent(this.syncToolCall, this.chatModel, this.mcpClientFactory); + + String expectResult = "\"tool result:\"0123"; + String realName = "realName"; + String baseUrl = "http://localhost"; + String sseEndpoint = "/sse"; + ToolInfo toolInfo = buildMcpToolInfo(realName, baseUrl, sseEndpoint); + ToolCall toolCall = ToolCall.custom().id("id").name(toolInfo.name()).arguments("{}").build(); + List toolCalls = Collections.singletonList(toolCall); + AtomicReference step = new AtomicReference<>(TOOL_CALL_STEP); + doAnswer(invocation -> { + Prompt prompt = invocation.getArgument(0); + Choir result = mockGenerateResult(step.get(), toolCalls, prompt); + step.set(TEXT_STEP); + return result; + }).when(chatModel).generate(any(), any()); + Map toolContext = MapBuilder.get().put("key", "value").build(); + McpClient mcpClient = mock(McpClient.class); + when(this.mcpClientFactory.create(baseUrl, sseEndpoint)).thenReturn(mcpClient); + when(mcpClient.callTool(realName, new HashMap<>())).thenReturn("tool result:"); + + AiProcessFlow flow = waterFlowAgent.buildFlow(); + ChatMessage result = flow.converse() + .bind(ChatOption.custom().build()) + .bind(AippConst.TOOL_CONTEXT_KEY, toolContext) + .bind(AippConst.TOOLS_KEY, Collections.singletonList(toolInfo)) + .offer(ChatMessages.from(new HumanMessage("hi"))).await(); + + verify(this.syncToolCall, times(0)).call(any(), any(), any()); + assertEquals(expectResult, result.text()); + } + + private static Choir mockGenerateResult(String step, List toolCalls, Prompt prompt) { + return Choir.create(emitter -> { + if (TOOL_CALL_STEP.equals(step)) { + emitter.emit(new AiMessage("tool_data", toolCalls)); + emitter.complete(); + return; + } + if (CollectionUtils.isNotEmpty(prompt.messages())) { + emitter.emit(new AiMessage(prompt.messages().get(prompt.messages().size() - 1).text())); + } + for (int i = 0; i < 4; i++) { + emitter.emit(new AiMessage(String.valueOf(i))); + } + emitter.complete(); + }); + } + + private static ToolInfo buildToolInfo(String realName) { + return ToolInfo.custom() + .name("tool1") + .description("desc") + .parameters(new HashMap<>()) + .extensions(MapBuilder.get().put(AippConst.TOOL_REAL_NAME, realName).build()) + .build(); + } + + private static ToolInfo buildMcpToolInfo(String realName, String baseUrl, String sseEndpoint) { + return ToolInfo.custom() + .name("tool1") + .description("desc") + .parameters(new HashMap<>()) + .extensions(MapBuilder.get() + .put(AippConst.TOOL_REAL_NAME, realName) + .put(AippConst.MCP_SERVER_KEY, + MapBuilder.get().put(AippConst.MCP_SERVER_URL_KEY, baseUrl + sseEndpoint).build()) + .build()) + .build(); + } +} \ No newline at end of file diff --git a/app-builder/jane/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java b/app-builder/jane/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java index e2c29a3a76..236fc6915a 100644 --- a/app-builder/jane/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java +++ b/app-builder/jane/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java @@ -166,7 +166,7 @@ protected AiProcessFlow buildFlow() { } private AbstractAgent getWaterFlowAgent(ChatModel model) { - return new WaterFlowAgent(this.syncToolCall, model); + return new WaterFlowAgent(this.syncToolCall, model, this.mcpClientFactory); } private ChatModel buildChatStreamModel(String exceptionMsg) { From 5825b9a39c5ee3c2ec73f88d6db95da161e2da37 Mon Sep 17 00:00:00 2001 From: song <271667068@qq.com> Date: Tue, 15 Jul 2025 15:14:16 +0800 Subject: [PATCH 3/3] [app-builder] clean code --- .../fit/jober/aipp/fitable/LlmComponent.java | 34 ++++++++++++------- .../jober/aipp/fitable/LlmComponentTest.java | 2 -- .../fit/jober/aipp/constants/AippConst.java | 1 - 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java index 86d28862f9..3fd55273a0 100644 --- a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java +++ b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/fitable/LlmComponent.java @@ -188,7 +188,7 @@ public List> handleTask(List> flowData) StreamMsgSender streamMsgSender = new StreamMsgSender(this.aippLogStreamService, this.serializer, path, msgId, instId); streamMsgSender.sendKnowledge(promptMessage.getMetadata(), businessData); - ChatOption chatOption = buildChatOptions(businessData); + ChatOption chatOption = this.buildChatOptions(businessData); agentFlow.converse() .bind((acc, chunk) -> { if (firstTokenFlag[0]) { @@ -448,17 +448,7 @@ private List buildMcpToolInfos(Map mcpServersConfig) { McpUtils.getSseEndpoint(url))) { mcpClient.initialize(); List tools = mcpClient.getTools(); - result.addAll(tools.stream() - .map(tool -> ToolInfo.custom() - .name(buildUniqueToolName(AippConst.MCP_SERVER_TYPE, serverName, tool.getName())) - .description(tool.getDescription()) - .parameters(tool.getInputSchema()) - .extensions(MapBuilder.get() - .put(AippConst.MCP_SERVER_KEY, serverConfig) - .put(AippConst.TOOL_REAL_NAME, tool.getName()) - .build()) - .build()) - .toList()); + result.addAll(tools.stream().map(tool -> buildMcpToolInfo(serverName, tool, serverConfig)).toList()); } catch (IOException exception) { throw new AippException(AippErrCode.CALL_MCP_SERVER_FAILED, exception.getMessage()); } @@ -487,10 +477,28 @@ private ToolInfo buildToolInfo(ToolData toolData) { .build(); } + private static ToolInfo buildMcpToolInfo(String serverName, Tool tool, Map serverConfig) { + return ToolInfo.custom() + .name(buildUniqueToolName(AippConst.MCP_SERVER_TYPE, serverName, tool.getName())) + .description(tool.getDescription()) + .parameters(tool.getInputSchema()) + .extensions(MapBuilder.get() + .put(AippConst.MCP_SERVER_KEY, serverConfig) + .put(AippConst.TOOL_REAL_NAME, tool.getName()) + .build()) + .build(); + } + private static String buildUniqueToolName(String type, String serverName, String toolName) { - return String.format("%s_%s_%s", type, serverName, toolName); + return StringUtils.format("{0}_{1}_{2}", type, serverName, toolName); } + /** + * 判断是否启用日志。 + * + * @param businessData 表示业务上下文数据的 {@link Map}{@code <}{@link String}{@code , }{@link Object}{@code >}。 + * @return 表示是否启用日志的 {@code boolean}。 + */ public static boolean checkEnableLog(Map businessData) { Object value = businessData.get(AippConst.BS_LLM_ENABLE_LOG); if (value == null) { diff --git a/app-builder/jane/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java b/app-builder/jane/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java index 236fc6915a..8330819f2e 100644 --- a/app-builder/jane/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java +++ b/app-builder/jane/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/fitable/LlmComponentTest.java @@ -395,8 +395,6 @@ void shouldGetMcpToolsWhenHandleTaskGivenMcpServersConfig() throws InterruptedEx Assertions.assertEquals(mcpServerInfo, toolInfo.extensions().get(AippConst.MCP_SERVER_KEY)); } - - private LlmComponent getLlmComponent(final AbstractAgent agent) { return new LlmComponent(this.flowInstanceService, this.toolService, diff --git a/app-builder/jane/services/aipp-genericable/src/main/java/modelengine/fit/jober/aipp/constants/AippConst.java b/app-builder/jane/services/aipp-genericable/src/main/java/modelengine/fit/jober/aipp/constants/AippConst.java index 3224b16389..6442fa8853 100644 --- a/app-builder/jane/services/aipp-genericable/src/main/java/modelengine/fit/jober/aipp/constants/AippConst.java +++ b/app-builder/jane/services/aipp-genericable/src/main/java/modelengine/fit/jober/aipp/constants/AippConst.java @@ -784,7 +784,6 @@ public class AippConst { */ public static final String MCP_SERVER_URL_KEY = "url"; - /** * mcp server type */