diff --git a/ruoyi-modules-api/ruoyi-chat-api/pom.xml b/ruoyi-modules-api/ruoyi-chat-api/pom.xml index 633977bc..72fd0d2a 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/pom.xml +++ b/ruoyi-modules-api/ruoyi-chat-api/pom.xml @@ -18,12 +18,44 @@ UTF-8 + + + + org.springframework.ai + spring-ai-bom + 1.0.0-M6 + pom + import + + + + org.ruoyi ruoyi-common-chat + + + org.springframework.ai + spring-ai-mcp-client-spring-boot-starter + + + + org.springframework.ai + spring-ai-mcp + + + + org.springframework.ai + spring-ai-openai-spring-boot-starter + + + + org.springframework.ai + spring-ai-ollama-spring-boot-starter + diff --git a/ruoyi-modules/ruoyi-chat/pom.xml b/ruoyi-modules/ruoyi-chat/pom.xml index 2a14fb14..5749f4e8 100644 --- a/ruoyi-modules/ruoyi-chat/pom.xml +++ b/ruoyi-modules/ruoyi-chat/pom.xml @@ -114,6 +114,20 @@ ruoyi-system-api + + org.springframework.ai + spring-ai-core + 1.0.0-M6 + compile + + + + org.springframework.ai + spring-ai-ollama + 1.0.0-M6 + compile + + diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/IChatService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/IChatService.java index 3a6c0aac..1fbadd4b 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/IChatService.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/IChatService.java @@ -16,4 +16,11 @@ public interface IChatService { * @param chatRequest 请求对象 */ SseEmitter chat(ChatRequest chatRequest,SseEmitter emitter); + + + /** + * 客户端发送消息到服务端 + * @param chatRequest 请求对象 + */ + SseEmitter mcpChat(ChatRequest chatRequest,SseEmitter emitter); } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OllamaServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OllamaServiceImpl.java index 7537e0b1..8b337b24 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OllamaServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OllamaServiceImpl.java @@ -6,8 +6,6 @@ import io.github.ollama4j.models.chat.OllamaChatMessageRole; import io.github.ollama4j.models.chat.OllamaChatRequestBuilder; import io.github.ollama4j.models.chat.OllamaChatRequestModel; import io.github.ollama4j.models.generate.OllamaStreamHandler; -import jakarta.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.ruoyi.chat.service.chat.IChatService; import org.ruoyi.chat.util.SSEUtil; @@ -15,7 +13,16 @@ import org.ruoyi.common.chat.entity.chat.Message; import org.ruoyi.common.chat.request.ChatRequest; import org.ruoyi.domain.vo.ChatModelVo; import org.ruoyi.service.IChatModelService; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; +import org.springframework.ai.chat.memory.ChatMemory; +import org.springframework.ai.chat.memory.InMemoryChatMemory; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.ollama.api.OllamaModel; +import org.springframework.ai.ollama.api.OllamaOptions; +import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @@ -32,6 +39,13 @@ public class OllamaServiceImpl implements IChatService { @Autowired private IChatModelService chatModelService; + @Autowired + private ChatClient chatClient; + @Autowired + private ToolCallbackProvider tools; + + private final ChatMemory chatMemory = new InMemoryChatMemory(); + @Override public SseEmitter chat(ChatRequest chatRequest,SseEmitter emitter) { @@ -77,4 +91,50 @@ public class OllamaServiceImpl implements IChatService { return emitter; } + + @Override + public SseEmitter mcpChat(ChatRequest chatRequest, SseEmitter emitter) { + List msgList = chatRequest.getMessages(); + // 添加记忆 + for (int i = 0; i < msgList.size(); i++) { + org.springframework.ai.chat.messages.Message springAiMessage = new UserMessage(msgList.get(i).getContent().toString()); + chatMemory.add(String.valueOf(i),springAiMessage); + } + var messageChatMemoryAdvisor = new MessageChatMemoryAdvisor(chatMemory, chatRequest.getUserId(), 10); + + this.chatClient.prompt(chatRequest.getPrompt()) + .advisors(messageChatMemoryAdvisor) + .tools(tools) + .options(OllamaOptions.builder() + .model(OllamaModel.QWEN_2_5_7B) + .temperature(0.4) + .build()) + .stream() + .chatResponse() + .subscribe( + chatResponse -> { + try { + emitter.send(chatResponse, MediaType.APPLICATION_JSON); + } catch (IOException e) { + e.printStackTrace(); + } + }, + error -> { + try { + emitter.completeWithError(error); + } catch (Exception e) { + e.printStackTrace(); + } + }, + () -> { + try { + emitter.complete(); + } catch (Exception e) { + e.printStackTrace(); + } + } + ); + + return emitter; + } } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java index b9d2012d..ebf37e13 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java @@ -5,14 +5,29 @@ import org.ruoyi.chat.config.ChatConfig; import org.ruoyi.chat.listener.SSEEventSourceListener; import org.ruoyi.chat.service.chat.IChatService; import org.ruoyi.common.chat.entity.chat.ChatCompletion; +import org.ruoyi.common.chat.entity.chat.Message; import org.ruoyi.common.chat.openai.OpenAiStreamClient; import org.ruoyi.common.chat.request.ChatRequest; import org.ruoyi.domain.vo.ChatModelVo; import org.ruoyi.service.IChatModelService; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; +import org.springframework.ai.chat.memory.ChatMemory; +import org.springframework.ai.chat.memory.InMemoryChatMemory; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import java.io.IOException; +import java.util.List; + +/** + * @author ageer + */ @Service @Slf4j public class OpenAIServiceImpl implements IChatService { @@ -23,6 +38,13 @@ public class OpenAIServiceImpl implements IChatService { private ChatConfig chatConfig; @Autowired private OpenAiStreamClient openAiStreamClient; + @Autowired + private ChatClient chatClient; + + @Autowired + private ToolCallbackProvider tools; + + private final ChatMemory chatMemory = new InMemoryChatMemory(); @Override public SseEmitter chat(ChatRequest chatRequest,SseEmitter emitter) { @@ -47,4 +69,46 @@ public class OpenAIServiceImpl implements IChatService { return emitter; } + + @Override + public SseEmitter mcpChat(ChatRequest chatRequest, SseEmitter emitter) { + List msgList = chatRequest.getMessages(); + // 添加记忆 + for (int i = 0; i < msgList.size(); i++) { + org.springframework.ai.chat.messages.Message springAiMessage = new UserMessage(msgList.get(i).getContent().toString()); + chatMemory.add(String.valueOf(i),springAiMessage); + } + var messageChatMemoryAdvisor = new MessageChatMemoryAdvisor(chatMemory, chatRequest.getUserId(), 10); + this.chatClient.prompt(chatRequest.getPrompt()) + .advisors(messageChatMemoryAdvisor) + .tools(tools) + .options(OpenAiChatOptions.builder().model(chatRequest.getModel()).build()) + .stream() + .chatResponse() + .subscribe( + chatResponse -> { + try { + emitter.send(chatResponse, MediaType.APPLICATION_JSON); + } catch (IOException e) { + e.printStackTrace(); + } + }, + error -> { + try { + emitter.completeWithError(error); + } catch (Exception e) { + e.printStackTrace(); + } + }, + () -> { + try { + emitter.complete(); + } catch (Exception e) { + e.printStackTrace(); + } + } + ); + + return emitter; + } } diff --git a/ruoyi-modules/ruoyi-mcp-server/pom.xml b/ruoyi-modules/ruoyi-mcp-server/pom.xml new file mode 100644 index 00000000..113e41c1 --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp-server/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + + org.ruoyi + ruoyi-modules + ${revision} + ../pom.xml + + + ruoyi-mcp-server + + + 17 + 17 + UTF-8 + + + + + + org.springframework.ai + spring-ai-bom + 1.0.0-M6 + pom + import + + + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.ai + spring-ai-mcp-server-webmvc-spring-boot-starter + + + + org.springframework.ai + spring-ai-mcp + + + + + + diff --git a/ruoyi-modules/ruoyi-mcp-server/src/main/java/org/ruoyi/mcp/McpServerApplication.java b/ruoyi-modules/ruoyi-mcp-server/src/main/java/org/ruoyi/mcp/McpServerApplication.java new file mode 100644 index 00000000..bf6b0ff1 --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp-server/src/main/java/org/ruoyi/mcp/McpServerApplication.java @@ -0,0 +1,16 @@ +package org.ruoyi.mcp; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author ageer + */ +@SpringBootApplication +public class McpServerApplication { + + public static void main(String[] args) { + SpringApplication.run(McpServerApplication.class, args); + } + +} diff --git a/ruoyi-modules/ruoyi-mcp-server/src/main/java/org/ruoyi/mcp/config/McpServerConfig.java b/ruoyi-modules/ruoyi-mcp-server/src/main/java/org/ruoyi/mcp/config/McpServerConfig.java new file mode 100644 index 00000000..4ba45e10 --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp-server/src/main/java/org/ruoyi/mcp/config/McpServerConfig.java @@ -0,0 +1,100 @@ +package org.ruoyi.mcp.config; + +import org.ruoyi.mcp.service.McpCustomService; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.server.McpServerFeatures; +import io.modelcontextprotocol.spec.McpSchema; +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.ai.tool.method.MethodToolCallbackProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + + +/** + * @author ageer + */ +@Configuration +@EnableWebMvc +public class McpServerConfig implements WebMvcConfigurer { + + @Bean + public ToolCallbackProvider openLibraryTools(McpCustomService mcpService) { + return MethodToolCallbackProvider.builder().toolObjects(mcpService).build(); + } + + @Bean + public List resourceRegistrations() { + + // Create a resource registration for system information + var systemInfoResource = new McpSchema.Resource( + "system://info", + "System Information", + "Provides basic system information including Java version, OS, etc.", + "application/json", null + ); + + var resourceRegistration = new McpServerFeatures.SyncResourceRegistration(systemInfoResource, (request) -> { + try { + var systemInfo = Map.of( + "javaVersion", System.getProperty("java.version"), + "osName", System.getProperty("os.name"), + "osVersion", System.getProperty("os.version"), + "osArch", System.getProperty("os.arch"), + "processors", Runtime.getRuntime().availableProcessors(), + "timestamp", System.currentTimeMillis()); + + String jsonContent = new ObjectMapper().writeValueAsString(systemInfo); + + return new McpSchema.ReadResourceResult( + List.of(new McpSchema.TextResourceContents(request.uri(), "application/json", jsonContent))); + } + catch (Exception e) { + throw new RuntimeException("Failed to generate system info", e); + } + }); + + return List.of(resourceRegistration); + } + + + + @Bean + public List promptRegistrations() { + + var prompt = new McpSchema.Prompt("greeting", "A friendly greeting prompt", + List.of(new McpSchema.PromptArgument("name", "The name to greet", true))); + + var promptRegistration = new McpServerFeatures.SyncPromptRegistration(prompt, getPromptRequest -> { + + String nameArgument = (String) getPromptRequest.arguments().get("name"); + if (nameArgument == null) { + nameArgument = "friend"; + } + + var userMessage = new McpSchema.PromptMessage(McpSchema.Role.USER, + new McpSchema.TextContent("Hello " + nameArgument + "! How can I assist you today?")); + + return new McpSchema.GetPromptResult("A personalized greeting message", List.of(userMessage)); + }); + + return List.of(promptRegistration); + } + + + @Bean + public Consumer> rootsChangeConsumer() { + return roots -> { + System.out.println("rootsChange"); + }; + } + + + + +} diff --git a/ruoyi-modules/ruoyi-mcp-server/src/main/java/org/ruoyi/mcp/service/McpCustomService.java b/ruoyi-modules/ruoyi-mcp-server/src/main/java/org/ruoyi/mcp/service/McpCustomService.java new file mode 100644 index 00000000..f5f34210 --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp-server/src/main/java/org/ruoyi/mcp/service/McpCustomService.java @@ -0,0 +1,24 @@ +package org.ruoyi.mcp.service; + +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @author ageer + */ +@Service +public class McpCustomService { + + + public record Book(List isbn, String title, List authorName) { + } + + @Tool(description = "Get list of Books by title") + public List getBooks(String title) { + // 这里模拟查询DB操作 + return List.of(new Book(List.of("ISBN-888"), "SpringAI教程", List.of("熊猫助手写的书"))); + } + +} diff --git a/ruoyi-modules/ruoyi-mcp-server/src/main/resources/application.yaml b/ruoyi-modules/ruoyi-mcp-server/src/main/resources/application.yaml new file mode 100644 index 00000000..57f9979d --- /dev/null +++ b/ruoyi-modules/ruoyi-mcp-server/src/main/resources/application.yaml @@ -0,0 +1,10 @@ +spring: + application: + name: mcp-server + ai: + mcp: + server: + name: webmvc-mcp-server + version: 1.0.0 + type: SYNC + sse-message-endpoint: /mcp/messages diff --git a/ruoyi-modules/ruoyi-system/src/main/resources/application.yml b/ruoyi-modules/ruoyi-system/src/main/resources/application.yml index 26173564..a414d371 100644 --- a/ruoyi-modules/ruoyi-system/src/main/resources/application.yml +++ b/ruoyi-modules/ruoyi-system/src/main/resources/application.yml @@ -318,5 +318,22 @@ wechat: token: '' aesKey: '' - - + # spring ai配置 +spring: + ai: + openai: + api-key: sk-xx + base-url: https://api.pandarobot.chat/ + mcp: + client: + enabled: true + name: call-mcp-server + sse: + connections: + server1: + url: http://127.0.0.1:8080 + ollama: + init: + pull-model-strategy: always + timeout: 60s + max-retries: 1