mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-03-20 16:13:44 +08:00
refactor: 重构聊天模块架构
- 删除废弃的ChatMessageDTO、ChatContext、AbstractChatMessageService等类 - 迁移ChatServiceFactory和IChatMessageService到ruoyi-chat模块 - 重构ChatHandler体系,移除DefaultChatHandler和ChatContextBuilder - 优化SSE消息处理,新增SseEventDto - 简化各AI服务提供商实现类代码 - 优化工作流节点消息处理逻辑 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -58,7 +58,7 @@ spring:
|
||||
driverClassName: com.mysql.cj.jdbc.Driver
|
||||
# jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
|
||||
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
|
||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-ai-agent?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
username: root
|
||||
password: root
|
||||
# agent:
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
package org.ruoyi.common.chat.domain.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 聊天消息DTO - 用于上下文传递
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/12/13
|
||||
*/
|
||||
@Data
|
||||
public class ChatMessageDTO {
|
||||
|
||||
/**
|
||||
* 消息角色: system/user/assistant
|
||||
*/
|
||||
private String role;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
public static ChatMessageDTO system(String content) {
|
||||
ChatMessageDTO msg = new ChatMessageDTO();
|
||||
msg.role = "system";
|
||||
msg.content = content;
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ChatMessageDTO user(String content) {
|
||||
ChatMessageDTO msg = new ChatMessageDTO();
|
||||
msg.role = "user";
|
||||
msg.content = content;
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ChatMessageDTO assistant(String content) {
|
||||
ChatMessageDTO msg = new ChatMessageDTO();
|
||||
msg.role = "assistant";
|
||||
msg.content = content;
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.ruoyi.common.chat.domain.dto.request;
|
||||
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import com.alibaba.fastjson.annotation.JSONField;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
import org.ruoyi.common.chat.domain.dto.ChatMessageDTO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 对话请求对象
|
||||
@@ -16,11 +16,15 @@ import java.util.List;
|
||||
@Data
|
||||
public class ChatRequest {
|
||||
|
||||
@NotEmpty(message = "对话消息不能为空")
|
||||
private List<ChatMessageDTO> messages;
|
||||
@NotEmpty(message = "传入的模型不能为空")
|
||||
private String model;
|
||||
|
||||
/**
|
||||
* 对话消息
|
||||
*/
|
||||
@NotEmpty(message = "对话消息不能为空")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 工作流请求体
|
||||
*/
|
||||
@@ -31,59 +35,49 @@ public class ChatRequest {
|
||||
*/
|
||||
private ReSumeRunner reSumeRunner;
|
||||
|
||||
/**
|
||||
* 是否为人机交互用户继续输入
|
||||
*/
|
||||
private Boolean isResume = false;
|
||||
|
||||
/**
|
||||
* 是否启用工作流
|
||||
*/
|
||||
private Boolean enableWorkFlow;
|
||||
private Boolean enableWorkFlow = false;
|
||||
|
||||
/**
|
||||
* 会话id
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
@JSONField(serializeUsing = String.class)
|
||||
private Long sessionId;
|
||||
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
private String appId;
|
||||
|
||||
/**
|
||||
* 知识库id
|
||||
*/
|
||||
private String knowledgeId;
|
||||
|
||||
/**
|
||||
* 对话id(每个聊天窗口都不一样)
|
||||
* 应用ID
|
||||
*/
|
||||
private Long uuid;
|
||||
private String appId;
|
||||
|
||||
|
||||
/**
|
||||
* 是否为人机交互用户继续输入
|
||||
* 对话id(每个聊天窗口都不一样)
|
||||
*/
|
||||
private Boolean isResume;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
@JSONField(serializeUsing = String.class)
|
||||
private Long uuid;
|
||||
|
||||
/**
|
||||
* 是否启用深度思考
|
||||
*/
|
||||
private Boolean enableThinking;
|
||||
|
||||
/**
|
||||
* 是否自动切换模型
|
||||
*/
|
||||
private Boolean autoSelectModel;
|
||||
private Boolean enableThinking = false;
|
||||
|
||||
/**
|
||||
* 是否支持联网
|
||||
*/
|
||||
private Boolean enableInternet;
|
||||
|
||||
/**
|
||||
* 会话令牌(为避免在非Web线程中获取Request,入口处注入)
|
||||
*/
|
||||
private String token;
|
||||
|
||||
/**
|
||||
* 原生对话对象
|
||||
*/
|
||||
private List<ChatMessage> chatMessages;
|
||||
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import cn.idev.excel.annotation.ExcelProperty;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import org.ruoyi.common.chat.entity.chat.ChatMessage;
|
||||
import org.ruoyi.common.excel.annotation.ExcelDictFormat;
|
||||
import org.ruoyi.common.excel.convert.ExcelDictConvert;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
@@ -6,12 +6,14 @@ import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class BaseEntity implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
package org.ruoyi.common.chat.entity.chat;
|
||||
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.common.chat.service.chat.IChatService;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
/**
|
||||
* 聊天对话上下文对象
|
||||
*
|
||||
* @author zengxb
|
||||
* @date 2026-02-14
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Builder
|
||||
public class ChatContext {
|
||||
|
||||
/**
|
||||
* 模型管理视图对象
|
||||
*/
|
||||
@NotNull(message = "模型管理视图对象不能为空")
|
||||
private ChatModelVo chatModelVo;
|
||||
|
||||
/**
|
||||
* 对话请求对象
|
||||
*/
|
||||
@NotNull(message = "对话请求对象不能为空")
|
||||
private ChatRequest chatRequest;
|
||||
|
||||
/**
|
||||
* SSe连接对象
|
||||
*/
|
||||
@NotNull(message = "SSe连接对象不能为空")
|
||||
private SseEmitter emitter;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@NotNull(message = "用户ID不能为空")
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* Token
|
||||
*/
|
||||
@NotNull(message = "Token不能为空")
|
||||
private String tokenValue;
|
||||
|
||||
/**
|
||||
* 响应处理器
|
||||
*/
|
||||
private StreamingChatResponseHandler handler;
|
||||
|
||||
/**
|
||||
* 聊天服务实例
|
||||
*/
|
||||
private IChatService chatService;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
package org.ruoyi.common.chat.service.chat;
|
||||
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import jakarta.validation.Valid;
|
||||
import org.ruoyi.common.chat.entity.chat.ChatContext;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
/**
|
||||
@@ -12,10 +13,15 @@ public interface IChatService {
|
||||
/**
|
||||
* 客户端发送对话消息到服务端
|
||||
*/
|
||||
SseEmitter chat(@Valid ChatContext chatContext);
|
||||
SseEmitter chat(@Valid ChatRequest chatRequest);
|
||||
|
||||
/**
|
||||
* 获取服务提供商名称
|
||||
* 支持外部 handler 的对话接口(跨模块调用)
|
||||
* 同时发送到 SSE 和外部 handler
|
||||
*
|
||||
* @param chatRequest 聊天请求
|
||||
* @param externalHandler 外部响应处理器(可为 null)
|
||||
*/
|
||||
String getProviderName();
|
||||
void chat(@Valid ChatRequest chatRequest, StreamingChatResponseHandler externalHandler);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
package org.ruoyi.common.chat.service.chatMessage;
|
||||
|
||||
import org.ruoyi.common.chat.domain.bo.chat.ChatMessageBo;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
/**
|
||||
* 聊天信息抽象基类 - 保存聊天信息
|
||||
*
|
||||
* @author Zengxb
|
||||
* @date 2026-02-24
|
||||
*/
|
||||
public abstract class AbstractChatMessageService {
|
||||
|
||||
/**
|
||||
* 创建日志对象
|
||||
*/
|
||||
Logger log = LoggerFactory.getLogger(AbstractChatMessageService.class);
|
||||
|
||||
@Autowired
|
||||
private IChatMessageService chatMessageService;
|
||||
|
||||
/**
|
||||
* 保存聊天信息
|
||||
*/
|
||||
public void saveChatMessage(ChatRequest chatRequest, Long userId, String content, String role, ChatModelVo chatModelVo){
|
||||
try {
|
||||
// 验证必要的上下文信息
|
||||
if (chatRequest == null || userId == null) {
|
||||
log.warn("缺少必要的聊天上下文信息,无法保存消息");
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建ChatMessageBo对象
|
||||
ChatMessageBo messageBO = new ChatMessageBo();
|
||||
messageBO.setUserId(userId);
|
||||
messageBO.setSessionId(chatRequest.getSessionId());
|
||||
messageBO.setContent(content);
|
||||
messageBO.setRole(role);
|
||||
messageBO.setModelName(chatRequest.getModel());
|
||||
messageBO.setRemark(null);
|
||||
|
||||
chatMessageService.insertByBo(messageBO);
|
||||
} catch (Exception e) {
|
||||
log.error("保存{}聊天消息时出错: {}", getProviderName(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务提供商名称
|
||||
*/
|
||||
protected String getProviderName(){
|
||||
return "默认工作流大模型";
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,11 @@ package org.ruoyi.common.sse.core;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.core.utils.SpringUtils;
|
||||
import org.ruoyi.common.redis.utils.RedisUtils;
|
||||
import org.ruoyi.common.sse.dto.SseEventDto;
|
||||
import org.ruoyi.common.sse.dto.SseMessageDto;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
@@ -65,7 +67,7 @@ public class SseEmitterManager {
|
||||
emitter.onCompletion(() -> {
|
||||
SseEmitter remove = emitters.remove(token);
|
||||
if (remove != null) {
|
||||
// remove.complete();
|
||||
remove.complete();
|
||||
}
|
||||
});
|
||||
emitter.onTimeout(() -> {
|
||||
@@ -174,9 +176,11 @@ public class SseEmitterManager {
|
||||
if (MapUtil.isNotEmpty(emitters)) {
|
||||
for (Map.Entry<String, SseEmitter> entry : emitters.entrySet()) {
|
||||
try {
|
||||
// 格式化为标准SSE JSON格式
|
||||
SseEventDto eventDto = SseEventDto.content(message);
|
||||
entry.getValue().send(SseEmitter.event()
|
||||
.name("message")
|
||||
.data(message));
|
||||
.data(JSONUtil.toJsonStr(eventDto)));
|
||||
} catch (Exception e) {
|
||||
SseEmitter remove = emitters.remove(entry.getKey());
|
||||
if (remove != null) {
|
||||
@@ -189,6 +193,33 @@ public class SseEmitterManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定的用户会话发送结构化事件
|
||||
*
|
||||
* @param userId 要发送消息的用户id
|
||||
* @param eventDto SSE事件对象
|
||||
*/
|
||||
public void sendEvent(Long userId, SseEventDto eventDto) {
|
||||
Map<String, SseEmitter> emitters = USER_TOKEN_EMITTERS.get(userId);
|
||||
if (MapUtil.isNotEmpty(emitters)) {
|
||||
for (Map.Entry<String, SseEmitter> entry : emitters.entrySet()) {
|
||||
try {
|
||||
entry.getValue().send(SseEmitter.event()
|
||||
.name(eventDto.getEvent())
|
||||
.data(JSONUtil.toJsonStr(eventDto)));
|
||||
} catch (Exception e) {
|
||||
SseEmitter remove = emitters.remove(entry.getKey());
|
||||
if (remove != null) {
|
||||
remove.complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
USER_TOKEN_EMITTERS.remove(userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 本机全用户会话发送消息
|
||||
*
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package org.ruoyi.common.sse.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* SSE 事件数据传输对象
|
||||
* <p>
|
||||
* 标准的 SSE 消息格式,支持不同事件类型
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/03/19
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SseEventDto implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 事件类型
|
||||
*/
|
||||
private String event;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 推理内容(深度思考模式)
|
||||
*/
|
||||
private String reasoningContent;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
private String error;
|
||||
|
||||
/**
|
||||
* 是否完成
|
||||
*/
|
||||
private Boolean done;
|
||||
|
||||
/**
|
||||
* 创建内容事件
|
||||
*/
|
||||
public static SseEventDto content(String content) {
|
||||
return SseEventDto.builder()
|
||||
.event("content")
|
||||
.content(content)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建推理内容事件
|
||||
*/
|
||||
public static SseEventDto reasoning(String reasoningContent) {
|
||||
return SseEventDto.builder()
|
||||
.event("reasoning")
|
||||
.reasoningContent(reasoningContent)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建完成事件
|
||||
*/
|
||||
public static SseEventDto done() {
|
||||
return SseEventDto.builder()
|
||||
.event("done")
|
||||
.done(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建错误事件
|
||||
*/
|
||||
public static SseEventDto error(String error) {
|
||||
return SseEventDto.builder()
|
||||
.event("error")
|
||||
.error(error)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package org.ruoyi.common.sse.utils;
|
||||
|
||||
import java.util.Collections;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.core.utils.SpringUtils;
|
||||
import org.ruoyi.common.sse.core.SseEmitterManager;
|
||||
import org.ruoyi.common.sse.dto.SseEventDto;
|
||||
import org.ruoyi.common.sse.dto.SseMessageDto;
|
||||
|
||||
/**
|
||||
@@ -27,6 +29,7 @@ public class SseMessageUtils {
|
||||
|
||||
/**
|
||||
* 向指定的SSE会话发送消息
|
||||
* 通过 Redis Pub/Sub 广播,确保跨模块消息可达
|
||||
*
|
||||
* @param userId 要发送消息的用户id
|
||||
* @param message 要发送的消息内容
|
||||
@@ -35,7 +38,11 @@ public class SseMessageUtils {
|
||||
if (!isEnable()) {
|
||||
return;
|
||||
}
|
||||
MANAGER.sendMessage(userId, message);
|
||||
// 通过 Redis 广播,让所有模块的 SseTopicListener 接收并转发到本地 SSE 连接
|
||||
SseMessageDto dto = new SseMessageDto();
|
||||
dto.setMessage(message);
|
||||
dto.setUserIds(Collections.singletonList(userId));
|
||||
MANAGER.publishMessage(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,6 +93,58 @@ public class SseMessageUtils {
|
||||
MANAGER.disconnect(userId, tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定的SSE会话发送结构化事件
|
||||
*
|
||||
* @param userId 要发送消息的用户id
|
||||
* @param eventDto SSE事件对象
|
||||
*/
|
||||
public static void sendEvent(Long userId, SseEventDto eventDto) {
|
||||
if (!isEnable()) {
|
||||
return;
|
||||
}
|
||||
MANAGER.sendEvent(userId, eventDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送内容事件
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param content 内容
|
||||
*/
|
||||
public static void sendContent(Long userId, String content) {
|
||||
sendEvent(userId, SseEventDto.content(content));
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送推理内容事件
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param reasoningContent 推理内容
|
||||
*/
|
||||
public static void sendReasoning(Long userId, String reasoningContent) {
|
||||
sendEvent(userId, SseEventDto.reasoning(reasoningContent));
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送完成事件
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
public static void sendDone(Long userId) {
|
||||
sendEvent(userId, SseEventDto.done());
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送错误事件
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param error 错误信息
|
||||
*/
|
||||
public static void sendError(Long userId, String error) {
|
||||
sendEvent(userId, SseEventDto.error(error));
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否开启
|
||||
*/
|
||||
|
||||
@@ -64,7 +64,8 @@ public class WorkflowMessageUtil {
|
||||
ChatRequest chatRequest = new ChatRequest();
|
||||
chatRequest.setSessionId(sessionId);
|
||||
WorkflowUtil workflowUtil = SpringUtils.getBean(WorkflowUtil.class);
|
||||
workflowUtil.saveChatMessage(chatRequest, userId, message, RoleType.WORKFLOW.getName(), new ChatModelVo());
|
||||
// todo 保存消息
|
||||
//workflowUtil.saveChatMessage(chatRequest, userId, message, RoleType.WORKFLOW.getName(), new ChatModelVo());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,23 +4,18 @@ import cn.hutool.core.collection.CollStreamUtil;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.data.message.SystemMessage;
|
||||
import dev.langchain4j.data.message.UserMessage;
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bsc.langgraph4j.langchain4j.generators.StreamingChatGenerator;
|
||||
import org.bsc.langgraph4j.state.AgentState;
|
||||
import org.ruoyi.common.chat.enums.RoleType;
|
||||
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||
import org.ruoyi.common.chat.service.chat.IChatService;
|
||||
import org.ruoyi.common.chat.service.chatMessage.AbstractChatMessageService;
|
||||
import org.ruoyi.common.chat.service.image.IImageGenerationService;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.entity.chat.ChatContext;
|
||||
import org.ruoyi.common.chat.entity.image.ImageContext;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.common.chat.factory.ChatServiceFactory;
|
||||
import org.ruoyi.common.chat.factory.ImageServiceFactory;
|
||||
import org.ruoyi.workflow.base.NodeInputConfigTypeHandler;
|
||||
import org.ruoyi.workflow.entity.WorkflowNode;
|
||||
@@ -29,9 +24,7 @@ import org.ruoyi.workflow.util.JsonUtil;
|
||||
import org.ruoyi.workflow.workflow.data.NodeIOData;
|
||||
import org.ruoyi.workflow.workflow.data.NodeIODataContent;
|
||||
import org.ruoyi.workflow.workflow.def.WfNodeParamRef;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@@ -39,10 +32,7 @@ import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.DEFAULT_O
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class WorkflowUtil extends AbstractChatMessageService {
|
||||
|
||||
@Resource
|
||||
private ChatServiceFactory chatServiceFactory;
|
||||
public class WorkflowUtil{
|
||||
|
||||
@Resource
|
||||
private ImageServiceFactory imageServiceFactory;
|
||||
@@ -50,6 +40,9 @@ public class WorkflowUtil extends AbstractChatMessageService {
|
||||
@Resource
|
||||
private IChatModelService chatModelService;
|
||||
|
||||
@Resource
|
||||
private IChatService chatService;
|
||||
|
||||
public static String renderTemplate(String template, List<NodeIOData> values) {
|
||||
// 🔒 关键修复:如果 template 为 null,直接返回 null 或空字符串
|
||||
if (template == null) {
|
||||
@@ -112,54 +105,23 @@ public class WorkflowUtil extends AbstractChatMessageService {
|
||||
}
|
||||
|
||||
public void streamingInvokeLLM(WfState wfState, WfNodeState state, WorkflowNode node, String modelName,
|
||||
List<SystemMessage> systemMessage, String nodeMessageTemplate) {
|
||||
String prompt, String nodeMessageTemplate) {
|
||||
log.info("stream invoke, modelName: {}", modelName);
|
||||
|
||||
// 根据模型名称查询模型信息
|
||||
ChatModelVo chatModelVo = chatModelService.selectModelByName(modelName);
|
||||
if (chatModelVo == null) {
|
||||
throw new IllegalArgumentException("模型不存在: " + modelName);
|
||||
}
|
||||
|
||||
// 路由服务提供商
|
||||
String category = chatModelVo.getProviderCode();
|
||||
// 根据 category 获取对应的 ChatService(不使用计费代理,工作流场景单独计费)
|
||||
IChatService chatService = chatServiceFactory.getOriginalService(category);
|
||||
|
||||
// 获取用户信息和Token以及SSe连接对象(对话接口需要使用)
|
||||
Long sessionId = wfState.getSessionId();
|
||||
Long userId = wfState.getUserId();
|
||||
String tokenValue = wfState.getTokenValue();
|
||||
SseEmitter sseEmitter = wfState.getSseEmitter();
|
||||
|
||||
// 构建 ruoyi-ai 的 ChatRequest
|
||||
List<ChatMessage> chatMessages = new ArrayList<>();
|
||||
addUserMessage(node, state.getInputs(), chatMessages);
|
||||
chatMessages.addAll(systemMessage);
|
||||
|
||||
// 定义模型调用对象
|
||||
ChatRequest chatRequest = new ChatRequest();
|
||||
// 目前工作流深度思考成员变量只能写死
|
||||
chatRequest.setSessionId(sessionId);
|
||||
chatRequest.setEnableThinking(false);
|
||||
chatRequest.setModel(modelName);
|
||||
chatRequest.setChatMessages(chatMessages);
|
||||
chatRequest.setContent(prompt);
|
||||
|
||||
// 构建流式生成器
|
||||
StreamingChatGenerator<AgentState> streamingGenerator = StreamingChatGenerator.builder()
|
||||
.mapResult(response -> {
|
||||
String responseTxt = response.aiMessage().text();
|
||||
log.info("llm response:{}", responseTxt);
|
||||
|
||||
// 会话ID不为空时插入数据库
|
||||
if (sessionId != null){
|
||||
// 获取模板消息拼接信息体
|
||||
String message = nodeMessageTemplate + responseTxt;
|
||||
// 保存助手回复消息
|
||||
saveChatMessage(chatRequest, userId, message, RoleType.ASSISTANT.getName(), chatModelVo);
|
||||
log.info("{}消息结束,已保存到数据库", getProviderName());
|
||||
}
|
||||
|
||||
// 传递所有输入数据 + 添加 LLM 输出
|
||||
wfState.getNodeStateByNodeUuid(node.getUuid()).ifPresent(item -> {
|
||||
List<NodeIOData> outputs = new ArrayList<>(item.getInputs());
|
||||
@@ -174,21 +136,13 @@ public class WorkflowUtil extends AbstractChatMessageService {
|
||||
.startingState(state)
|
||||
.build();
|
||||
|
||||
// 构建流式回调响应器
|
||||
StreamingChatResponseHandler handler = streamingGenerator.handler();
|
||||
// 获取 StreamingChatGenerator 的 handler,用于处理流式响应
|
||||
StreamingChatResponseHandler workflowHandler = streamingGenerator.handler();
|
||||
|
||||
//构建聊天对话上下文参数
|
||||
ChatContext chatContext = ChatContext.builder()
|
||||
.chatModelVo(chatModelVo)
|
||||
.chatRequest(chatRequest)
|
||||
.emitter(sseEmitter)
|
||||
.userId(userId)
|
||||
.tokenValue(tokenValue)
|
||||
.handler(handler)
|
||||
.build();
|
||||
// 调用 Chat 服务,传入 workflow 的 handler
|
||||
// 消息会同时发送到 SSE(前端)和 workflowHandler(工作流处理)
|
||||
chatService.chat(chatRequest, workflowHandler);
|
||||
|
||||
// 使用工作流专用方法
|
||||
chatService.chat(chatContext);
|
||||
wfState.getNodeToStreamingGenerator().put(node.getUuid(), streamingGenerator);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,13 +46,11 @@ public class LLMAnswerNode extends AbstractWfNode {
|
||||
// 调用LLM
|
||||
WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class);
|
||||
String modelName = nodeConfigObj.getModelName();
|
||||
// 转换系统信息结构
|
||||
List<SystemMessage> systemMessage = List.of(new SystemMessage(prompt));
|
||||
// 获取节点模板提示词信息
|
||||
String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.LLM_RESPONSE.getValue());
|
||||
// 发送SSE驱动事件消息
|
||||
WorkflowMessageUtil.sendEmitterMessage(wfState.getSseEmitter(), node, nodeMessageTemplate);
|
||||
workflowUtil.streamingInvokeLLM(wfState, state, node, modelName, systemMessage, nodeMessageTemplate);
|
||||
workflowUtil.streamingInvokeLLM(wfState, state, node, modelName, prompt, nodeMessageTemplate);
|
||||
return new NodeProcessResult();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,13 +67,12 @@ public class KeywordExtractorNode extends AbstractWfNode {
|
||||
// 调用 LLM 进行关键词提取
|
||||
WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class);
|
||||
String modelName = config.getModelName();
|
||||
List<SystemMessage> systemMessage = List.of(new SystemMessage(prompt));
|
||||
// 获取节点模板提示词信息
|
||||
String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.KEYWORD_EXTRACTOR.getValue());
|
||||
// 发送SSE事件消息
|
||||
WorkflowMessageUtil.sendEmitterMessage(wfState.getSseEmitter(), node, nodeMessageTemplate);
|
||||
// 使用流式调用
|
||||
workflowUtil.streamingInvokeLLM(wfState, state, node, modelName, systemMessage, nodeMessageTemplate);
|
||||
workflowUtil.streamingInvokeLLM(wfState, state, node, modelName, prompt, nodeMessageTemplate);
|
||||
return new NodeProcessResult();
|
||||
}
|
||||
|
||||
|
||||
@@ -151,7 +151,6 @@ public class KnowledgeRetrievalNode extends AbstractWfNode {
|
||||
|
||||
// 使用WorkflowUtil调用LLM(流式)
|
||||
WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class);
|
||||
List<SystemMessage> systemMessage = List.of(new SystemMessage(prompt));
|
||||
|
||||
// 调用流式LLM
|
||||
String modelName = StringUtils.isNotBlank(config.getModelName()) ? config.getModelName() : "deepseek-chat";
|
||||
@@ -161,7 +160,7 @@ public class KnowledgeRetrievalNode extends AbstractWfNode {
|
||||
tempState,
|
||||
tempNode,
|
||||
modelName,
|
||||
systemMessage,
|
||||
prompt,
|
||||
""
|
||||
);
|
||||
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
<artifactId>ruoyi-common-chat</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-sse</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-sensitive</artifactId>
|
||||
|
||||
@@ -30,8 +30,8 @@ public class ChatController {
|
||||
*/
|
||||
@PostMapping("/send")
|
||||
@ResponseBody
|
||||
public SseEmitter sseChat(@RequestBody @Valid ChatRequest chatRequest, HttpServletRequest request) {
|
||||
return chatService.sseChat(chatRequest,request);
|
||||
public SseEmitter sseChat(@RequestBody @Valid ChatRequest chatRequest) {
|
||||
return chatService.sseChat(chatRequest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import jakarta.validation.constraints.*;
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import org.ruoyi.common.chat.domain.bo.chat.ChatMessageBo;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatMessageVo;
|
||||
import org.ruoyi.common.chat.service.chatMessage.IChatMessageService;
|
||||
import org.ruoyi.service.chat.IChatMessageService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.ruoyi.common.idempotent.annotation.RepeatSubmit;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.ruoyi.common.chat.factory;
|
||||
package org.ruoyi.factory;
|
||||
|
||||
import org.ruoyi.common.chat.service.chat.IChatService;
|
||||
import org.ruoyi.service.chat.AbstractChatService;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
@@ -18,13 +19,13 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
@Component
|
||||
public class ChatServiceFactory implements ApplicationContextAware {
|
||||
|
||||
private final Map<String, IChatService> chatServiceMap = new ConcurrentHashMap<>();
|
||||
private final Map<String, AbstractChatService> chatServiceMap = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
// 初始化时收集所有IChatService的实现
|
||||
Map<String, IChatService> serviceMap = applicationContext.getBeansOfType(IChatService.class);
|
||||
for (IChatService service : serviceMap.values()) {
|
||||
Map<String, AbstractChatService> serviceMap = applicationContext.getBeansOfType(AbstractChatService.class);
|
||||
for (AbstractChatService service : serviceMap.values()) {
|
||||
if (service != null ) {
|
||||
chatServiceMap.put(service.getProviderName(), service);
|
||||
}
|
||||
@@ -35,8 +36,8 @@ public class ChatServiceFactory implements ApplicationContextAware {
|
||||
/**
|
||||
* 获取原始服务(不包装代理)
|
||||
*/
|
||||
public IChatService getOriginalService(String category) {
|
||||
IChatService service = chatServiceMap.get(category);
|
||||
public AbstractChatService getOriginalService(String category) {
|
||||
AbstractChatService service = chatServiceMap.get(category);
|
||||
if (service == null) {
|
||||
throw new IllegalArgumentException("不支持的模型类别: " + category);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.ruoyi.service.chat;
|
||||
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
|
||||
/**
|
||||
* 聊天消息Service接口
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-14
|
||||
*/
|
||||
public interface AbstractChatService {
|
||||
|
||||
/**
|
||||
* 创建流式聊天模型
|
||||
*
|
||||
* @param chatModelVo 模型配置
|
||||
* @param chatRequest 聊天请求
|
||||
* @return 流式聊天模型实例
|
||||
*/
|
||||
StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest);
|
||||
|
||||
/**
|
||||
* 获取服务提供商名称
|
||||
*/
|
||||
String getProviderName();
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.ruoyi.common.chat.service.chatMessage;
|
||||
package org.ruoyi.service.chat;
|
||||
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import org.ruoyi.common.chat.domain.bo.chat.ChatMessageBo;
|
||||
import org.ruoyi.common.chat.domain.dto.ChatMessageDTO;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatMessageVo;
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
@@ -74,7 +74,7 @@ public interface IChatMessageService {
|
||||
* @param sessionId 会话ID
|
||||
* @return 消息DTO列表
|
||||
*/
|
||||
List<ChatMessageDTO> getMessagesBySessionId(Long sessionId);
|
||||
List<ChatMessage> getMessagesBySessionId(Long sessionId);
|
||||
|
||||
/**
|
||||
* 根据会话ID删除所有消息
|
||||
@@ -84,4 +84,15 @@ public interface IChatMessageService {
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
Boolean deleteBySessionId(Long sessionId);
|
||||
|
||||
/**
|
||||
* 保存聊天消息
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param sessionId 会话ID
|
||||
* @param content 消息内容
|
||||
* @param role 角色类型
|
||||
* @param modelName 模型名称
|
||||
*/
|
||||
void saveChatMessage(Long userId, Long sessionId, String content, String role, String modelName);
|
||||
}
|
||||
@@ -1,158 +1,132 @@
|
||||
package org.ruoyi.service.chat.handler;
|
||||
|
||||
import dev.langchain4j.agentic.AgenticServices;
|
||||
import dev.langchain4j.community.model.dashscope.QwenChatModel;
|
||||
import dev.langchain4j.service.tool.ToolProvider;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.agent.McpAgent;
|
||||
import org.ruoyi.common.chat.domain.bo.chat.ChatMessageBo;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.common.chat.entity.chat.ChatContext;
|
||||
import org.ruoyi.common.chat.enums.RoleType;
|
||||
import org.ruoyi.common.chat.service.chatMessage.IChatMessageService;
|
||||
import org.ruoyi.common.sse.utils.SseMessageUtils;
|
||||
import org.ruoyi.mcp.service.core.ToolProviderFactory;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Agent 深度思考处理器
|
||||
* <p>
|
||||
* 处理 enableThinking=true 的场景,使用 Agent 进行深度思考和工具调用
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/12/13
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(3)
|
||||
@RequiredArgsConstructor
|
||||
public class AgentChatHandler implements ChatHandler {
|
||||
|
||||
private final ToolProviderFactory toolProviderFactory;
|
||||
private final IChatMessageService chatMessageService;
|
||||
|
||||
@Override
|
||||
public boolean supports(ChatContext context) {
|
||||
Boolean enableThinking = context.getChatRequest().getEnableThinking();
|
||||
return enableThinking != null && enableThinking;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SseEmitter handle(ChatContext context) {
|
||||
log.info("处理 Agent 深度思考,用户: {}", context.getUserId());
|
||||
|
||||
Long userId = context.getUserId();
|
||||
String tokenValue = context.getTokenValue();
|
||||
ChatModelVo chatModelVo = context.getChatModelVo();
|
||||
|
||||
try {
|
||||
// 1. 保存用户消息
|
||||
String content = extractUserContent(context);
|
||||
saveChatMessage(context.getChatRequest(), userId, content,
|
||||
RoleType.USER.getName(), chatModelVo);
|
||||
|
||||
// 2. 执行 Agent 任务
|
||||
String result = doAgent(content, chatModelVo);
|
||||
|
||||
// 3. 发送结果并保存
|
||||
SseMessageUtils.sendMessage(userId, result);
|
||||
SseMessageUtils.completeConnection(userId, tokenValue);
|
||||
saveChatMessage(context.getChatRequest(), userId, result,
|
||||
RoleType.ASSISTANT.getName(), chatModelVo);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Agent 执行失败: {}", e.getMessage(), e);
|
||||
SseMessageUtils.sendMessage(userId, "Agent 执行失败:" + e.getMessage());
|
||||
SseMessageUtils.completeConnection(userId, tokenValue);
|
||||
}
|
||||
|
||||
return context.getEmitter();
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 Agent 任务
|
||||
*/
|
||||
private String doAgent(String userMessage, ChatModelVo chatModelVo) {
|
||||
log.info("执行 Agent 任务,消息: {}", userMessage);
|
||||
|
||||
try {
|
||||
// 1. 加载 LLM 模型
|
||||
QwenChatModel qwenChatModel = QwenChatModel.builder()
|
||||
.apiKey(chatModelVo.getApiKey())
|
||||
.modelName(chatModelVo.getModelName())
|
||||
.build();
|
||||
|
||||
// 2. 获取内置工具
|
||||
List<Object> builtinTools = toolProviderFactory.getAllBuiltinToolObjects();
|
||||
List<Object> allTools = new ArrayList<>(builtinTools);
|
||||
log.debug("加载 {} 个内置工具", builtinTools.size());
|
||||
|
||||
// 3. 获取 MCP 工具提供者
|
||||
ToolProvider mcpToolProvider = toolProviderFactory.getAllEnabledMcpToolsProvider();
|
||||
|
||||
// 4. 创建 MCP Agent
|
||||
var agentBuilder = AgenticServices.agentBuilder(McpAgent.class)
|
||||
.chatModel(qwenChatModel);
|
||||
|
||||
if (!allTools.isEmpty()) {
|
||||
agentBuilder.tools(allTools.toArray(new Object[0]));
|
||||
}
|
||||
if (mcpToolProvider != null) {
|
||||
agentBuilder.toolProvider(mcpToolProvider);
|
||||
}
|
||||
|
||||
McpAgent mcpAgent = agentBuilder.build();
|
||||
|
||||
// 5. 调用 Agent
|
||||
String result = mcpAgent.callMcpTool(userMessage);
|
||||
log.info("Agent 执行完成,结果长度: {}", result.length());
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Agent 模式执行失败: {}", e.getMessage(), e);
|
||||
return "Agent 执行失败: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取用户消息内容
|
||||
*/
|
||||
private String extractUserContent(ChatContext context) {
|
||||
var messages = context.getChatRequest().getMessages();
|
||||
if (messages != null && !messages.isEmpty()) {
|
||||
return messages.get(0).getContent();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存聊天消息
|
||||
*/
|
||||
private void saveChatMessage(org.ruoyi.common.chat.domain.dto.request.ChatRequest chatRequest,
|
||||
Long userId, String content, String role, ChatModelVo chatModelVo) {
|
||||
try {
|
||||
if (chatRequest == null || userId == null) {
|
||||
log.warn("缺少必要的聊天上下文信息,无法保存消息");
|
||||
return;
|
||||
}
|
||||
|
||||
ChatMessageBo messageBO = new ChatMessageBo();
|
||||
messageBO.setUserId(userId);
|
||||
messageBO.setSessionId(chatRequest.getSessionId());
|
||||
messageBO.setContent(content);
|
||||
messageBO.setRole(role);
|
||||
messageBO.setModelName(chatRequest.getModel());
|
||||
messageBO.setRemark(null);
|
||||
|
||||
chatMessageService.insertByBo(messageBO);
|
||||
} catch (Exception e) {
|
||||
log.error("保存聊天消息时出错: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
//package org.ruoyi.service.chat.handler;
|
||||
//
|
||||
//import dev.langchain4j.agentic.AgenticServices;
|
||||
//import dev.langchain4j.community.model.dashscope.QwenChatModel;
|
||||
//import dev.langchain4j.service.tool.ToolProvider;
|
||||
//import lombok.RequiredArgsConstructor;
|
||||
//import lombok.extern.slf4j.Slf4j;
|
||||
//import org.ruoyi.agent.McpAgent;
|
||||
//import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
//import org.ruoyi.common.chat.entity.chat.ChatContext;
|
||||
//import org.ruoyi.common.chat.service.chatMessage.IChatMessageService;
|
||||
//import org.ruoyi.common.sse.utils.SseMessageUtils;
|
||||
//import org.ruoyi.mcp.service.core.ToolProviderFactory;
|
||||
//import org.springframework.core.annotation.Order;
|
||||
//import org.springframework.stereotype.Component;
|
||||
//import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
//
|
||||
//import java.util.ArrayList;
|
||||
//import java.util.List;
|
||||
//
|
||||
///**
|
||||
// * Agent 深度思考处理器
|
||||
// * <p>
|
||||
// * 处理 enableThinking=true 的场景,使用 Agent 进行深度思考和工具调用
|
||||
// *
|
||||
// * @author ageerle@163.com
|
||||
// * @date 2025/12/13
|
||||
// */
|
||||
//@Slf4j
|
||||
//@Component
|
||||
//@Order(3)
|
||||
//@RequiredArgsConstructor
|
||||
//public class AgentChatHandler implements ChatHandler {
|
||||
//
|
||||
// private final ToolProviderFactory toolProviderFactory;
|
||||
//
|
||||
// @Override
|
||||
// public boolean supports(ChatContext context) {
|
||||
// Boolean enableThinking = context.getChatRequest().getEnableThinking();
|
||||
// return enableThinking != null && enableThinking;
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public SseEmitter handle(ChatContext context) {
|
||||
// log.info("处理 Agent 深度思考,用户: {}", context.getUserId());
|
||||
//
|
||||
// Long userId = context.getUserId();
|
||||
// String tokenValue = context.getTokenValue();
|
||||
// ChatModelVo chatModelVo = context.getChatModelVo();
|
||||
//
|
||||
// try {
|
||||
// // 1. 保存用户消息
|
||||
// String content = extractUserContent(context);
|
||||
//// saveChatMessage(context.getChatRequest(), userId, content,
|
||||
//// RoleType.USER.getName(), chatModelVo);
|
||||
//
|
||||
// // 2. 执行 Agent 任务
|
||||
// String result = doAgent(content, chatModelVo);
|
||||
//
|
||||
// // 3. 发送结果并保存
|
||||
// SseMessageUtils.sendMessage(userId, result);
|
||||
// SseMessageUtils.completeConnection(userId, tokenValue);
|
||||
//
|
||||
//// saveChatMessage(context.getChatRequest(), userId, result,
|
||||
//// RoleType.ASSISTANT.getName(), chatModelVo);
|
||||
// // todo 保存消息
|
||||
// } catch (Exception e) {
|
||||
// log.error("Agent 执行失败: {}", e.getMessage(), e);
|
||||
// SseMessageUtils.sendMessage(userId, "Agent 执行失败:" + e.getMessage());
|
||||
// SseMessageUtils.completeConnection(userId, tokenValue);
|
||||
// }
|
||||
//
|
||||
// return context.getEmitter();
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 执行 Agent 任务
|
||||
// */
|
||||
// private String doAgent(String userMessage, ChatModelVo chatModelVo) {
|
||||
// log.info("执行 Agent 任务,消息: {}", userMessage);
|
||||
//
|
||||
// try {
|
||||
// // 1. 加载 LLM 模型
|
||||
// QwenChatModel qwenChatModel = QwenChatModel.builder()
|
||||
// .apiKey(chatModelVo.getApiKey())
|
||||
// .modelName(chatModelVo.getModelName())
|
||||
// .build();
|
||||
//
|
||||
// // 2. 获取内置工具
|
||||
// List<Object> builtinTools = toolProviderFactory.getAllBuiltinToolObjects();
|
||||
// List<Object> allTools = new ArrayList<>(builtinTools);
|
||||
// log.debug("加载 {} 个内置工具", builtinTools.size());
|
||||
//
|
||||
// // 3. 获取 MCP 工具提供者
|
||||
// ToolProvider mcpToolProvider = toolProviderFactory.getAllEnabledMcpToolsProvider();
|
||||
//
|
||||
// // 4. 创建 MCP Agent
|
||||
// var agentBuilder = AgenticServices.agentBuilder(McpAgent.class)
|
||||
// .chatModel(qwenChatModel);
|
||||
//
|
||||
// if (!allTools.isEmpty()) {
|
||||
// agentBuilder.tools(allTools.toArray(new Object[0]));
|
||||
// }
|
||||
// if (mcpToolProvider != null) {
|
||||
// agentBuilder.toolProvider(mcpToolProvider);
|
||||
// }
|
||||
//
|
||||
// McpAgent mcpAgent = agentBuilder.build();
|
||||
//
|
||||
// // 5. 调用 Agent
|
||||
// String result = mcpAgent.callMcpTool(userMessage);
|
||||
// log.info("Agent 执行完成,结果长度: {}", result.length());
|
||||
// return result;
|
||||
//
|
||||
// } catch (Exception e) {
|
||||
// log.error("Agent 模式执行失败: {}", e.getMessage(), e);
|
||||
// return "Agent 执行失败: " + e.getMessage();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 提取用户消息内容
|
||||
// */
|
||||
// private String extractUserContent(ChatContext context) {
|
||||
// var messages = context.getChatRequest().getMessages();
|
||||
// if (messages != null && !messages.isEmpty()) {
|
||||
// return messages.get(0).getContent();
|
||||
// }
|
||||
// return "";
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
package org.ruoyi.service.chat.handler;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.domain.dto.ChatMessageDTO;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.common.chat.entity.chat.ChatContext;
|
||||
import org.ruoyi.common.chat.factory.ChatServiceFactory;
|
||||
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||
import org.ruoyi.common.chat.service.chat.IChatService;
|
||||
import org.ruoyi.common.satoken.utils.LoginHelper;
|
||||
import org.ruoyi.common.sse.core.SseEmitterManager;
|
||||
import org.ruoyi.domain.bo.vector.QueryVectorBo;
|
||||
import org.ruoyi.domain.vo.knowledge.KnowledgeInfoVo;
|
||||
import org.ruoyi.service.knowledge.IKnowledgeInfoService;
|
||||
import org.ruoyi.service.vector.VectorStoreService;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 对话上下文构建器
|
||||
* <p>
|
||||
* 负责构建完整的对话上下文,包括:
|
||||
* 1. 模型配置查询
|
||||
* 2. 知识库检索增强
|
||||
* 3. SSE连接创建
|
||||
* 4. 用户信息注入
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/12/13
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ChatContextBuilder {
|
||||
|
||||
private final IChatModelService chatModelService;
|
||||
private final IKnowledgeInfoService knowledgeInfoService;
|
||||
private final VectorStoreService vectorStoreService;
|
||||
private final SseEmitterManager sseEmitterManager;
|
||||
private final ChatServiceFactory chatServiceFactory;
|
||||
|
||||
/**
|
||||
* 构建对话上下文
|
||||
*
|
||||
* @param chatRequest 对话请求
|
||||
* @return 完整的对话上下文
|
||||
*/
|
||||
public ChatContext build(ChatRequest chatRequest) {
|
||||
// 1. 查询模型配置
|
||||
ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
|
||||
if (chatModelVo == null) {
|
||||
throw new IllegalArgumentException("模型不存在: " + chatRequest.getModel());
|
||||
}
|
||||
|
||||
// 2. 构建上下文消息(知识库增强)
|
||||
List<ChatMessageDTO> contextMessages = buildContextMessages(chatRequest);
|
||||
chatRequest.setMessages(contextMessages);
|
||||
|
||||
// 3. 获取用户信息
|
||||
Long userId = LoginHelper.getUserId();
|
||||
String tokenValue = StpUtil.getTokenValue();
|
||||
|
||||
// 4. 创建SSE连接
|
||||
SseEmitter emitter = sseEmitterManager.connect(userId, tokenValue);
|
||||
|
||||
// 5. 获取服务提供商
|
||||
String category = chatModelVo.getProviderCode();
|
||||
IChatService chatService = chatServiceFactory.getOriginalService(category);
|
||||
log.info("路由到服务提供商: {}, 模型: {}", category, chatRequest.getModel());
|
||||
|
||||
// 6. 构建上下文对象
|
||||
return ChatContext.builder()
|
||||
.chatModelVo(chatModelVo)
|
||||
.chatRequest(chatRequest)
|
||||
.emitter(emitter)
|
||||
.userId(userId)
|
||||
.tokenValue(tokenValue)
|
||||
.chatService(chatService)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建上下文消息列表(知识库增强)
|
||||
*/
|
||||
private List<ChatMessageDTO> buildContextMessages(ChatRequest chatRequest) {
|
||||
List<ChatMessageDTO> messages = chatRequest.getMessages();
|
||||
|
||||
// 从向量库查询相关历史消息
|
||||
if (chatRequest.getKnowledgeId() != null) {
|
||||
KnowledgeInfoVo knowledgeInfoVo = knowledgeInfoService.queryById(Long.valueOf(chatRequest.getKnowledgeId()));
|
||||
if (knowledgeInfoVo == null) {
|
||||
log.warn("知识库信息不存在,kid: {}", chatRequest.getKnowledgeId());
|
||||
return messages;
|
||||
}
|
||||
|
||||
// 查询向量模型配置
|
||||
ChatModelVo chatModel = chatModelService.selectModelByName(knowledgeInfoVo.getEmbeddingModel());
|
||||
if (chatModel == null) {
|
||||
log.warn("向量模型配置不存在,模型名称: {}", knowledgeInfoVo.getEmbeddingModel());
|
||||
return messages;
|
||||
}
|
||||
|
||||
// 构建向量查询参数并检索
|
||||
QueryVectorBo queryVectorBo = buildQueryVectorBo(chatRequest, knowledgeInfoVo, chatModel);
|
||||
List<String> nearestList = vectorStoreService.getQueryVector(queryVectorBo);
|
||||
|
||||
// 知识库内容作为系统上下文添加
|
||||
for (String prompt : nearestList) {
|
||||
messages.add(ChatMessageDTO.system(prompt));
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建向量查询参数
|
||||
*/
|
||||
private QueryVectorBo buildQueryVectorBo(ChatRequest chatRequest, KnowledgeInfoVo knowledgeInfoVo,
|
||||
ChatModelVo chatModel) {
|
||||
QueryVectorBo queryVectorBo = new QueryVectorBo();
|
||||
queryVectorBo.setQuery(chatRequest.getMessages().get(0).getContent());
|
||||
queryVectorBo.setKid(chatRequest.getKnowledgeId());
|
||||
queryVectorBo.setApiKey(chatModel.getApiKey());
|
||||
queryVectorBo.setBaseUrl(chatModel.getApiHost());
|
||||
queryVectorBo.setVectorModelName(knowledgeInfoVo.getVectorModel());
|
||||
queryVectorBo.setEmbeddingModelName(knowledgeInfoVo.getEmbeddingModel());
|
||||
queryVectorBo.setMaxResults(knowledgeInfoVo.getRetrieveLimit());
|
||||
return queryVectorBo;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package org.ruoyi.service.chat.handler;
|
||||
|
||||
import org.ruoyi.common.chat.entity.chat.ChatContext;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
/**
|
||||
* 对话处理器接口
|
||||
* <p>
|
||||
* 使用策略模式,每种对话场景独立实现
|
||||
* 通过 Order 注解控制优先级
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/12/13
|
||||
*/
|
||||
public interface ChatHandler {
|
||||
|
||||
/**
|
||||
* 是否支持处理该请求
|
||||
*
|
||||
* @param context 对话上下文
|
||||
* @return true-支持处理,false-不支持
|
||||
*/
|
||||
boolean supports(ChatContext context);
|
||||
|
||||
/**
|
||||
* 处理对话
|
||||
*
|
||||
* @param context 对话上下文
|
||||
* @return SSE发射器
|
||||
*/
|
||||
SseEmitter handle(ChatContext context);
|
||||
|
||||
/**
|
||||
* 优先级(越小越优先)
|
||||
* 默认 100,数字越小优先级越高
|
||||
*
|
||||
* @return 优先级数值
|
||||
*/
|
||||
default int getOrder() {
|
||||
return 100;
|
||||
}
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
package org.ruoyi.service.chat.handler;
|
||||
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.data.message.UserMessage;
|
||||
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.entity.chat.ChatContext;
|
||||
import org.ruoyi.common.chat.enums.RoleType;
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
import org.ruoyi.common.sse.utils.SseMessageUtils;
|
||||
import org.ruoyi.service.chat.impl.AbstractStreamingChatService;
|
||||
import org.ruoyi.service.chat.impl.memory.PersistentChatMemoryStore;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 默认对话处理器
|
||||
* <p>
|
||||
* 处理普通对话场景,包含:
|
||||
* 1. 历史记忆管理
|
||||
* 2. 消息保存
|
||||
* 3. 流式对话响应
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/12/13
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(100)
|
||||
public class DefaultChatHandler implements ChatHandler {
|
||||
|
||||
private final Map<String, AbstractStreamingChatService> chatServiceMap;
|
||||
|
||||
/**
|
||||
* 默认保留的消息窗口大小
|
||||
*/
|
||||
private static final int DEFAULT_MAX_MESSAGES = 20;
|
||||
|
||||
/**
|
||||
* 是否启用长期记忆
|
||||
*/
|
||||
private static final boolean ENABLE_PERSISTENT_MEMORY = true;
|
||||
|
||||
/**
|
||||
* 内存实例缓存
|
||||
*/
|
||||
private static final Map<Object, MessageWindowChatMemory> MEMORY_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 构造函数,注入所有聊天服务实现
|
||||
*/
|
||||
public DefaultChatHandler(List<AbstractStreamingChatService> chatServices) {
|
||||
this.chatServiceMap = chatServices.stream()
|
||||
.collect(Collectors.toMap(
|
||||
AbstractStreamingChatService::getProviderName,
|
||||
Function.identity()
|
||||
));
|
||||
log.info("已加载 {} 个聊天服务: {}", chatServiceMap.size(), chatServiceMap.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 providerCode 获取对应的聊天服务
|
||||
*/
|
||||
private AbstractStreamingChatService getChatService(String providerCode) {
|
||||
if (StringUtils.isBlank(providerCode)) {
|
||||
// 默认使用千问服务
|
||||
return chatServiceMap.get("qianwen");
|
||||
}
|
||||
AbstractStreamingChatService service = chatServiceMap.get(providerCode.toLowerCase());
|
||||
if (service == null) {
|
||||
log.warn("未找到提供商 {} 对应的服务,使用默认千问服务", providerCode);
|
||||
return chatServiceMap.get("qianwen");
|
||||
}
|
||||
return service;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(ChatContext context) {
|
||||
// 默认处理器,始终支持
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SseEmitter handle(ChatContext context) {
|
||||
log.info("处理默认对话,用户: {}, 会话: {}",
|
||||
context.getUserId(), context.getChatRequest().getSessionId());
|
||||
|
||||
Long userId = context.getUserId();
|
||||
String tokenValue = context.getTokenValue();
|
||||
|
||||
// 根据 providerCode 获取对应的聊天服务
|
||||
String providerCode = context.getChatModelVo().getProviderCode();
|
||||
AbstractStreamingChatService chatService = getChatService(providerCode);
|
||||
log.info("使用服务提供商: {}", chatService.getProviderName());
|
||||
|
||||
try {
|
||||
// 1. 提取用户消息内容
|
||||
String content = extractUserContent(context);
|
||||
|
||||
// 2. 保存用户消息
|
||||
chatService.saveChatMessage(context.getChatRequest(), userId, content,
|
||||
RoleType.USER.getName(), context.getChatModelVo());
|
||||
|
||||
// 3. 构建包含历史记忆的消息列表
|
||||
List<ChatMessage> messagesWithMemory = buildMessagesWithMemory(context.getChatRequest());
|
||||
|
||||
// 4. 创建响应处理器
|
||||
StreamingChatResponseHandler handler = createResponseHandler(
|
||||
context.getChatRequest(), userId, tokenValue, context.getChatModelVo(), chatService);
|
||||
|
||||
// 5. 构建流式模型并执行对话
|
||||
StreamingChatModel streamingModel = chatService.buildStreamingChatModel(
|
||||
context.getChatModelVo(), context.getChatRequest());
|
||||
streamingModel.chat(messagesWithMemory, handler);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("对话处理失败: {}", e.getMessage(), e);
|
||||
SseMessageUtils.sendMessage(userId, "对话出错:" + e.getMessage());
|
||||
SseMessageUtils.completeConnection(userId, tokenValue);
|
||||
}
|
||||
|
||||
return context.getEmitter();
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取用户消息内容
|
||||
*/
|
||||
private String extractUserContent(ChatContext context) {
|
||||
return Optional.ofNullable(context.getChatRequest().getMessages())
|
||||
.filter(messages -> !messages.isEmpty())
|
||||
.map(messages -> messages.get(0).getContent())
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.orElseGet(() -> Optional.ofNullable(context.getChatRequest().getChatMessages())
|
||||
.orElse(List.of()).stream()
|
||||
.filter(message -> message instanceof UserMessage)
|
||||
.map(message -> ((UserMessage) message).singleText())
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.findFirst()
|
||||
.orElse(""));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建包含历史消息的消息列表
|
||||
*/
|
||||
private List<ChatMessage> buildMessagesWithMemory(org.ruoyi.common.chat.domain.dto.request.ChatRequest chatRequest) {
|
||||
List<ChatMessage> messages = new ArrayList<>();
|
||||
|
||||
// 添加工作流对话消息
|
||||
List<ChatMessage> chatMessages = chatRequest.getChatMessages();
|
||||
if (!CollectionUtils.isEmpty(chatMessages)) {
|
||||
messages.addAll(chatMessages);
|
||||
}
|
||||
|
||||
// 添加历史记忆
|
||||
if (ENABLE_PERSISTENT_MEMORY && chatRequest.getSessionId() != null) {
|
||||
MessageWindowChatMemory memory = createChatMemory(chatRequest.getSessionId());
|
||||
if (memory != null) {
|
||||
List<ChatMessage> historicalMessages = memory.messages();
|
||||
if (historicalMessages != null && !historicalMessages.isEmpty()) {
|
||||
messages.addAll(historicalMessages);
|
||||
log.debug("已加载 {} 条历史消息用于会话 {}", historicalMessages.size(), chatRequest.getSessionId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建或获取聊天内存实例
|
||||
*/
|
||||
private MessageWindowChatMemory createChatMemory(Object memoryId) {
|
||||
return MEMORY_CACHE.computeIfAbsent(memoryId, key -> {
|
||||
try {
|
||||
PersistentChatMemoryStore store = new PersistentChatMemoryStore();
|
||||
return MessageWindowChatMemory.builder()
|
||||
.id(memoryId)
|
||||
.maxMessages(DEFAULT_MAX_MESSAGES)
|
||||
.chatMemoryStore(store)
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.warn("创建聊天内存失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建响应处理器
|
||||
*/
|
||||
private StreamingChatResponseHandler createResponseHandler(
|
||||
org.ruoyi.common.chat.domain.dto.request.ChatRequest chatRequest,
|
||||
Long userId,
|
||||
String tokenValue,
|
||||
org.ruoyi.common.chat.domain.vo.chat.ChatModelVo chatModelVo,
|
||||
AbstractStreamingChatService chatService) {
|
||||
|
||||
return new StreamingChatResponseHandler() {
|
||||
private final StringBuilder messageBuffer = new StringBuilder();
|
||||
|
||||
@Override
|
||||
public void onPartialResponse(String partialResponse) {
|
||||
messageBuffer.append(partialResponse);
|
||||
SseMessageUtils.sendMessage(userId, partialResponse);
|
||||
log.debug("收到消息片段: {}", partialResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleteResponse(dev.langchain4j.model.chat.response.ChatResponse completeResponse) {
|
||||
try {
|
||||
String fullMessage = messageBuffer.toString();
|
||||
if (!fullMessage.isEmpty()) {
|
||||
chatService.saveChatMessage(chatRequest, userId, fullMessage,
|
||||
RoleType.ASSISTANT.getName(), chatModelVo);
|
||||
}
|
||||
SseMessageUtils.completeConnection(userId, tokenValue);
|
||||
log.info("消息结束,已保存到数据库");
|
||||
} catch (Exception e) {
|
||||
log.error("完成响应时出错: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
log.error("流式响应错误: {}", error.getMessage(), error);
|
||||
try {
|
||||
SseMessageUtils.sendMessage(userId, "模型调用失败: " + error.getMessage());
|
||||
SseMessageUtils.completeConnection(userId, tokenValue);
|
||||
} catch (Exception e) {
|
||||
log.error("发送错误消息失败: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,53 +1,53 @@
|
||||
package org.ruoyi.service.chat.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ReSumeRunner;
|
||||
import org.ruoyi.common.chat.entity.chat.ChatContext;
|
||||
import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService;
|
||||
import org.ruoyi.common.core.utils.ObjectUtils;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
/**
|
||||
* 人机交互恢复处理器
|
||||
* <p>
|
||||
* 处理 isResume=true 的场景,恢复工作流的人机交互
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/12/13
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(1)
|
||||
@RequiredArgsConstructor
|
||||
public class ResumeChatHandler implements ChatHandler {
|
||||
|
||||
private final IWorkFlowStarterService workFlowStarterService;
|
||||
|
||||
@Override
|
||||
public boolean supports(ChatContext context) {
|
||||
Boolean isResume = context.getChatRequest().getIsResume();
|
||||
return isResume != null && isResume;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SseEmitter handle(ChatContext context) {
|
||||
log.info("处理人机交互恢复,用户: {}", context.getUserId());
|
||||
|
||||
ReSumeRunner reSumeRunner = context.getChatRequest().getReSumeRunner();
|
||||
if (ObjectUtils.isEmpty(reSumeRunner)) {
|
||||
log.warn("人机交互恢复参数为空");
|
||||
return context.getEmitter();
|
||||
}
|
||||
|
||||
workFlowStarterService.resumeFlow(
|
||||
reSumeRunner.getRuntimeUuid(),
|
||||
reSumeRunner.getFeedbackContent(),
|
||||
context.getEmitter()
|
||||
);
|
||||
|
||||
return context.getEmitter();
|
||||
}
|
||||
}
|
||||
//package org.ruoyi.service.chat.handler;
|
||||
//
|
||||
//import lombok.RequiredArgsConstructor;
|
||||
//import lombok.extern.slf4j.Slf4j;
|
||||
//import org.ruoyi.common.chat.domain.dto.request.ReSumeRunner;
|
||||
//import org.ruoyi.common.chat.entity.chat.ChatContext;
|
||||
//import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService;
|
||||
//import org.ruoyi.common.core.utils.ObjectUtils;
|
||||
//import org.springframework.core.annotation.Order;
|
||||
//import org.springframework.stereotype.Component;
|
||||
//import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
//
|
||||
///**
|
||||
// * 人机交互恢复处理器
|
||||
// * <p>
|
||||
// * 处理 isResume=true 的场景,恢复工作流的人机交互
|
||||
// *
|
||||
// * @author ageerle@163.com
|
||||
// * @date 2025/12/13
|
||||
// */
|
||||
//@Slf4j
|
||||
//@Component
|
||||
//@Order(1)
|
||||
//@RequiredArgsConstructor
|
||||
//public class ResumeChatHandler implements ChatHandler {
|
||||
//
|
||||
// private final IWorkFlowStarterService workFlowStarterService;
|
||||
//
|
||||
// @Override
|
||||
// public boolean supports(ChatContext context) {
|
||||
// Boolean isResume = context.getChatRequest().getIsResume();
|
||||
// return isResume != null && isResume;
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public SseEmitter handle(ChatContext context) {
|
||||
// log.info("处理人机交互恢复,用户: {}", context.getUserId());
|
||||
//
|
||||
// ReSumeRunner reSumeRunner = context.getChatRequest().getReSumeRunner();
|
||||
// if (ObjectUtils.isEmpty(reSumeRunner)) {
|
||||
// log.warn("人机交互恢复参数为空");
|
||||
// return context.getEmitter();
|
||||
// }
|
||||
//
|
||||
// workFlowStarterService.resumeFlow(
|
||||
// reSumeRunner.getRuntimeUuid(),
|
||||
// reSumeRunner.getFeedbackContent(),
|
||||
// context.getEmitter()
|
||||
// );
|
||||
//
|
||||
// return context.getEmitter();
|
||||
// }
|
||||
//}
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
package org.ruoyi.service.chat.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.base.ThreadContext;
|
||||
import org.ruoyi.common.chat.domain.dto.request.WorkFlowRunner;
|
||||
import org.ruoyi.common.chat.entity.chat.ChatContext;
|
||||
import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService;
|
||||
import org.ruoyi.common.core.utils.ObjectUtils;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
/**
|
||||
* 工作流对话处理器
|
||||
* <p>
|
||||
* 处理 enableWorkFlow=true 的场景,启动工作流对话
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/12/13
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(2)
|
||||
@RequiredArgsConstructor
|
||||
public class WorkflowChatHandler implements ChatHandler {
|
||||
|
||||
private final IWorkFlowStarterService workFlowStarterService;
|
||||
|
||||
@Override
|
||||
public boolean supports(ChatContext context) {
|
||||
Boolean enableWorkFlow = context.getChatRequest().getEnableWorkFlow();
|
||||
return enableWorkFlow != null && enableWorkFlow;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SseEmitter handle(ChatContext context) {
|
||||
log.info("处理工作流对话,用户: {}, 会话: {}",
|
||||
context.getUserId(), context.getChatRequest().getSessionId());
|
||||
|
||||
WorkFlowRunner runner = context.getChatRequest().getWorkFlowRunner();
|
||||
if (ObjectUtils.isEmpty(runner)) {
|
||||
log.warn("工作流参数为空");
|
||||
return context.getEmitter();
|
||||
}
|
||||
|
||||
return workFlowStarterService.streaming(
|
||||
ThreadContext.getCurrentUser(),
|
||||
runner.getUuid(),
|
||||
runner.getInputs(),
|
||||
context.getChatRequest().getSessionId()
|
||||
);
|
||||
}
|
||||
}
|
||||
//package org.ruoyi.service.chat.handler;
|
||||
//
|
||||
//import lombok.RequiredArgsConstructor;
|
||||
//import lombok.extern.slf4j.Slf4j;
|
||||
//import org.ruoyi.common.chat.base.ThreadContext;
|
||||
//import org.ruoyi.common.chat.domain.dto.request.WorkFlowRunner;
|
||||
//import org.ruoyi.common.chat.entity.chat.ChatContext;
|
||||
//import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService;
|
||||
//import org.ruoyi.common.core.utils.ObjectUtils;
|
||||
//import org.springframework.core.annotation.Order;
|
||||
//import org.springframework.stereotype.Component;
|
||||
//import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
//
|
||||
///**
|
||||
// * 工作流对话处理器
|
||||
// * <p>
|
||||
// * 处理 enableWorkFlow=true 的场景,启动工作流对话
|
||||
// *
|
||||
// * @author ageerle@163.com
|
||||
// * @date 2025/12/13
|
||||
// */
|
||||
//@Slf4j
|
||||
//@Component
|
||||
//@Order(2)
|
||||
//@RequiredArgsConstructor
|
||||
//public class WorkflowChatHandler implements ChatHandler {
|
||||
//
|
||||
// private final IWorkFlowStarterService workFlowStarterService;
|
||||
//
|
||||
// @Override
|
||||
// public boolean supports(ChatContext context) {
|
||||
// Boolean enableWorkFlow = context.getChatRequest().getEnableWorkFlow();
|
||||
// return enableWorkFlow != null && enableWorkFlow;
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public SseEmitter handle(ChatContext context) {
|
||||
// log.info("处理工作流对话,用户: {}, 会话: {}",
|
||||
// context.getUserId(), context.getChatRequest().getSessionId());
|
||||
//
|
||||
// WorkFlowRunner runner = context.getChatRequest().getWorkFlowRunner();
|
||||
// if (ObjectUtils.isEmpty(runner)) {
|
||||
// log.warn("工作流参数为空");
|
||||
// return context.getEmitter();
|
||||
// }
|
||||
//
|
||||
// return workFlowStarterService.streaming(
|
||||
// ThreadContext.getCurrentUser(),
|
||||
// runner.getUuid(),
|
||||
// runner.getInputs(),
|
||||
// context.getChatRequest().getSessionId()
|
||||
// );
|
||||
// }
|
||||
//}
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
package org.ruoyi.service.chat.impl;
|
||||
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.data.message.UserMessage;
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.chat.response.ChatResponse;
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.common.chat.entity.chat.ChatContext;
|
||||
import org.ruoyi.common.chat.enums.RoleType;
|
||||
import org.ruoyi.common.chat.service.chat.IChatService;
|
||||
import org.ruoyi.common.chat.service.chatMessage.AbstractChatMessageService;
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
import org.ruoyi.common.sse.utils.SseMessageUtils;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 流式聊天服务抽象基类
|
||||
* <p>
|
||||
* 提供核心的流式对话能力:
|
||||
* 1. 构建流式聊天模型
|
||||
* 2. 创建响应处理器
|
||||
* 3. 消息持久化
|
||||
* <p>
|
||||
* 设计原则:
|
||||
* - 抽象层只依赖业务模型,不依赖具体SDK
|
||||
* - 子类负责将业务模型转换为厂商SDK格式
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/12/13
|
||||
*/
|
||||
@Slf4j
|
||||
@Validated
|
||||
public abstract class AbstractStreamingChatService extends AbstractChatMessageService implements IChatService {
|
||||
|
||||
/**
|
||||
* 定义聊天流程骨架
|
||||
* 注意:此方法已被 Handler 模式取代,保留是为了兼容旧调用
|
||||
*/
|
||||
@Override
|
||||
@Deprecated
|
||||
public SseEmitter chat(ChatContext chatContext) {
|
||||
ChatModelVo chatModelVo = chatContext.getChatModelVo();
|
||||
ChatRequest chatRequest = chatContext.getChatRequest();
|
||||
Long userId = chatContext.getUserId();
|
||||
String tokenValue = chatContext.getTokenValue();
|
||||
SseEmitter emitter = chatContext.getEmitter();
|
||||
|
||||
try {
|
||||
// 提取用户消息内容
|
||||
String content = extractUserContent(chatRequest);
|
||||
|
||||
// 保存用户消息
|
||||
saveChatMessage(chatRequest, userId, content, RoleType.USER.getName(), chatModelVo);
|
||||
|
||||
// 构建消息列表(由 Handler 负责构建,这里简单处理)
|
||||
List<ChatMessage> messages = convertToChatMessages(chatRequest);
|
||||
|
||||
// 创建响应处理器
|
||||
StreamingChatResponseHandler handler = createResponseHandler(
|
||||
chatRequest, userId, tokenValue, chatModelVo);
|
||||
|
||||
// 调用具体实现的聊天方法
|
||||
doChat(chatModelVo, chatRequest, messages, handler);
|
||||
|
||||
} catch (Exception e) {
|
||||
SseMessageUtils.sendMessage(userId, "对话出错:" + e.getMessage());
|
||||
SseMessageUtils.completeConnection(userId, tokenValue);
|
||||
log.error("{}请求失败:{}", getProviderName(), e.getMessage(), e);
|
||||
}
|
||||
|
||||
return emitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取用户消息内容
|
||||
*/
|
||||
private String extractUserContent(ChatRequest chatRequest) {
|
||||
return Optional.ofNullable(chatRequest.getMessages())
|
||||
.filter(messages -> !messages.isEmpty())
|
||||
.map(messages -> messages.get(0).getContent())
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.orElseGet(() -> Optional.ofNullable(chatRequest.getChatMessages())
|
||||
.orElse(List.of()).stream()
|
||||
.filter(message -> message instanceof UserMessage)
|
||||
.map(message -> ((UserMessage) message).singleText())
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.findFirst()
|
||||
.orElse(""));
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换消息格式
|
||||
*/
|
||||
private List<ChatMessage> convertToChatMessages(ChatRequest chatRequest) {
|
||||
List<ChatMessage> chatMessages = chatRequest.getChatMessages();
|
||||
return chatMessages != null ? chatMessages : List.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行聊天(钩子方法 - 子类必须实现)
|
||||
*
|
||||
* @param chatModelVo 模型配置
|
||||
* @param chatRequest 聊天请求
|
||||
* @param messagesWithMemory 消息列表
|
||||
* @param handler 响应处理器
|
||||
*/
|
||||
protected abstract void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest,
|
||||
List<ChatMessage> messagesWithMemory, StreamingChatResponseHandler handler);
|
||||
|
||||
/**
|
||||
* 创建标准的响应处理器
|
||||
*
|
||||
* @param chatRequest 聊天请求
|
||||
* @param userId 用户ID
|
||||
* @param tokenValue 会话令牌
|
||||
* @param chatModelVo 模型配置
|
||||
* @return 流式响应处理器
|
||||
*/
|
||||
protected StreamingChatResponseHandler createResponseHandler(ChatRequest chatRequest, Long userId,
|
||||
String tokenValue, ChatModelVo chatModelVo) {
|
||||
return new StreamingChatResponseHandler() {
|
||||
private final StringBuilder messageBuffer = new StringBuilder();
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public void onPartialResponse(String partialResponse) {
|
||||
messageBuffer.append(partialResponse);
|
||||
SseMessageUtils.sendMessage(userId, partialResponse);
|
||||
log.debug("收到{}消息片段: {}", getProviderName(), partialResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleteResponse(ChatResponse completeResponse) {
|
||||
try {
|
||||
String fullMessage = messageBuffer.toString();
|
||||
if (!fullMessage.isEmpty()) {
|
||||
saveChatMessage(chatRequest, userId, fullMessage,
|
||||
RoleType.ASSISTANT.getName(), chatModelVo);
|
||||
}
|
||||
SseMessageUtils.completeConnection(userId, tokenValue);
|
||||
log.info("{}消息结束,已保存到数据库", getProviderName());
|
||||
} catch (Exception e) {
|
||||
log.error("{}完成响应时出错: {}", getProviderName(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
log.error("{}流式响应错误: {}", getProviderName(), error.getMessage(), error);
|
||||
try {
|
||||
String errorMessage = String.format("模型调用失败: %s", error.getMessage());
|
||||
SseMessageUtils.sendMessage(userId, errorMessage);
|
||||
SseMessageUtils.completeConnection(userId, tokenValue);
|
||||
} catch (Exception e) {
|
||||
log.error("发送错误消息失败: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取提供者名称(子类必须实现)
|
||||
*/
|
||||
public abstract String getProviderName();
|
||||
|
||||
/**
|
||||
* 创建流式聊天模型(子类必须实现)
|
||||
*
|
||||
* @param chatModelVo 模型配置
|
||||
* @param chatRequest 聊天请求
|
||||
* @return 流式聊天模型实例
|
||||
*/
|
||||
public abstract StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest);
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.ruoyi.service.chat.impl;
|
||||
|
||||
import dev.langchain4j.data.message.AiMessage;
|
||||
import dev.langchain4j.data.message.UserMessage;
|
||||
import org.ruoyi.common.chat.domain.bo.chat.ChatMessageBo;
|
||||
import org.ruoyi.common.chat.domain.dto.ChatMessageDTO;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatMessageVo;
|
||||
import org.ruoyi.common.chat.entity.chat.ChatMessage;
|
||||
import org.ruoyi.common.chat.service.chatMessage.IChatMessageService;
|
||||
import org.ruoyi.common.core.utils.MapstructUtils;
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
@@ -14,9 +14,11 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.service.chat.IChatMessageService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.ruoyi.mapper.chat.ChatMessageMapper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Collection;
|
||||
@@ -144,25 +146,27 @@ public class ChatMessageServiceImpl implements IChatMessageService {
|
||||
* @return 消息DTO列表
|
||||
*/
|
||||
@Override
|
||||
public List<ChatMessageDTO> getMessagesBySessionId(Long sessionId) {
|
||||
public List<dev.langchain4j.data.message.ChatMessage> getMessagesBySessionId(Long sessionId) {
|
||||
if (sessionId == null) {
|
||||
return new java.util.ArrayList<>();
|
||||
}
|
||||
|
||||
List<dev.langchain4j.data.message.ChatMessage> chatMessageList = new ArrayList<>();
|
||||
ChatMessageBo bo = new ChatMessageBo();
|
||||
bo.setSessionId(sessionId);
|
||||
List<ChatMessageVo> voList = queryList(bo);
|
||||
|
||||
return voList.stream()
|
||||
.map(vo -> {
|
||||
ChatMessageDTO dto = new ChatMessageDTO();
|
||||
dto.setRole(vo.getRole());
|
||||
dto.setContent(vo.getContent());
|
||||
return dto;
|
||||
})
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
for (ChatMessageVo chatMessageVo : voList) {
|
||||
switch (chatMessageVo.getRole()) {
|
||||
case "user" -> chatMessageList.add(UserMessage.from(chatMessageVo.getContent()));
|
||||
case "assistant" -> chatMessageList.add(AiMessage.from(chatMessageVo.getContent()));
|
||||
}
|
||||
}
|
||||
return chatMessageList;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 根据会话ID删除所有消息
|
||||
* 用于清理会话历史
|
||||
@@ -180,4 +184,35 @@ public class ChatMessageServiceImpl implements IChatMessageService {
|
||||
lqw.eq(ChatMessage::getSessionId, sessionId);
|
||||
return baseMapper.delete(lqw) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存聊天消息
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param sessionId 会话ID
|
||||
* @param content 消息内容
|
||||
* @param role 角色类型
|
||||
* @param modelName 模型名称
|
||||
*/
|
||||
@Override
|
||||
public void saveChatMessage(Long userId, Long sessionId, String content, String role, String modelName) {
|
||||
try {
|
||||
if (userId == null) {
|
||||
log.warn("缺少用户ID,无法保存消息");
|
||||
return;
|
||||
}
|
||||
|
||||
ChatMessageBo messageBo = new ChatMessageBo();
|
||||
messageBo.setUserId(userId);
|
||||
messageBo.setSessionId(sessionId);
|
||||
messageBo.setContent(content);
|
||||
messageBo.setRole(role);
|
||||
messageBo.setModelName(modelName);
|
||||
|
||||
insertByBo(messageBo);
|
||||
log.debug("保存聊天消息成功,角色: {}, 会话: {}", role, sessionId);
|
||||
} catch (Exception e) {
|
||||
log.error("保存聊天消息时出错: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,37 @@
|
||||
package org.ruoyi.service.chat.impl;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import dev.langchain4j.data.message.*;
|
||||
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.chat.response.ChatResponse;
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.entity.chat.ChatContext;
|
||||
import org.ruoyi.service.chat.handler.ChatContextBuilder;
|
||||
import org.ruoyi.service.chat.handler.ChatHandler;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.common.chat.enums.RoleType;
|
||||
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||
import org.ruoyi.common.chat.service.chat.IChatService;
|
||||
import org.ruoyi.common.satoken.utils.LoginHelper;
|
||||
import org.ruoyi.common.sse.core.SseEmitterManager;
|
||||
import org.ruoyi.common.sse.utils.SseMessageUtils;
|
||||
import org.ruoyi.domain.bo.vector.QueryVectorBo;
|
||||
import org.ruoyi.domain.vo.knowledge.KnowledgeInfoVo;
|
||||
import org.ruoyi.factory.ChatServiceFactory;
|
||||
import org.ruoyi.service.chat.AbstractChatService;
|
||||
import org.ruoyi.service.chat.IChatMessageService;
|
||||
import org.ruoyi.service.chat.impl.memory.PersistentChatMemoryStore;
|
||||
import org.ruoyi.service.knowledge.IKnowledgeInfoService;
|
||||
import org.ruoyi.service.vector.VectorStoreService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 聊天服务门面层
|
||||
@@ -25,27 +46,324 @@ import java.util.List;
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class ChatServiceFacade {
|
||||
public class ChatServiceFacade implements IChatService {
|
||||
|
||||
private final ChatContextBuilder contextBuilder;
|
||||
private final List<ChatHandler> handlers;
|
||||
private static final Integer DEFAULT_MAX_MESSAGES = 20;
|
||||
|
||||
private final IChatModelService chatModelService;
|
||||
|
||||
private final ChatServiceFactory chatServiceFactory;
|
||||
|
||||
private final IKnowledgeInfoService knowledgeInfoService;
|
||||
|
||||
private final VectorStoreService vectorStoreService;
|
||||
|
||||
private final SseEmitterManager sseEmitterManager;
|
||||
|
||||
private final IChatMessageService chatMessageService;
|
||||
|
||||
/**
|
||||
* 内存实例缓存,避免同一会话重复创建
|
||||
* Key: sessionId, Value: MessageWindowChatMemory实例
|
||||
*/
|
||||
private static final Map<Object, MessageWindowChatMemory> memoryCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 统一聊天入口 - SSE流式响应
|
||||
*
|
||||
* @param chatRequest 聊天请求
|
||||
* @param request HTTP请求对象
|
||||
* @return SseEmitter
|
||||
*/
|
||||
public SseEmitter sseChat(ChatRequest chatRequest, HttpServletRequest request) {
|
||||
// 1. 构建对话上下文
|
||||
ChatContext context = contextBuilder.build(chatRequest);
|
||||
public SseEmitter sseChat(ChatRequest chatRequest) {
|
||||
|
||||
// 2. 路由到对应的处理器
|
||||
return handlers.stream()
|
||||
.filter(handler -> handler.supports(context))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("无可用对话处理器"))
|
||||
.handle(context);
|
||||
// 1. 根据模型名称查询完整配置
|
||||
ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
|
||||
if (chatModelVo == null) {
|
||||
throw new IllegalArgumentException("模型不存在: " + chatRequest.getModel());
|
||||
}
|
||||
|
||||
// 2. 构建上下文消息列表
|
||||
List<ChatMessage> contextMessages = buildContextMessages(chatRequest);
|
||||
|
||||
// 3. 路由服务提供商
|
||||
String providerCode = chatModelVo.getProviderCode();
|
||||
log.info("路由到服务提供商: {}, 模型: {}", providerCode, chatRequest.getModel());
|
||||
AbstractChatService chatService = chatServiceFactory.getOriginalService(providerCode);
|
||||
// 4. 具体的服务实现
|
||||
Long userId = LoginHelper.getUserId();
|
||||
String tokenValue = StpUtil.getTokenValue();
|
||||
SseEmitter emitter = sseEmitterManager.connect(userId, tokenValue);
|
||||
|
||||
StreamingChatResponseHandler handler = createResponseHandler(userId, tokenValue,chatRequest);
|
||||
|
||||
// 保存用户消息
|
||||
chatMessageService.saveChatMessage(userId, chatRequest.getSessionId(), chatRequest.getContent(), RoleType.USER.getName(), chatRequest.getModel());
|
||||
|
||||
// 5. 发起对话
|
||||
StreamingChatModel streamingChatModel = chatService.buildStreamingChatModel(chatModelVo, chatRequest);
|
||||
streamingChatModel.chat(contextMessages, handler);
|
||||
return emitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持外部 handler 的对话接口(跨模块调用)
|
||||
* 同时发送到 SSE 和外部 handler
|
||||
*
|
||||
* @param chatRequest 聊天请求
|
||||
* @param externalHandler 外部响应处理器(可为 null)
|
||||
*/
|
||||
@Override
|
||||
public void chat(ChatRequest chatRequest, StreamingChatResponseHandler externalHandler) {
|
||||
// 1. 根据模型名称查询完整配置
|
||||
ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
|
||||
if (chatModelVo == null) {
|
||||
throw new IllegalArgumentException("模型不存在: " + chatRequest.getModel());
|
||||
}
|
||||
|
||||
// 3. 路由服务提供商
|
||||
String providerCode = chatModelVo.getProviderCode();
|
||||
log.info("跨模块调用 - 路由到服务提供商: {}, 模型: {}", providerCode, chatRequest.getModel());
|
||||
AbstractChatService chatService = chatServiceFactory.getOriginalService(providerCode);
|
||||
|
||||
// 4. 获取用户信息
|
||||
Long userId = LoginHelper.getUserId();
|
||||
String tokenValue = StpUtil.getTokenValue();
|
||||
|
||||
// 5. 建立 SSE 连接(用于前端监听)
|
||||
sseEmitterManager.connect(userId, tokenValue);
|
||||
|
||||
// 保存用户消息
|
||||
chatMessageService.saveChatMessage(userId, chatRequest.getSessionId(), chatRequest.getContent(), RoleType.USER.getName(), chatRequest.getModel());
|
||||
|
||||
// 6. 创建组合 handler:同时发送到 SSE 和外部 handler
|
||||
StreamingChatResponseHandler combinedHandler = createCombinedHandler(userId, tokenValue, externalHandler);
|
||||
|
||||
// 7. 发起对话
|
||||
StreamingChatModel streamingChatModel = chatService.buildStreamingChatModel(chatModelVo, chatRequest);
|
||||
streamingChatModel.chat(chatRequest.getContent(), combinedHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现接口默认方法 - 不带 handler 的调用
|
||||
*/
|
||||
@Override
|
||||
public SseEmitter chat(ChatRequest chatRequest) {
|
||||
return sseChat(chatRequest);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建或获取聊天内存实例(缓存机制)
|
||||
* 同一个会话ID会返回同一个内存实例,避免重复创建和消息丢失
|
||||
*
|
||||
* @param memoryId 内存ID(会话ID)
|
||||
* @return MessageWindowChatMemory实例
|
||||
*/
|
||||
private MessageWindowChatMemory createChatMemory(Object memoryId) {
|
||||
// 先从缓存中获取
|
||||
return memoryCache.computeIfAbsent(memoryId, key -> {
|
||||
try {
|
||||
PersistentChatMemoryStore store = new PersistentChatMemoryStore(chatMessageService);
|
||||
return MessageWindowChatMemory.builder()
|
||||
.id(memoryId)
|
||||
.maxMessages(DEFAULT_MAX_MESSAGES)
|
||||
.chatMemoryStore(store)
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.warn("创建聊天内存失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 构建上下文消息列表
|
||||
*
|
||||
* @param chatRequest 聊天请求
|
||||
* @return 上下文消息列表
|
||||
*/
|
||||
private List<ChatMessage> buildContextMessages(ChatRequest chatRequest) {
|
||||
List<ChatMessage> messages = new ArrayList<>();
|
||||
// 构建用户消息
|
||||
UserMessage userMessage = UserMessage.userMessage(chatRequest.getContent());
|
||||
messages.add(userMessage);
|
||||
|
||||
// 从向量库查询相关历史消息
|
||||
if (chatRequest.getKnowledgeId() != null) {
|
||||
// 查询知识库信息
|
||||
KnowledgeInfoVo knowledgeInfoVo = knowledgeInfoService.queryById(Long.valueOf(chatRequest.getKnowledgeId()));
|
||||
if (knowledgeInfoVo == null) {
|
||||
log.warn("知识库信息不存在,kid: {}", chatRequest.getKnowledgeId());
|
||||
return messages;
|
||||
}
|
||||
|
||||
// 查询向量模型配置信息
|
||||
ChatModelVo chatModel = chatModelService.selectModelByName(knowledgeInfoVo.getEmbeddingModel());
|
||||
if (chatModel == null) {
|
||||
log.warn("向量模型配置不存在,模型名称: {}", knowledgeInfoVo.getEmbeddingModel());
|
||||
return messages;
|
||||
}
|
||||
|
||||
// 构建向量查询参数
|
||||
QueryVectorBo queryVectorBo = buildQueryVectorBo(chatRequest, knowledgeInfoVo, chatModel);
|
||||
|
||||
// 获取向量查询结果
|
||||
List<String> nearestList = vectorStoreService.getQueryVector(queryVectorBo);
|
||||
for (String prompt : nearestList) {
|
||||
// 知识库内容作为系统上下文添加
|
||||
messages.add( new AiMessage(prompt));
|
||||
}
|
||||
}
|
||||
|
||||
// 从数据库查询历史对话消息
|
||||
if (chatRequest.getSessionId() != null) {
|
||||
MessageWindowChatMemory memory = createChatMemory(chatRequest.getSessionId());
|
||||
if (memory != null) {
|
||||
List<ChatMessage> historicalMessages = memory.messages();
|
||||
if (historicalMessages != null && !historicalMessages.isEmpty()) {
|
||||
messages.addAll(historicalMessages);
|
||||
log.debug("已加载 {} 条历史消息用于会话 {}", historicalMessages.size(), chatRequest.getSessionId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建向量查询参数
|
||||
*/
|
||||
private QueryVectorBo buildQueryVectorBo(ChatRequest chatRequest, KnowledgeInfoVo knowledgeInfoVo,
|
||||
ChatModelVo chatModel) {
|
||||
QueryVectorBo queryVectorBo = new QueryVectorBo();
|
||||
queryVectorBo.setQuery(chatRequest.getContent());
|
||||
queryVectorBo.setKid(chatRequest.getKnowledgeId());
|
||||
queryVectorBo.setApiKey(chatModel.getApiKey());
|
||||
queryVectorBo.setBaseUrl(chatModel.getApiHost());
|
||||
queryVectorBo.setVectorModelName(knowledgeInfoVo.getVectorModel());
|
||||
queryVectorBo.setEmbeddingModelName(knowledgeInfoVo.getEmbeddingModel());
|
||||
queryVectorBo.setMaxResults(knowledgeInfoVo.getRetrieveLimit());
|
||||
return queryVectorBo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建标准的响应处理器
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param tokenValue 会话令牌
|
||||
* @return 标准的流式响应处理器
|
||||
*/
|
||||
protected StreamingChatResponseHandler createResponseHandler(Long userId, String tokenValue,ChatRequest chatRequest) {
|
||||
return new StreamingChatResponseHandler() {
|
||||
|
||||
private final StringBuilder messageBuffer = new StringBuilder();
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public void onPartialResponse(String partialResponse) {
|
||||
// 将消息片段追加到缓冲区
|
||||
messageBuffer.append(partialResponse);
|
||||
|
||||
// 实时发送内容事件到客户端
|
||||
SseMessageUtils.sendContent(userId, partialResponse);
|
||||
log.debug("收到消息片段: {}", partialResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleteResponse(ChatResponse completeResponse) {
|
||||
try {
|
||||
// 发送完成事件
|
||||
SseMessageUtils.sendDone(userId);
|
||||
|
||||
// 消息流完成,保存消息到数据库和内存
|
||||
String fullMessage = messageBuffer.toString();
|
||||
|
||||
if (fullMessage.isEmpty()) {
|
||||
log.warn("接收到空消息");
|
||||
} else {
|
||||
// 保存助手回复消息
|
||||
chatMessageService.saveChatMessage(userId, chatRequest.getSessionId(), fullMessage, RoleType.ASSISTANT.getName(), chatRequest.getModel());
|
||||
}
|
||||
|
||||
// 关闭SSE连接
|
||||
SseMessageUtils.completeConnection(userId, tokenValue);
|
||||
log.info("消息结束,已保存到数据库");
|
||||
} catch (Exception e) {
|
||||
log.error("完成响应时出错: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
// 发送错误事件
|
||||
SseMessageUtils.sendError(userId, error.getMessage());
|
||||
log.error("流式响应错误: {}", error.getMessage());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建组合响应处理器 - 同时发送到 SSE 和外部 handler
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param tokenValue 会话令牌
|
||||
* @param externalHandler 外部响应处理器(可为 null)
|
||||
* @return 组合的流式响应处理器
|
||||
*/
|
||||
protected StreamingChatResponseHandler createCombinedHandler(Long userId, String tokenValue,
|
||||
StreamingChatResponseHandler externalHandler) {
|
||||
return new StreamingChatResponseHandler() {
|
||||
|
||||
private final StringBuilder messageBuffer = new StringBuilder();
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public void onPartialResponse(String partialResponse) {
|
||||
// 1. 追加到缓冲区
|
||||
messageBuffer.append(partialResponse);
|
||||
|
||||
// 2. 发送内容事件到 SSE(前端可通过 SSE 监听)
|
||||
SseMessageUtils.sendContent(userId, partialResponse);
|
||||
|
||||
// 3. 转发给外部 handler(Workflow 等模块可处理)
|
||||
if (externalHandler != null) {
|
||||
externalHandler.onPartialResponse(partialResponse);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleteResponse(ChatResponse completeResponse) {
|
||||
try {
|
||||
// 1. 发送完成事件
|
||||
SseMessageUtils.sendDone(userId);
|
||||
|
||||
// 2. 关闭 SSE 连接
|
||||
SseMessageUtils.completeConnection(userId, tokenValue);
|
||||
|
||||
// 3. 转发给外部 handler
|
||||
if (externalHandler != null) {
|
||||
externalHandler.onCompleteResponse(completeResponse);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("完成响应时出错: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
// 发送错误事件
|
||||
SseMessageUtils.sendError(userId, error.getMessage());
|
||||
log.error("流式响应错误: {}", error.getMessage(), error);
|
||||
|
||||
// 转发给外部 handler
|
||||
if (externalHandler != null) {
|
||||
externalHandler.onError(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
package org.ruoyi.service.chat.impl.memory;
|
||||
|
||||
import dev.langchain4j.memory.chat.ChatMemoryProvider;
|
||||
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 聊天记忆提供者工厂
|
||||
* 为每个会话创建独立的ChatMemoryProvider实例
|
||||
* 支持消息窗口滑动和持久化存储
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/01/10
|
||||
*/
|
||||
@Slf4j
|
||||
public class ChatMemoryProviderFactory {
|
||||
|
||||
/**
|
||||
* 默认保留的消息数量(不包括当前消息)
|
||||
*/
|
||||
private static final int DEFAULT_MAX_MESSAGES = 20;
|
||||
|
||||
/**
|
||||
* 创建聊天记忆提供者
|
||||
*
|
||||
* @param maxMessages 最多保留的消息数量
|
||||
* @return ChatMemoryProvider实例
|
||||
*/
|
||||
public static ChatMemoryProvider createChatMemoryProvider(int maxMessages) {
|
||||
PersistentChatMemoryStore store = new PersistentChatMemoryStore();
|
||||
|
||||
return memoryId -> MessageWindowChatMemory.builder()
|
||||
.id(memoryId)
|
||||
.maxMessages(maxMessages)
|
||||
.chatMemoryStore(store)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用默认消息数量创建聊天记忆提供者
|
||||
*
|
||||
* @return ChatMemoryProvider实例
|
||||
*/
|
||||
public static ChatMemoryProvider createChatMemoryProvider() {
|
||||
return createChatMemoryProvider(DEFAULT_MAX_MESSAGES);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建自定义的聊天记忆提供者
|
||||
* 允许更灵活的配置
|
||||
*
|
||||
* @param maxMessages 最多保留的消息数量
|
||||
* @param chatMemoryStore 自定义的存储实现
|
||||
* @return ChatMemoryProvider实例
|
||||
*/
|
||||
public static ChatMemoryProvider createChatMemoryProvider(int maxMessages,
|
||||
dev.langchain4j.store.memory.chat.ChatMemoryStore chatMemoryStore) {
|
||||
return memoryId -> MessageWindowChatMemory.builder()
|
||||
.id(memoryId)
|
||||
.maxMessages(maxMessages)
|
||||
.chatMemoryStore(chatMemoryStore)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,9 @@ package org.ruoyi.service.chat.impl.memory;
|
||||
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.domain.dto.ChatMessageDTO;
|
||||
import org.ruoyi.common.chat.service.chatMessage.IChatMessageService;
|
||||
import org.ruoyi.common.core.utils.SpringUtils;
|
||||
import org.ruoyi.service.chat.IChatMessageService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -18,13 +17,11 @@ import java.util.List;
|
||||
* @date 2025/01/10
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class PersistentChatMemoryStore implements ChatMemoryStore {
|
||||
|
||||
private final IChatMessageService chatMessageService;
|
||||
|
||||
public PersistentChatMemoryStore() {
|
||||
this.chatMessageService = SpringUtils.getBean(IChatMessageService.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据会话ID获取历史消息
|
||||
@@ -38,16 +35,8 @@ public class PersistentChatMemoryStore implements ChatMemoryStore {
|
||||
}
|
||||
|
||||
Long sessionId = Long.parseLong(memoryId.toString());
|
||||
|
||||
// 从数据库获取该会话的所有消息
|
||||
List<ChatMessageDTO> dtoList = chatMessageService.getMessagesBySessionId(sessionId);
|
||||
|
||||
if (dtoList == null || dtoList.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 转换为LangChain4j格式
|
||||
return convertToLangChainMessages(dtoList);
|
||||
return chatMessageService.getMessagesBySessionId(sessionId);
|
||||
} catch (Exception e) {
|
||||
log.error("获取会话 {} 的消息失败: {}", memoryId, e.getMessage(), e);
|
||||
return new ArrayList<>();
|
||||
@@ -89,19 +78,5 @@ public class PersistentChatMemoryStore implements ChatMemoryStore {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将ChatMessageDTO列表转换为LangChain4j的ChatMessage列表
|
||||
*/
|
||||
private List<ChatMessage> convertToLangChainMessages(List<ChatMessageDTO> dtoList) {
|
||||
List<ChatMessage> messages = new ArrayList<>();
|
||||
for (ChatMessageDTO dto : dtoList) {
|
||||
ChatMessage message = switch (dto.getRole()) {
|
||||
case "system" -> dev.langchain4j.data.message.SystemMessage.from(dto.getContent());
|
||||
case "assistant" -> dev.langchain4j.data.message.AiMessage.from(dto.getContent());
|
||||
default -> dev.langchain4j.data.message.UserMessage.from(dto.getContent());
|
||||
};
|
||||
messages.add(message);
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
package org.ruoyi.service.chat.impl.provider;
|
||||
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.enums.ChatModeType;
|
||||
import org.ruoyi.service.chat.impl.AbstractStreamingChatService;
|
||||
import org.ruoyi.service.chat.AbstractChatService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author: xiaoen
|
||||
@@ -20,7 +17,7 @@ import java.util.List;
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class DeepseekServiceImpl extends AbstractStreamingChatService {
|
||||
public class DeepseekServiceImpl implements AbstractChatService {
|
||||
|
||||
@Override
|
||||
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
||||
@@ -32,11 +29,6 @@ public class DeepseekServiceImpl extends AbstractStreamingChatService {
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List<ChatMessage> messagesWithMemory, StreamingChatResponseHandler handler) {
|
||||
StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo, chatRequest);
|
||||
streamingChatModel.chat(messagesWithMemory, handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProviderName() {
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
package org.ruoyi.service.chat.impl.provider;
|
||||
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import dev.langchain4j.model.ollama.OllamaStreamingChatModel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.enums.ChatModeType;
|
||||
import org.ruoyi.service.chat.impl.AbstractStreamingChatService;
|
||||
import org.ruoyi.service.chat.AbstractChatService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* OllamaAI服务调用
|
||||
*
|
||||
@@ -21,7 +17,7 @@ import java.util.List;
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class OllamaServiceImpl extends AbstractStreamingChatService {
|
||||
public class OllamaServiceImpl implements AbstractChatService {
|
||||
|
||||
@Override
|
||||
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
||||
@@ -31,16 +27,6 @@ public class OllamaServiceImpl extends AbstractStreamingChatService {
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List<ChatMessage> messagesWithMemory,StreamingChatResponseHandler handler) {
|
||||
StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo, chatRequest);
|
||||
streamingChatModel.chat(messagesWithMemory, handler);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public String getProviderName() {
|
||||
return ChatModeType.OLLAMA.getCode();
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
package org.ruoyi.service.chat.impl.provider;
|
||||
|
||||
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.enums.ChatModeType;
|
||||
import org.ruoyi.service.chat.impl.AbstractStreamingChatService;
|
||||
import org.ruoyi.service.chat.AbstractChatService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* OPENAI服务调用
|
||||
@@ -23,7 +19,7 @@ import java.util.List;
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class OpenAIServiceImpl extends AbstractStreamingChatService {
|
||||
public class OpenAIServiceImpl implements AbstractChatService {
|
||||
|
||||
@Override
|
||||
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
||||
@@ -35,13 +31,6 @@ public class OpenAIServiceImpl extends AbstractStreamingChatService {
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List<ChatMessage> messagesWithMemory, StreamingChatResponseHandler handler) {
|
||||
StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo, chatRequest);
|
||||
streamingChatModel.chat(messagesWithMemory, handler);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getProviderName() {
|
||||
return ChatModeType.OPEN_AI.getCode();
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
package org.ruoyi.service.chat.impl.provider;
|
||||
|
||||
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.enums.ChatModeType;
|
||||
import org.ruoyi.service.chat.impl.AbstractStreamingChatService;
|
||||
import org.ruoyi.service.chat.AbstractChatService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* OPENAI服务调用
|
||||
*
|
||||
@@ -23,7 +17,7 @@ import java.util.List;
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class PPIOServiceImpl extends AbstractStreamingChatService {
|
||||
public class PPIOServiceImpl implements AbstractChatService {
|
||||
|
||||
@Override
|
||||
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
||||
@@ -35,13 +29,6 @@ public class PPIOServiceImpl extends AbstractStreamingChatService {
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List<ChatMessage> messagesWithMemory, StreamingChatResponseHandler handler) {
|
||||
StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo, chatRequest);
|
||||
streamingChatModel.chat(messagesWithMemory, handler);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getProviderName() {
|
||||
return ChatModeType.PPIO.getCode();
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
package org.ruoyi.service.chat.impl.provider;
|
||||
|
||||
import dev.langchain4j.community.model.dashscope.QwenStreamingChatModel;
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.data.message.SystemMessage;
|
||||
import dev.langchain4j.data.message.UserMessage;
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.enums.ChatModeType;
|
||||
import org.ruoyi.service.chat.impl.AbstractStreamingChatService;
|
||||
import org.ruoyi.service.chat.AbstractChatService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* qianWenAI服务调用
|
||||
@@ -25,11 +18,9 @@ import java.util.List;
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class QianWenChatServiceImpl extends AbstractStreamingChatService {
|
||||
public class QianWenChatServiceImpl implements AbstractChatService {
|
||||
|
||||
// 添加文档解析的前缀字段
|
||||
private static final String UPLOAD_FILE_API_PREFIX = "fileid";
|
||||
|
||||
@Override
|
||||
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo,ChatRequest chatRequest) {
|
||||
return QwenStreamingChatModel.builder()
|
||||
@@ -38,47 +29,6 @@ public class QianWenChatServiceImpl extends AbstractStreamingChatService {
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doChat(ChatModelVo chatModelVo,ChatRequest chatRequest,List<ChatMessage> messagesWithMemory,
|
||||
StreamingChatResponseHandler handler) {
|
||||
StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo,chatRequest);
|
||||
// 判断是否存在需要使用阿里千问的文档解析功能
|
||||
List<ChatMessage> chatMessages = hasFileIdData(messagesWithMemory);
|
||||
streamingChatModel.chat(chatMessages, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否包含fileId数据
|
||||
*/
|
||||
private List<ChatMessage> hasFileIdData(List<ChatMessage> messagesWithMemory) {
|
||||
if (CollectionUtils.isEmpty(messagesWithMemory)) {
|
||||
return messagesWithMemory;
|
||||
}
|
||||
|
||||
// 找到包含阿里上传文件前缀的用户信息
|
||||
var foundUserMessage = messagesWithMemory.stream()
|
||||
.filter(message -> message instanceof UserMessage)
|
||||
.map(message -> (UserMessage) message)
|
||||
.filter(userMessage ->
|
||||
userMessage.singleText().toLowerCase().contains(UPLOAD_FILE_API_PREFIX.toLowerCase())
|
||||
)
|
||||
.findFirst();
|
||||
|
||||
// 找到原本SystemMessage
|
||||
var systemMessage = messagesWithMemory.stream()
|
||||
.filter(message -> message instanceof SystemMessage)
|
||||
.map(message -> (SystemMessage) message)
|
||||
.findFirst();
|
||||
|
||||
// 判断是否存在并重新构建信息体(符合千问文档解析格式)
|
||||
return foundUserMessage.map(userMsg -> {
|
||||
List<ChatMessage> messages = new ArrayList<>();
|
||||
messages.add(new SystemMessage(userMsg.singleText()));
|
||||
systemMessage.ifPresent(sysMsg -> messages.add(new UserMessage(sysMsg.text())));
|
||||
return messages;
|
||||
}).orElse(messagesWithMemory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProviderName() {
|
||||
return ChatModeType.QIAN_WEN.getCode();
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
package org.ruoyi.service.chat.impl.provider;
|
||||
|
||||
import dev.langchain4j.community.model.zhipu.ZhipuAiStreamingChatModel;
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.enums.ChatModeType;
|
||||
import org.ruoyi.service.chat.impl.AbstractStreamingChatService;
|
||||
import org.ruoyi.service.chat.AbstractChatService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 智谱AI服务调用
|
||||
@@ -21,12 +18,7 @@ import java.util.List;
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class ZhiPuChatServiceImpl extends AbstractStreamingChatService {
|
||||
@Override
|
||||
public void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List<ChatMessage> messagesWithMemory, StreamingChatResponseHandler handler) {
|
||||
StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo,chatRequest);
|
||||
streamingChatModel.chat(messagesWithMemory, handler);
|
||||
}
|
||||
public class ZhiPuChatServiceImpl implements AbstractChatService {
|
||||
|
||||
@Override
|
||||
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
||||
|
||||
Reference in New Issue
Block a user