ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/controller/api/ChatController.java
@@ -49,9 +49,6 @@ @PostMapping("/send") @ResponseBody public SseEmitter sseChat(@RequestBody @Valid ChatRequest chatRequest, HttpServletRequest request) { if (chatRequest.getModel().startsWith("ollama")) { return sseService.ollamaChat(chatRequest); } return sseService.sseChat(chatRequest,request); } ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/factory/SseServiceFactory.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,24 @@ package org.ruoyi.chat.factory; import lombok.extern.slf4j.Slf4j; import org.ruoyi.chat.service.chat.IChatService; import org.ruoyi.chat.service.chat.impl.OllamaServiceImpl; import org.ruoyi.chat.service.chat.impl.OpenAIServiceImpl; import org.springframework.stereotype.Component; @Component @Slf4j public class SseServiceFactory { public IChatService getSseService(String type) { if ("openai".equals(type)) { return new OpenAIServiceImpl(); } else if ("ollama".equals(type)) { return new OllamaServiceImpl(); } else { throw new IllegalArgumentException("Unknown type: " + type); } } } ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/factory/VectorStoreFactory.java
ÎļþÃû´Ó ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/knowledge/vectorstore/VectorStoreFactory.java ÐÞ¸Ä @@ -1,8 +1,10 @@ package org.ruoyi.chat.service.knowledge.vectorstore; package org.ruoyi.chat.factory; import cn.hutool.core.util.StrUtil; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.ruoyi.chat.service.knowledge.vectorstore.MilvusVectorStore; import org.ruoyi.chat.service.knowledge.vectorstore.WeaviateVectorStore; import org.ruoyi.domain.vo.KnowledgeInfoVo; import org.ruoyi.mapper.KnowledgeInfoMapper; import org.ruoyi.service.VectorStoreService; ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/factory/VectorizationFactory.java
ÎļþÃû´Ó ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/knowledge/vectorizer/VectorizationFactory.java ÐÞ¸Ä @@ -1,9 +1,11 @@ package org.ruoyi.chat.service.knowledge.vectorizer; package org.ruoyi.chat.factory; import cn.hutool.core.util.StrUtil; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.ruoyi.chat.service.knowledge.vectorizer.BgeLargeVectorization; import org.ruoyi.chat.service.knowledge.vectorizer.OpenAiVectorization; import org.ruoyi.domain.vo.KnowledgeInfoVo; import org.ruoyi.service.IKnowledgeInfoService; import org.ruoyi.service.VectorizationService; ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/IChatService.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,19 @@ package org.ruoyi.chat.service.chat; import org.ruoyi.common.chat.request.ChatRequest; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; /** * 对è¯Serviceæ¥å£ * * @author ageerle * @date 2025-04-08 */ public interface IChatService { /** * 客æ·ç«¯åéæ¶æ¯å°æå¡ç«¯ * @param chatRequest 请æ±å¯¹è±¡ */ SseEmitter chat(ChatRequest chatRequest,SseEmitter emitter); } ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/ISseService.java
@@ -39,9 +39,8 @@ */ ResponseEntity<Resource> textToSpeed(TextToSpeech textToSpeech); /** * ä¸ä¼ æä»¶å°apiæå¡å¨ * ä¸ä¼ æä»¶å°æå¡å¨ * * @param file æä»¶ä¿¡æ¯ * @return è¿åæä»¶ä¿¡æ¯ @@ -50,19 +49,11 @@ /** * 使ç¨ollamaè°ç¨æ¬å°æ¨¡å * @param chatRequest 对è¯ä¿¡æ¯ * @return æµå¼è¾åºè¿åå 容 */ SseEmitter ollamaChat(ChatRequest chatRequest); /** * ä¼ä¸åºç¨åå¤ * @param prompt æç¤ºè¯ * @return åå¤å 容 */ String wxCpChat(String prompt); /** * èç½æ¥è¯¢ ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OllamaServiceImpl.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,80 @@ package org.ruoyi.chat.service.chat.impl; import io.github.ollama4j.OllamaAPI; import io.github.ollama4j.models.chat.OllamaChatMessage; 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; 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.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; @Service @Slf4j public class OllamaServiceImpl implements IChatService { @Autowired private IChatModelService chatModelService; @Override public SseEmitter chat(ChatRequest chatRequest,SseEmitter emitter) { ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel()); String host = chatModelVo.getApiHost(); List<Message> msgList = chatRequest.getMessages(); List<OllamaChatMessage> messages = new ArrayList<>(); for (Message message : msgList) { OllamaChatMessage ollamaChatMessage = new OllamaChatMessage(); ollamaChatMessage.setRole(OllamaChatMessageRole.USER); ollamaChatMessage.setContent(message.getContent().toString()); messages.add(ollamaChatMessage); } OllamaAPI api = new OllamaAPI(host); api.setRequestTimeoutSeconds(100); OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(chatRequest.getModel()); OllamaChatRequestModel requestModel = builder .withMessages(messages) .build(); // 弿¥æ§è¡ OllAma API è°ç¨ CompletableFuture.runAsync(() -> { try { StringBuilder response = new StringBuilder(); OllamaStreamHandler streamHandler = (s) -> { String substr = s.substring(response.length()); response.append(substr); System.out.println(substr); try { emitter.send(substr); } catch (IOException e) { SSEUtil.sendErrorEvent(emitter, e.getMessage()); } }; api.chat(requestModel, streamHandler); emitter.complete(); } catch (Exception e) { SSEUtil.sendErrorEvent(emitter, e.getMessage()); } }); return emitter; } } ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,50 @@ package org.ruoyi.chat.service.chat.impl; import lombok.extern.slf4j.Slf4j; 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.openai.OpenAiStreamClient; import org.ruoyi.common.chat.request.ChatRequest; import org.ruoyi.domain.vo.ChatModelVo; import org.ruoyi.service.IChatModelService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @Service @Slf4j public class OpenAIServiceImpl implements IChatService { @Autowired private IChatModelService chatModelService; @Autowired private ChatConfig chatConfig; @Autowired private OpenAiStreamClient openAiStreamClient; @Override public SseEmitter chat(ChatRequest chatRequest,SseEmitter emitter) { SSEEventSourceListener openAIEventSourceListener = new SSEEventSourceListener(emitter); // æ¥è¯¢æ¨¡åä¿¡æ¯ ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel()); if(chatModelVo!=null){ // 建请æ±å®¢æ·ç«¯ openAiStreamClient = chatConfig.createOpenAiStreamClient(chatModelVo.getApiHost(), chatModelVo.getApiKey()); // 设置é»è®¤æç¤ºè¯ chatRequest.setSysPrompt(chatModelVo.getSystemPrompt()); } ChatCompletion completion = ChatCompletion .builder() .messages(chatRequest.getMessages()) .model(chatRequest.getModel()) .stream(chatRequest.getStream()) .build(); openAiStreamClient.streamChatCompletion(completion, openAIEventSourceListener); return emitter; } } ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java
@@ -6,22 +6,17 @@ import com.google.protobuf.ServiceException; import com.zhipu.oapi.ClientV4; import com.zhipu.oapi.service.v4.tools.*; import io.github.ollama4j.OllamaAPI; import io.github.ollama4j.models.chat.OllamaChatMessage; 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 okhttp3.*; import org.ruoyi.chat.config.ChatConfig; import org.ruoyi.chat.listener.SSEEventSourceListener; import org.ruoyi.chat.service.chat.IChatCostService; import org.ruoyi.chat.service.chat.IChatService; import org.ruoyi.chat.service.chat.ISseService; import org.ruoyi.chat.factory.SseServiceFactory; import org.ruoyi.chat.util.IpUtil; import org.ruoyi.chat.util.SSEUtil; import org.ruoyi.common.chat.request.ChatRequest; import org.ruoyi.common.chat.entity.Tts.TextToSpeech; import org.ruoyi.common.chat.entity.chat.ChatCompletion; @@ -32,15 +27,14 @@ import org.ruoyi.common.chat.entity.whisper.WhisperResponse; import org.ruoyi.common.chat.openai.OpenAiStreamClient; import org.ruoyi.common.core.service.ConfigService; import org.ruoyi.common.core.utils.DateUtils; import org.ruoyi.common.core.utils.StringUtils; import org.ruoyi.common.core.utils.file.FileUtils; import org.ruoyi.common.core.utils.file.MimeTypeUtils; import org.ruoyi.common.redis.utils.RedisUtils; import org.ruoyi.domain.vo.ChatModelVo; import org.ruoyi.service.EmbeddingService; import org.ruoyi.service.IChatModelService; import org.ruoyi.service.VectorStoreService; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; @@ -60,7 +54,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -72,10 +65,6 @@ private final OpenAiStreamClient openAiStreamClient; private final ChatConfig chatConfig; private final IChatModelService chatModelService; private final EmbeddingService embeddingService; private final VectorStoreService vectorStore; @@ -84,6 +73,8 @@ private final IChatCostService chatCostService; private final SseServiceFactory sseServiceFactory; private static final String requestIdTemplate = "company-%d"; private static final ObjectMapper mapper = new ObjectMapper(); @@ -91,29 +82,30 @@ @Override public SseEmitter sseChat(ChatRequest chatRequest, HttpServletRequest request) { SseEmitter sseEmitter = new SseEmitter(0L); SSEEventSourceListener openAIEventSourceListener = new SSEEventSourceListener(sseEmitter); // è·åå¯¹è¯æ¶æ¯å表 List<Message> messages = chatRequest.getMessages(); try { // æ¥è¯¢æ¨¡åä¿¡æ¯ ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel()); OpenAiStreamClient openAiModelStreamClient; if(chatModelVo!=null){ // 建请æ±å®¢æ·ç«¯ openAiModelStreamClient = chatConfig.createOpenAiStreamClient(chatModelVo.getApiHost(), chatModelVo.getApiKey()); // 设置é»è®¤æç¤ºè¯ chatRequest.setSysPrompt(chatModelVo.getSystemPrompt()); }else { // 使ç¨é»è®¤å®¢æ·ç«¯ openAiModelStreamClient = openAiStreamClient; } // æå»ºæ¶æ¯å表å¢å èç½ãç¥è¯åºçå 容 buildChatMessageList(chatRequest); // æ ¹æ®æ¨¡ååç§°åç¼è°ç¨ä¸åçå¤çé»è¾ switchModelAndHandle(chatRequest); switchModelAndHandle(chatRequest,sseEmitter); // æªç»å½ç¨æ·éå¶å¯¹è¯æ¬¡æ° checkUnauthenticatedUserChatLimit(request); // ä¿åæ¶æ¯è®°å½ å¹¶æ£é¤è´¹ç¨ chatCostService.deductToken(chatRequest); } catch (Exception e) { String message = e.getMessage(); SSEUtil.sendErrorEvent(sseEmitter, message); return sseEmitter; } return sseEmitter; } /** * æ£æ¥æªç»å½ç¨æ·æ¯å¦è¶ è¿å½æ¥å¯¹è¯æ¬¡æ°éå¶ * * @param request å½åè¯·æ± * @throws ServiceException 妿彿¥å 费次æ°å·²ç¨å® */ public void checkUnauthenticatedUserChatLimit(HttpServletRequest request) throws ServiceException { // æªç»å½ç¨æ·éå¶å¯¹è¯æ¬¡æ° if (!StpUtil.isLogin()) { String clientIp = IpUtil.getClientIp(request); @@ -121,6 +113,7 @@ int timeWindowInSeconds = 5; String redisKey = "clientIp:" + clientIp; int count = 0; // æ£æ¥Redisä¸çå¯¹è¯æ¬¡æ° if (RedisUtils.getCacheObject(redisKey) == null) { // ç¼åæææ¶é´1天 RedisUtils.setCacheObject(redisKey, count, Duration.ofSeconds(86400)); @@ -133,36 +126,20 @@ RedisUtils.setCacheObject(redisKey, count); } } ChatCompletion completion = ChatCompletion .builder() .messages(messages) .model(chatRequest.getModel()) .stream(chatRequest.getStream()) .build(); openAiModelStreamClient.streamChatCompletion(completion, openAIEventSourceListener); // ä¿åæ¶æ¯è®°å½ å¹¶æ£é¤è´¹ç¨ chatCostService.deductToken(chatRequest); } catch (Exception e) { String message = e.getMessage(); sendErrorEvent(sseEmitter, message); return sseEmitter; } return sseEmitter; } /** * æ ¹æ®æ¨¡ååç§°åç¼è°ç¨ä¸åçå¤çé»è¾ */ private void switchModelAndHandle(ChatRequest chatRequest) { private void switchModelAndHandle(ChatRequest chatRequest,SseEmitter emitter) { String model = chatRequest.getModel(); // å¦ææ¨¡åå称以ollamaå¼å¤´ï¼åè°ç¨ollamaä¸é¨ç½²çæ¬å°æ¨¡å if (model.startsWith("ollama-")) { String[] parts = chatRequest.getModel().split("ollama-", 2); // éå¶å岿¬¡æ°ä¸º2 if (parts.length > 1) { chatRequest.setModel(parts[1]); ollamaChat(chatRequest); IChatService chatService = sseServiceFactory.getSseService("ollama"); chatService.chat(chatRequest,emitter); } else { throw new IllegalArgumentException("Invalid ollama model name: " + chatRequest.getModel()); } @@ -177,8 +154,13 @@ private void buildChatMessageList(ChatRequest chatRequest){ // è·åå¯¹è¯æ¶æ¯å表 List<Message> messages = chatRequest.getMessages(); String sysPrompt = chatRequest.getSysPrompt(); if(StringUtils.isEmpty(sysPrompt)){ sysPrompt ="ä½ æ¯ä¸ä¸ªç±RuoYI-AIå¼åç人工æºè½å©æï¼ååå«çç«å©æãä½ æ é¿ä¸è±æå¯¹è¯ï¼è½å¤ç解并å¤çåç§é®é¢ï¼æä¾å®å ¨ãæå¸®å©ãåç¡®çåçã" + "å½åæ¶é´ï¼"+ DateUtils.getDate(); } // 设置系ç»é»è®¤æç¤ºè¯ Message sysMessage = Message.builder().content(chatRequest.getSysPrompt()).role(Message.Role.SYSTEM).build(); Message sysMessage = Message.builder().content(sysPrompt).role(Message.Role.SYSTEM).build(); messages.add(0,sysMessage); // æ¥è¯¢åéåºç¸å ³ä¿¡æ¯å å ¥å°ä¸ä¸æ @@ -216,23 +198,6 @@ } } /** * åéSSEé误äºä»¶çå°è£ æ¹æ³ * * @param sseEmitter * @param errorMessage */ private void sendErrorEvent(SseEmitter sseEmitter, String errorMessage) { SseEmitter.SseEventBuilder event = SseEmitter.event() .name("error") .data(errorMessage); try { sseEmitter.send(event); } catch (IOException e) { log.error("SSEåé失败: {}", e.getMessage()); } sseEmitter.complete(); } /** * æå转è¯é³ @@ -323,51 +288,6 @@ return file; } @Override public SseEmitter ollamaChat(ChatRequest chatRequest) { ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel()); final SseEmitter emitter = new SseEmitter(); String host = chatModelVo.getApiHost(); List<Message> msgList = chatRequest.getMessages(); List<OllamaChatMessage> messages = new ArrayList<>(); for (Message message : msgList) { OllamaChatMessage ollamaChatMessage = new OllamaChatMessage(); ollamaChatMessage.setRole(OllamaChatMessageRole.USER); ollamaChatMessage.setContent(message.getContent().toString()); messages.add(ollamaChatMessage); } OllamaAPI api = new OllamaAPI(host); api.setRequestTimeoutSeconds(100); OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance(chatRequest.getModel()); OllamaChatRequestModel requestModel = builder .withMessages(messages) .build(); // 弿¥æ§è¡ OllAma API è°ç¨ CompletableFuture.runAsync(() -> { try { StringBuilder response = new StringBuilder(); OllamaStreamHandler streamHandler = (s) -> { String substr = s.substring(response.length()); response.append(substr); System.out.println(substr); try { emitter.send(substr); } catch (IOException e) { sendErrorEvent(emitter, e.getMessage()); } }; api.chat(requestModel, streamHandler); emitter.complete(); } catch (Exception e) { sendErrorEvent(emitter, e.getMessage()); } }); return emitter; } @Override public String wxCpChat(String prompt) { ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/util/SSEUtil.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,33 @@ package org.ruoyi.chat.util; import lombok.extern.slf4j.Slf4j; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; /** * sseå·¥å ·ç±» * * @author WangLe */ @Slf4j public class SSEUtil { /** * åéSSEé误äºä»¶çå°è£ æ¹æ³ * * @param sseEmitter sseäºä»¶å¯¹è±¡ * @param errorMessage éè¯¯ä¿¡æ¯ */ public static void sendErrorEvent(SseEmitter sseEmitter, String errorMessage) { SseEmitter.SseEventBuilder event = SseEmitter.event() .name("error") .data(errorMessage); try { sseEmitter.send(event); } catch (IOException e) { log.error("SSEåé失败: {}", e.getMessage()); } sseEmitter.complete(); } }