ageer преди 1 месец
родител
ревизия
9b32e3b3b2

+ 32 - 0
ruoyi-modules-api/ruoyi-chat-api/pom.xml

@@ -18,12 +18,44 @@
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     </properties>
 
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.springframework.ai</groupId>
+                <artifactId>spring-ai-bom</artifactId>
+                <version>1.0.0-M6</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
     <!-- 对话基础模块 -->
     <dependencies>
         <dependency>
             <groupId>org.ruoyi</groupId>
             <artifactId>ruoyi-common-chat</artifactId>
         </dependency>
+
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-mcp</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
+        </dependency>
     </dependencies>
 
 </project>

+ 14 - 0
ruoyi-modules/ruoyi-chat/pom.xml

@@ -114,6 +114,20 @@
             <artifactId>ruoyi-system-api</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-core</artifactId>
+            <version>1.0.0-M6</version>
+            <scope>compile</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-ollama</artifactId>
+            <version>1.0.0-M6</version>
+            <scope>compile</scope>
+        </dependency>
+
     </dependencies>
 
 </project>

+ 7 - 0
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);
 }

+ 62 - 2
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<Message> 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;
+    }
 }

+ 64 - 0
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<Message> 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;
+    }
 }

+ 63 - 0
ruoyi-modules/ruoyi-mcp-server/pom.xml

@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.ruoyi</groupId>
+        <artifactId>ruoyi-modules</artifactId>
+        <version>${revision}</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>ruoyi-mcp-server</artifactId>
+
+    <properties>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.springframework.ai</groupId>
+                <artifactId>spring-ai-bom</artifactId>
+                <version>1.0.0-M6</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-mcp-server-webmvc-spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-mcp</artifactId>
+        </dependency>
+
+    </dependencies>
+
+
+</project>

+ 16 - 0
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);
+    }
+
+}

+ 100 - 0
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<McpServerFeatures.SyncResourceRegistration> 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<McpServerFeatures.SyncPromptRegistration> 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<List<McpSchema.Root>> rootsChangeConsumer() {
+        return roots -> {
+            System.out.println("rootsChange");
+        };
+    }
+
+
+
+
+}

+ 24 - 0
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<String> isbn, String title, List<String> authorName) {
+	}
+
+	@Tool(description = "Get list of Books by title")
+	public List<Book> getBooks(String title) {
+		// 这里模拟查询DB操作
+		return List.of(new Book(List.of("ISBN-888"), "SpringAI教程", List.of("熊猫助手写的书")));
+	}
+
+}

+ 10 - 0
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

+ 19 - 2
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