mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-04-08 17:27:31 +00: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
|
driverClassName: com.mysql.cj.jdbc.Driver
|
||||||
# jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
|
# jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
|
||||||
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
|
# 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
|
username: root
|
||||||
password: root
|
password: root
|
||||||
# agent:
|
# 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;
|
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 jakarta.validation.constraints.NotEmpty;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.ruoyi.common.chat.domain.dto.ChatMessageDTO;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对话请求对象
|
* 对话请求对象
|
||||||
@@ -16,11 +16,15 @@ import java.util.List;
|
|||||||
@Data
|
@Data
|
||||||
public class ChatRequest {
|
public class ChatRequest {
|
||||||
|
|
||||||
@NotEmpty(message = "对话消息不能为空")
|
|
||||||
private List<ChatMessageDTO> messages;
|
|
||||||
@NotEmpty(message = "传入的模型不能为空")
|
@NotEmpty(message = "传入的模型不能为空")
|
||||||
private String model;
|
private String model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对话消息
|
||||||
|
*/
|
||||||
|
@NotEmpty(message = "对话消息不能为空")
|
||||||
|
private String content;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工作流请求体
|
* 工作流请求体
|
||||||
*/
|
*/
|
||||||
@@ -31,59 +35,49 @@ public class ChatRequest {
|
|||||||
*/
|
*/
|
||||||
private ReSumeRunner reSumeRunner;
|
private ReSumeRunner reSumeRunner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为人机交互用户继续输入
|
||||||
|
*/
|
||||||
|
private Boolean isResume = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否启用工作流
|
* 是否启用工作流
|
||||||
*/
|
*/
|
||||||
private Boolean enableWorkFlow;
|
private Boolean enableWorkFlow = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 会话id
|
* 会话id
|
||||||
*/
|
*/
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
|
@JSONField(serializeUsing = String.class)
|
||||||
private Long sessionId;
|
private Long sessionId;
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用ID
|
|
||||||
*/
|
|
||||||
private String appId;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 知识库id
|
* 知识库id
|
||||||
*/
|
*/
|
||||||
private String knowledgeId;
|
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 enableThinking = false;
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否自动切换模型
|
|
||||||
*/
|
|
||||||
private Boolean autoSelectModel;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否支持联网
|
* 是否支持联网
|
||||||
*/
|
*/
|
||||||
private Boolean enableInternet;
|
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 io.github.linpeilie.annotations.AutoMapper;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.ruoyi.common.chat.entity.chat.ChatMessage;
|
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.Serial;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import com.baomidou.mybatisplus.annotation.TableId;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class BaseEntity implements Serializable {
|
public class BaseEntity implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
@TableId(type = IdType.AUTO)
|
@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;
|
package org.ruoyi.common.chat.service.chat;
|
||||||
|
|
||||||
|
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||||
import jakarta.validation.Valid;
|
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;
|
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.collection.CollUtil;
|
||||||
import cn.hutool.core.map.MapUtil;
|
import cn.hutool.core.map.MapUtil;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.common.core.utils.SpringUtils;
|
import org.ruoyi.common.core.utils.SpringUtils;
|
||||||
import org.ruoyi.common.redis.utils.RedisUtils;
|
import org.ruoyi.common.redis.utils.RedisUtils;
|
||||||
|
import org.ruoyi.common.sse.dto.SseEventDto;
|
||||||
import org.ruoyi.common.sse.dto.SseMessageDto;
|
import org.ruoyi.common.sse.dto.SseMessageDto;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
@@ -65,7 +67,7 @@ public class SseEmitterManager {
|
|||||||
emitter.onCompletion(() -> {
|
emitter.onCompletion(() -> {
|
||||||
SseEmitter remove = emitters.remove(token);
|
SseEmitter remove = emitters.remove(token);
|
||||||
if (remove != null) {
|
if (remove != null) {
|
||||||
// remove.complete();
|
remove.complete();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
emitter.onTimeout(() -> {
|
emitter.onTimeout(() -> {
|
||||||
@@ -174,9 +176,11 @@ public class SseEmitterManager {
|
|||||||
if (MapUtil.isNotEmpty(emitters)) {
|
if (MapUtil.isNotEmpty(emitters)) {
|
||||||
for (Map.Entry<String, SseEmitter> entry : emitters.entrySet()) {
|
for (Map.Entry<String, SseEmitter> entry : emitters.entrySet()) {
|
||||||
try {
|
try {
|
||||||
|
// 格式化为标准SSE JSON格式
|
||||||
|
SseEventDto eventDto = SseEventDto.content(message);
|
||||||
entry.getValue().send(SseEmitter.event()
|
entry.getValue().send(SseEmitter.event()
|
||||||
.name("message")
|
.name("message")
|
||||||
.data(message));
|
.data(JSONUtil.toJsonStr(eventDto)));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
SseEmitter remove = emitters.remove(entry.getKey());
|
SseEmitter remove = emitters.remove(entry.getKey());
|
||||||
if (remove != null) {
|
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;
|
package org.ruoyi.common.sse.utils;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.common.core.utils.SpringUtils;
|
import org.ruoyi.common.core.utils.SpringUtils;
|
||||||
import org.ruoyi.common.sse.core.SseEmitterManager;
|
import org.ruoyi.common.sse.core.SseEmitterManager;
|
||||||
|
import org.ruoyi.common.sse.dto.SseEventDto;
|
||||||
import org.ruoyi.common.sse.dto.SseMessageDto;
|
import org.ruoyi.common.sse.dto.SseMessageDto;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,6 +29,7 @@ public class SseMessageUtils {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 向指定的SSE会话发送消息
|
* 向指定的SSE会话发送消息
|
||||||
|
* 通过 Redis Pub/Sub 广播,确保跨模块消息可达
|
||||||
*
|
*
|
||||||
* @param userId 要发送消息的用户id
|
* @param userId 要发送消息的用户id
|
||||||
* @param message 要发送的消息内容
|
* @param message 要发送的消息内容
|
||||||
@@ -35,7 +38,11 @@ public class SseMessageUtils {
|
|||||||
if (!isEnable()) {
|
if (!isEnable()) {
|
||||||
return;
|
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);
|
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 chatRequest = new ChatRequest();
|
||||||
chatRequest.setSessionId(sessionId);
|
chatRequest.setSessionId(sessionId);
|
||||||
WorkflowUtil workflowUtil = SpringUtils.getBean(WorkflowUtil.class);
|
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.collection.CollUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import dev.langchain4j.data.message.ChatMessage;
|
import dev.langchain4j.data.message.ChatMessage;
|
||||||
import dev.langchain4j.data.message.SystemMessage;
|
|
||||||
import dev.langchain4j.data.message.UserMessage;
|
import dev.langchain4j.data.message.UserMessage;
|
||||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.bsc.langgraph4j.langchain4j.generators.StreamingChatGenerator;
|
import org.bsc.langgraph4j.langchain4j.generators.StreamingChatGenerator;
|
||||||
import org.bsc.langgraph4j.state.AgentState;
|
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.IChatModelService;
|
||||||
import org.ruoyi.common.chat.service.chat.IChatService;
|
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.service.image.IImageGenerationService;
|
||||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
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.entity.image.ImageContext;
|
||||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
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.common.chat.factory.ImageServiceFactory;
|
||||||
import org.ruoyi.workflow.base.NodeInputConfigTypeHandler;
|
import org.ruoyi.workflow.base.NodeInputConfigTypeHandler;
|
||||||
import org.ruoyi.workflow.entity.WorkflowNode;
|
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.NodeIOData;
|
||||||
import org.ruoyi.workflow.workflow.data.NodeIODataContent;
|
import org.ruoyi.workflow.workflow.data.NodeIODataContent;
|
||||||
import org.ruoyi.workflow.workflow.def.WfNodeParamRef;
|
import org.ruoyi.workflow.workflow.def.WfNodeParamRef;
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@@ -39,10 +32,7 @@ import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.DEFAULT_O
|
|||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class WorkflowUtil extends AbstractChatMessageService {
|
public class WorkflowUtil{
|
||||||
|
|
||||||
@Resource
|
|
||||||
private ChatServiceFactory chatServiceFactory;
|
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private ImageServiceFactory imageServiceFactory;
|
private ImageServiceFactory imageServiceFactory;
|
||||||
@@ -50,6 +40,9 @@ public class WorkflowUtil extends AbstractChatMessageService {
|
|||||||
@Resource
|
@Resource
|
||||||
private IChatModelService chatModelService;
|
private IChatModelService chatModelService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IChatService chatService;
|
||||||
|
|
||||||
public static String renderTemplate(String template, List<NodeIOData> values) {
|
public static String renderTemplate(String template, List<NodeIOData> values) {
|
||||||
// 🔒 关键修复:如果 template 为 null,直接返回 null 或空字符串
|
// 🔒 关键修复:如果 template 为 null,直接返回 null 或空字符串
|
||||||
if (template == null) {
|
if (template == null) {
|
||||||
@@ -112,54 +105,23 @@ public class WorkflowUtil extends AbstractChatMessageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void streamingInvokeLLM(WfState wfState, WfNodeState state, WorkflowNode node, String modelName,
|
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);
|
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连接对象(对话接口需要使用)
|
// 获取用户信息和Token以及SSe连接对象(对话接口需要使用)
|
||||||
Long sessionId = wfState.getSessionId();
|
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 chatRequest = new ChatRequest();
|
||||||
// 目前工作流深度思考成员变量只能写死
|
|
||||||
chatRequest.setSessionId(sessionId);
|
chatRequest.setSessionId(sessionId);
|
||||||
chatRequest.setEnableThinking(false);
|
chatRequest.setEnableThinking(false);
|
||||||
chatRequest.setModel(modelName);
|
chatRequest.setModel(modelName);
|
||||||
chatRequest.setChatMessages(chatMessages);
|
chatRequest.setContent(prompt);
|
||||||
|
|
||||||
// 构建流式生成器
|
// 构建流式生成器
|
||||||
StreamingChatGenerator<AgentState> streamingGenerator = StreamingChatGenerator.builder()
|
StreamingChatGenerator<AgentState> streamingGenerator = StreamingChatGenerator.builder()
|
||||||
.mapResult(response -> {
|
.mapResult(response -> {
|
||||||
String responseTxt = response.aiMessage().text();
|
String responseTxt = response.aiMessage().text();
|
||||||
log.info("llm response:{}", responseTxt);
|
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 输出
|
// 传递所有输入数据 + 添加 LLM 输出
|
||||||
wfState.getNodeStateByNodeUuid(node.getUuid()).ifPresent(item -> {
|
wfState.getNodeStateByNodeUuid(node.getUuid()).ifPresent(item -> {
|
||||||
List<NodeIOData> outputs = new ArrayList<>(item.getInputs());
|
List<NodeIOData> outputs = new ArrayList<>(item.getInputs());
|
||||||
@@ -174,21 +136,13 @@ public class WorkflowUtil extends AbstractChatMessageService {
|
|||||||
.startingState(state)
|
.startingState(state)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// 构建流式回调响应器
|
// 获取 StreamingChatGenerator 的 handler,用于处理流式响应
|
||||||
StreamingChatResponseHandler handler = streamingGenerator.handler();
|
StreamingChatResponseHandler workflowHandler = streamingGenerator.handler();
|
||||||
|
|
||||||
//构建聊天对话上下文参数
|
// 调用 Chat 服务,传入 workflow 的 handler
|
||||||
ChatContext chatContext = ChatContext.builder()
|
// 消息会同时发送到 SSE(前端)和 workflowHandler(工作流处理)
|
||||||
.chatModelVo(chatModelVo)
|
chatService.chat(chatRequest, workflowHandler);
|
||||||
.chatRequest(chatRequest)
|
|
||||||
.emitter(sseEmitter)
|
|
||||||
.userId(userId)
|
|
||||||
.tokenValue(tokenValue)
|
|
||||||
.handler(handler)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// 使用工作流专用方法
|
|
||||||
chatService.chat(chatContext);
|
|
||||||
wfState.getNodeToStreamingGenerator().put(node.getUuid(), streamingGenerator);
|
wfState.getNodeToStreamingGenerator().put(node.getUuid(), streamingGenerator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,13 +46,11 @@ public class LLMAnswerNode extends AbstractWfNode {
|
|||||||
// 调用LLM
|
// 调用LLM
|
||||||
WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class);
|
WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class);
|
||||||
String modelName = nodeConfigObj.getModelName();
|
String modelName = nodeConfigObj.getModelName();
|
||||||
// 转换系统信息结构
|
|
||||||
List<SystemMessage> systemMessage = List.of(new SystemMessage(prompt));
|
|
||||||
// 获取节点模板提示词信息
|
// 获取节点模板提示词信息
|
||||||
String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.LLM_RESPONSE.getValue());
|
String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.LLM_RESPONSE.getValue());
|
||||||
// 发送SSE驱动事件消息
|
// 发送SSE驱动事件消息
|
||||||
WorkflowMessageUtil.sendEmitterMessage(wfState.getSseEmitter(), node, nodeMessageTemplate);
|
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();
|
return new NodeProcessResult();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,13 +67,12 @@ public class KeywordExtractorNode extends AbstractWfNode {
|
|||||||
// 调用 LLM 进行关键词提取
|
// 调用 LLM 进行关键词提取
|
||||||
WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class);
|
WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class);
|
||||||
String modelName = config.getModelName();
|
String modelName = config.getModelName();
|
||||||
List<SystemMessage> systemMessage = List.of(new SystemMessage(prompt));
|
|
||||||
// 获取节点模板提示词信息
|
// 获取节点模板提示词信息
|
||||||
String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.KEYWORD_EXTRACTOR.getValue());
|
String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.KEYWORD_EXTRACTOR.getValue());
|
||||||
// 发送SSE事件消息
|
// 发送SSE事件消息
|
||||||
WorkflowMessageUtil.sendEmitterMessage(wfState.getSseEmitter(), node, nodeMessageTemplate);
|
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();
|
return new NodeProcessResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -151,7 +151,6 @@ public class KnowledgeRetrievalNode extends AbstractWfNode {
|
|||||||
|
|
||||||
// 使用WorkflowUtil调用LLM(流式)
|
// 使用WorkflowUtil调用LLM(流式)
|
||||||
WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class);
|
WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class);
|
||||||
List<SystemMessage> systemMessage = List.of(new SystemMessage(prompt));
|
|
||||||
|
|
||||||
// 调用流式LLM
|
// 调用流式LLM
|
||||||
String modelName = StringUtils.isNotBlank(config.getModelName()) ? config.getModelName() : "deepseek-chat";
|
String modelName = StringUtils.isNotBlank(config.getModelName()) ? config.getModelName() : "deepseek-chat";
|
||||||
@@ -161,7 +160,7 @@ public class KnowledgeRetrievalNode extends AbstractWfNode {
|
|||||||
tempState,
|
tempState,
|
||||||
tempNode,
|
tempNode,
|
||||||
modelName,
|
modelName,
|
||||||
systemMessage,
|
prompt,
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,11 @@
|
|||||||
<artifactId>ruoyi-common-chat</artifactId>
|
<artifactId>ruoyi-common-chat</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ruoyi</groupId>
|
||||||
|
<artifactId>ruoyi-common-sse</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.ruoyi</groupId>
|
<groupId>org.ruoyi</groupId>
|
||||||
<artifactId>ruoyi-common-sensitive</artifactId>
|
<artifactId>ruoyi-common-sensitive</artifactId>
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ public class ChatController {
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/send")
|
@PostMapping("/send")
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
public SseEmitter sseChat(@RequestBody @Valid ChatRequest chatRequest, HttpServletRequest request) {
|
public SseEmitter sseChat(@RequestBody @Valid ChatRequest chatRequest) {
|
||||||
return chatService.sseChat(chatRequest,request);
|
return chatService.sseChat(chatRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import jakarta.validation.constraints.*;
|
|||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import org.ruoyi.common.chat.domain.bo.chat.ChatMessageBo;
|
import org.ruoyi.common.chat.domain.bo.chat.ChatMessageBo;
|
||||||
import org.ruoyi.common.chat.domain.vo.chat.ChatMessageVo;
|
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.web.bind.annotation.*;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.ruoyi.common.idempotent.annotation.RepeatSubmit;
|
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.common.chat.service.chat.IChatService;
|
||||||
|
import org.ruoyi.service.chat.AbstractChatService;
|
||||||
import org.springframework.beans.BeansException;
|
import org.springframework.beans.BeansException;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.context.ApplicationContextAware;
|
import org.springframework.context.ApplicationContextAware;
|
||||||
@@ -18,13 +19,13 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
@Component
|
@Component
|
||||||
public class ChatServiceFactory implements ApplicationContextAware {
|
public class ChatServiceFactory implements ApplicationContextAware {
|
||||||
|
|
||||||
private final Map<String, IChatService> chatServiceMap = new ConcurrentHashMap<>();
|
private final Map<String, AbstractChatService> chatServiceMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||||
// 初始化时收集所有IChatService的实现
|
// 初始化时收集所有IChatService的实现
|
||||||
Map<String, IChatService> serviceMap = applicationContext.getBeansOfType(IChatService.class);
|
Map<String, AbstractChatService> serviceMap = applicationContext.getBeansOfType(AbstractChatService.class);
|
||||||
for (IChatService service : serviceMap.values()) {
|
for (AbstractChatService service : serviceMap.values()) {
|
||||||
if (service != null ) {
|
if (service != null ) {
|
||||||
chatServiceMap.put(service.getProviderName(), service);
|
chatServiceMap.put(service.getProviderName(), service);
|
||||||
}
|
}
|
||||||
@@ -35,8 +36,8 @@ public class ChatServiceFactory implements ApplicationContextAware {
|
|||||||
/**
|
/**
|
||||||
* 获取原始服务(不包装代理)
|
* 获取原始服务(不包装代理)
|
||||||
*/
|
*/
|
||||||
public IChatService getOriginalService(String category) {
|
public AbstractChatService getOriginalService(String category) {
|
||||||
IChatService service = chatServiceMap.get(category);
|
AbstractChatService service = chatServiceMap.get(category);
|
||||||
if (service == null) {
|
if (service == null) {
|
||||||
throw new IllegalArgumentException("不支持的模型类别: " + category);
|
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.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.domain.vo.chat.ChatMessageVo;
|
||||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||||
@@ -74,7 +74,7 @@ public interface IChatMessageService {
|
|||||||
* @param sessionId 会话ID
|
* @param sessionId 会话ID
|
||||||
* @return 消息DTO列表
|
* @return 消息DTO列表
|
||||||
*/
|
*/
|
||||||
List<ChatMessageDTO> getMessagesBySessionId(Long sessionId);
|
List<ChatMessage> getMessagesBySessionId(Long sessionId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据会话ID删除所有消息
|
* 根据会话ID删除所有消息
|
||||||
@@ -84,4 +84,15 @@ public interface IChatMessageService {
|
|||||||
* @return 是否删除成功
|
* @return 是否删除成功
|
||||||
*/
|
*/
|
||||||
Boolean deleteBySessionId(Long sessionId);
|
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;
|
//package org.ruoyi.service.chat.handler;
|
||||||
|
//
|
||||||
import dev.langchain4j.agentic.AgenticServices;
|
//import dev.langchain4j.agentic.AgenticServices;
|
||||||
import dev.langchain4j.community.model.dashscope.QwenChatModel;
|
//import dev.langchain4j.community.model.dashscope.QwenChatModel;
|
||||||
import dev.langchain4j.service.tool.ToolProvider;
|
//import dev.langchain4j.service.tool.ToolProvider;
|
||||||
import lombok.RequiredArgsConstructor;
|
//import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
//import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.agent.McpAgent;
|
//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.domain.vo.chat.ChatModelVo;
|
//import org.ruoyi.common.chat.entity.chat.ChatContext;
|
||||||
import org.ruoyi.common.chat.entity.chat.ChatContext;
|
//import org.ruoyi.common.chat.service.chatMessage.IChatMessageService;
|
||||||
import org.ruoyi.common.chat.enums.RoleType;
|
//import org.ruoyi.common.sse.utils.SseMessageUtils;
|
||||||
import org.ruoyi.common.chat.service.chatMessage.IChatMessageService;
|
//import org.ruoyi.mcp.service.core.ToolProviderFactory;
|
||||||
import org.ruoyi.common.sse.utils.SseMessageUtils;
|
//import org.springframework.core.annotation.Order;
|
||||||
import org.ruoyi.mcp.service.core.ToolProviderFactory;
|
//import org.springframework.stereotype.Component;
|
||||||
import org.springframework.core.annotation.Order;
|
//import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
import org.springframework.stereotype.Component;
|
//
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
//import java.util.ArrayList;
|
||||||
|
//import java.util.List;
|
||||||
import java.util.ArrayList;
|
//
|
||||||
import java.util.List;
|
///**
|
||||||
|
// * Agent 深度思考处理器
|
||||||
/**
|
// * <p>
|
||||||
* Agent 深度思考处理器
|
// * 处理 enableThinking=true 的场景,使用 Agent 进行深度思考和工具调用
|
||||||
* <p>
|
// *
|
||||||
* 处理 enableThinking=true 的场景,使用 Agent 进行深度思考和工具调用
|
// * @author ageerle@163.com
|
||||||
*
|
// * @date 2025/12/13
|
||||||
* @author ageerle@163.com
|
// */
|
||||||
* @date 2025/12/13
|
//@Slf4j
|
||||||
*/
|
//@Component
|
||||||
@Slf4j
|
//@Order(3)
|
||||||
@Component
|
//@RequiredArgsConstructor
|
||||||
@Order(3)
|
//public class AgentChatHandler implements ChatHandler {
|
||||||
@RequiredArgsConstructor
|
//
|
||||||
public class AgentChatHandler implements ChatHandler {
|
// private final ToolProviderFactory toolProviderFactory;
|
||||||
|
//
|
||||||
private final ToolProviderFactory toolProviderFactory;
|
// @Override
|
||||||
private final IChatMessageService chatMessageService;
|
// public boolean supports(ChatContext context) {
|
||||||
|
// Boolean enableThinking = context.getChatRequest().getEnableThinking();
|
||||||
@Override
|
// return enableThinking != null && enableThinking;
|
||||||
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());
|
||||||
@Override
|
//
|
||||||
public SseEmitter handle(ChatContext context) {
|
// Long userId = context.getUserId();
|
||||||
log.info("处理 Agent 深度思考,用户: {}", context.getUserId());
|
// String tokenValue = context.getTokenValue();
|
||||||
|
// ChatModelVo chatModelVo = context.getChatModelVo();
|
||||||
Long userId = context.getUserId();
|
//
|
||||||
String tokenValue = context.getTokenValue();
|
// try {
|
||||||
ChatModelVo chatModelVo = context.getChatModelVo();
|
// // 1. 保存用户消息
|
||||||
|
// String content = extractUserContent(context);
|
||||||
try {
|
//// saveChatMessage(context.getChatRequest(), userId, content,
|
||||||
// 1. 保存用户消息
|
//// RoleType.USER.getName(), chatModelVo);
|
||||||
String content = extractUserContent(context);
|
//
|
||||||
saveChatMessage(context.getChatRequest(), userId, content,
|
// // 2. 执行 Agent 任务
|
||||||
RoleType.USER.getName(), chatModelVo);
|
// String result = doAgent(content, chatModelVo);
|
||||||
|
//
|
||||||
// 2. 执行 Agent 任务
|
// // 3. 发送结果并保存
|
||||||
String result = doAgent(content, chatModelVo);
|
// SseMessageUtils.sendMessage(userId, result);
|
||||||
|
// SseMessageUtils.completeConnection(userId, tokenValue);
|
||||||
// 3. 发送结果并保存
|
//
|
||||||
SseMessageUtils.sendMessage(userId, result);
|
//// saveChatMessage(context.getChatRequest(), userId, result,
|
||||||
SseMessageUtils.completeConnection(userId, tokenValue);
|
//// RoleType.ASSISTANT.getName(), chatModelVo);
|
||||||
saveChatMessage(context.getChatRequest(), userId, result,
|
// // todo 保存消息
|
||||||
RoleType.ASSISTANT.getName(), chatModelVo);
|
// } catch (Exception e) {
|
||||||
|
// log.error("Agent 执行失败: {}", e.getMessage(), e);
|
||||||
} catch (Exception e) {
|
// SseMessageUtils.sendMessage(userId, "Agent 执行失败:" + e.getMessage());
|
||||||
log.error("Agent 执行失败: {}", e.getMessage(), e);
|
// SseMessageUtils.completeConnection(userId, tokenValue);
|
||||||
SseMessageUtils.sendMessage(userId, "Agent 执行失败:" + e.getMessage());
|
// }
|
||||||
SseMessageUtils.completeConnection(userId, tokenValue);
|
//
|
||||||
}
|
// return context.getEmitter();
|
||||||
|
// }
|
||||||
return context.getEmitter();
|
//
|
||||||
}
|
// /**
|
||||||
|
// * 执行 Agent 任务
|
||||||
/**
|
// */
|
||||||
* 执行 Agent 任务
|
// private String doAgent(String userMessage, ChatModelVo chatModelVo) {
|
||||||
*/
|
// log.info("执行 Agent 任务,消息: {}", userMessage);
|
||||||
private String doAgent(String userMessage, ChatModelVo chatModelVo) {
|
//
|
||||||
log.info("执行 Agent 任务,消息: {}", userMessage);
|
// try {
|
||||||
|
// // 1. 加载 LLM 模型
|
||||||
try {
|
// QwenChatModel qwenChatModel = QwenChatModel.builder()
|
||||||
// 1. 加载 LLM 模型
|
// .apiKey(chatModelVo.getApiKey())
|
||||||
QwenChatModel qwenChatModel = QwenChatModel.builder()
|
// .modelName(chatModelVo.getModelName())
|
||||||
.apiKey(chatModelVo.getApiKey())
|
// .build();
|
||||||
.modelName(chatModelVo.getModelName())
|
//
|
||||||
.build();
|
// // 2. 获取内置工具
|
||||||
|
// List<Object> builtinTools = toolProviderFactory.getAllBuiltinToolObjects();
|
||||||
// 2. 获取内置工具
|
// List<Object> allTools = new ArrayList<>(builtinTools);
|
||||||
List<Object> builtinTools = toolProviderFactory.getAllBuiltinToolObjects();
|
// log.debug("加载 {} 个内置工具", builtinTools.size());
|
||||||
List<Object> allTools = new ArrayList<>(builtinTools);
|
//
|
||||||
log.debug("加载 {} 个内置工具", builtinTools.size());
|
// // 3. 获取 MCP 工具提供者
|
||||||
|
// ToolProvider mcpToolProvider = toolProviderFactory.getAllEnabledMcpToolsProvider();
|
||||||
// 3. 获取 MCP 工具提供者
|
//
|
||||||
ToolProvider mcpToolProvider = toolProviderFactory.getAllEnabledMcpToolsProvider();
|
// // 4. 创建 MCP Agent
|
||||||
|
// var agentBuilder = AgenticServices.agentBuilder(McpAgent.class)
|
||||||
// 4. 创建 MCP Agent
|
// .chatModel(qwenChatModel);
|
||||||
var agentBuilder = AgenticServices.agentBuilder(McpAgent.class)
|
//
|
||||||
.chatModel(qwenChatModel);
|
// if (!allTools.isEmpty()) {
|
||||||
|
// agentBuilder.tools(allTools.toArray(new Object[0]));
|
||||||
if (!allTools.isEmpty()) {
|
// }
|
||||||
agentBuilder.tools(allTools.toArray(new Object[0]));
|
// if (mcpToolProvider != null) {
|
||||||
}
|
// agentBuilder.toolProvider(mcpToolProvider);
|
||||||
if (mcpToolProvider != null) {
|
// }
|
||||||
agentBuilder.toolProvider(mcpToolProvider);
|
//
|
||||||
}
|
// McpAgent mcpAgent = agentBuilder.build();
|
||||||
|
//
|
||||||
McpAgent mcpAgent = agentBuilder.build();
|
// // 5. 调用 Agent
|
||||||
|
// String result = mcpAgent.callMcpTool(userMessage);
|
||||||
// 5. 调用 Agent
|
// log.info("Agent 执行完成,结果长度: {}", result.length());
|
||||||
String result = mcpAgent.callMcpTool(userMessage);
|
// return result;
|
||||||
log.info("Agent 执行完成,结果长度: {}", result.length());
|
//
|
||||||
return result;
|
// } catch (Exception e) {
|
||||||
|
// log.error("Agent 模式执行失败: {}", e.getMessage(), e);
|
||||||
} catch (Exception e) {
|
// return "Agent 执行失败: " + e.getMessage();
|
||||||
log.error("Agent 模式执行失败: {}", e.getMessage(), e);
|
// }
|
||||||
return "Agent 执行失败: " + e.getMessage();
|
// }
|
||||||
}
|
//
|
||||||
}
|
// /**
|
||||||
|
// * 提取用户消息内容
|
||||||
/**
|
// */
|
||||||
* 提取用户消息内容
|
// private String extractUserContent(ChatContext context) {
|
||||||
*/
|
// var messages = context.getChatRequest().getMessages();
|
||||||
private String extractUserContent(ChatContext context) {
|
// if (messages != null && !messages.isEmpty()) {
|
||||||
var messages = context.getChatRequest().getMessages();
|
// return messages.get(0).getContent();
|
||||||
if (messages != null && !messages.isEmpty()) {
|
// }
|
||||||
return messages.get(0).getContent();
|
// return "";
|
||||||
}
|
// }
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
//package org.ruoyi.service.chat.handler;
|
||||||
|
//
|
||||||
import lombok.RequiredArgsConstructor;
|
//import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
//import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.common.chat.domain.dto.request.ReSumeRunner;
|
//import org.ruoyi.common.chat.domain.dto.request.ReSumeRunner;
|
||||||
import org.ruoyi.common.chat.entity.chat.ChatContext;
|
//import org.ruoyi.common.chat.entity.chat.ChatContext;
|
||||||
import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService;
|
//import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService;
|
||||||
import org.ruoyi.common.core.utils.ObjectUtils;
|
//import org.ruoyi.common.core.utils.ObjectUtils;
|
||||||
import org.springframework.core.annotation.Order;
|
//import org.springframework.core.annotation.Order;
|
||||||
import org.springframework.stereotype.Component;
|
//import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
//import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
//
|
||||||
/**
|
///**
|
||||||
* 人机交互恢复处理器
|
// * 人机交互恢复处理器
|
||||||
* <p>
|
// * <p>
|
||||||
* 处理 isResume=true 的场景,恢复工作流的人机交互
|
// * 处理 isResume=true 的场景,恢复工作流的人机交互
|
||||||
*
|
// *
|
||||||
* @author ageerle@163.com
|
// * @author ageerle@163.com
|
||||||
* @date 2025/12/13
|
// * @date 2025/12/13
|
||||||
*/
|
// */
|
||||||
@Slf4j
|
//@Slf4j
|
||||||
@Component
|
//@Component
|
||||||
@Order(1)
|
//@Order(1)
|
||||||
@RequiredArgsConstructor
|
//@RequiredArgsConstructor
|
||||||
public class ResumeChatHandler implements ChatHandler {
|
//public class ResumeChatHandler implements ChatHandler {
|
||||||
|
//
|
||||||
private final IWorkFlowStarterService workFlowStarterService;
|
// private final IWorkFlowStarterService workFlowStarterService;
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public boolean supports(ChatContext context) {
|
// public boolean supports(ChatContext context) {
|
||||||
Boolean isResume = context.getChatRequest().getIsResume();
|
// Boolean isResume = context.getChatRequest().getIsResume();
|
||||||
return isResume != null && isResume;
|
// return isResume != null && isResume;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public SseEmitter handle(ChatContext context) {
|
// public SseEmitter handle(ChatContext context) {
|
||||||
log.info("处理人机交互恢复,用户: {}", context.getUserId());
|
// log.info("处理人机交互恢复,用户: {}", context.getUserId());
|
||||||
|
//
|
||||||
ReSumeRunner reSumeRunner = context.getChatRequest().getReSumeRunner();
|
// ReSumeRunner reSumeRunner = context.getChatRequest().getReSumeRunner();
|
||||||
if (ObjectUtils.isEmpty(reSumeRunner)) {
|
// if (ObjectUtils.isEmpty(reSumeRunner)) {
|
||||||
log.warn("人机交互恢复参数为空");
|
// log.warn("人机交互恢复参数为空");
|
||||||
return context.getEmitter();
|
// return context.getEmitter();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
workFlowStarterService.resumeFlow(
|
// workFlowStarterService.resumeFlow(
|
||||||
reSumeRunner.getRuntimeUuid(),
|
// reSumeRunner.getRuntimeUuid(),
|
||||||
reSumeRunner.getFeedbackContent(),
|
// reSumeRunner.getFeedbackContent(),
|
||||||
context.getEmitter()
|
// context.getEmitter()
|
||||||
);
|
// );
|
||||||
|
//
|
||||||
return context.getEmitter();
|
// return context.getEmitter();
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|||||||
@@ -1,54 +1,54 @@
|
|||||||
package org.ruoyi.service.chat.handler;
|
//package org.ruoyi.service.chat.handler;
|
||||||
|
//
|
||||||
import lombok.RequiredArgsConstructor;
|
//import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
//import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.common.chat.base.ThreadContext;
|
//import org.ruoyi.common.chat.base.ThreadContext;
|
||||||
import org.ruoyi.common.chat.domain.dto.request.WorkFlowRunner;
|
//import org.ruoyi.common.chat.domain.dto.request.WorkFlowRunner;
|
||||||
import org.ruoyi.common.chat.entity.chat.ChatContext;
|
//import org.ruoyi.common.chat.entity.chat.ChatContext;
|
||||||
import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService;
|
//import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService;
|
||||||
import org.ruoyi.common.core.utils.ObjectUtils;
|
//import org.ruoyi.common.core.utils.ObjectUtils;
|
||||||
import org.springframework.core.annotation.Order;
|
//import org.springframework.core.annotation.Order;
|
||||||
import org.springframework.stereotype.Component;
|
//import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
//import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
//
|
||||||
/**
|
///**
|
||||||
* 工作流对话处理器
|
// * 工作流对话处理器
|
||||||
* <p>
|
// * <p>
|
||||||
* 处理 enableWorkFlow=true 的场景,启动工作流对话
|
// * 处理 enableWorkFlow=true 的场景,启动工作流对话
|
||||||
*
|
// *
|
||||||
* @author ageerle@163.com
|
// * @author ageerle@163.com
|
||||||
* @date 2025/12/13
|
// * @date 2025/12/13
|
||||||
*/
|
// */
|
||||||
@Slf4j
|
//@Slf4j
|
||||||
@Component
|
//@Component
|
||||||
@Order(2)
|
//@Order(2)
|
||||||
@RequiredArgsConstructor
|
//@RequiredArgsConstructor
|
||||||
public class WorkflowChatHandler implements ChatHandler {
|
//public class WorkflowChatHandler implements ChatHandler {
|
||||||
|
//
|
||||||
private final IWorkFlowStarterService workFlowStarterService;
|
// private final IWorkFlowStarterService workFlowStarterService;
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public boolean supports(ChatContext context) {
|
// public boolean supports(ChatContext context) {
|
||||||
Boolean enableWorkFlow = context.getChatRequest().getEnableWorkFlow();
|
// Boolean enableWorkFlow = context.getChatRequest().getEnableWorkFlow();
|
||||||
return enableWorkFlow != null && enableWorkFlow;
|
// return enableWorkFlow != null && enableWorkFlow;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public SseEmitter handle(ChatContext context) {
|
// public SseEmitter handle(ChatContext context) {
|
||||||
log.info("处理工作流对话,用户: {}, 会话: {}",
|
// log.info("处理工作流对话,用户: {}, 会话: {}",
|
||||||
context.getUserId(), context.getChatRequest().getSessionId());
|
// context.getUserId(), context.getChatRequest().getSessionId());
|
||||||
|
//
|
||||||
WorkFlowRunner runner = context.getChatRequest().getWorkFlowRunner();
|
// WorkFlowRunner runner = context.getChatRequest().getWorkFlowRunner();
|
||||||
if (ObjectUtils.isEmpty(runner)) {
|
// if (ObjectUtils.isEmpty(runner)) {
|
||||||
log.warn("工作流参数为空");
|
// log.warn("工作流参数为空");
|
||||||
return context.getEmitter();
|
// return context.getEmitter();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
return workFlowStarterService.streaming(
|
// return workFlowStarterService.streaming(
|
||||||
ThreadContext.getCurrentUser(),
|
// ThreadContext.getCurrentUser(),
|
||||||
runner.getUuid(),
|
// runner.getUuid(),
|
||||||
runner.getInputs(),
|
// runner.getInputs(),
|
||||||
context.getChatRequest().getSessionId()
|
// 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;
|
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.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.domain.vo.chat.ChatMessageVo;
|
||||||
import org.ruoyi.common.chat.entity.chat.ChatMessage;
|
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.MapstructUtils;
|
||||||
import org.ruoyi.common.core.utils.StringUtils;
|
import org.ruoyi.common.core.utils.StringUtils;
|
||||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
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 com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.ruoyi.service.chat.IChatMessageService;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.ruoyi.mapper.chat.ChatMessageMapper;
|
import org.ruoyi.mapper.chat.ChatMessageMapper;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@@ -144,25 +146,27 @@ public class ChatMessageServiceImpl implements IChatMessageService {
|
|||||||
* @return 消息DTO列表
|
* @return 消息DTO列表
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public List<ChatMessageDTO> getMessagesBySessionId(Long sessionId) {
|
public List<dev.langchain4j.data.message.ChatMessage> getMessagesBySessionId(Long sessionId) {
|
||||||
if (sessionId == null) {
|
if (sessionId == null) {
|
||||||
return new java.util.ArrayList<>();
|
return new java.util.ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<dev.langchain4j.data.message.ChatMessage> chatMessageList = new ArrayList<>();
|
||||||
ChatMessageBo bo = new ChatMessageBo();
|
ChatMessageBo bo = new ChatMessageBo();
|
||||||
bo.setSessionId(sessionId);
|
bo.setSessionId(sessionId);
|
||||||
List<ChatMessageVo> voList = queryList(bo);
|
List<ChatMessageVo> voList = queryList(bo);
|
||||||
|
|
||||||
return voList.stream()
|
for (ChatMessageVo chatMessageVo : voList) {
|
||||||
.map(vo -> {
|
switch (chatMessageVo.getRole()) {
|
||||||
ChatMessageDTO dto = new ChatMessageDTO();
|
case "user" -> chatMessageList.add(UserMessage.from(chatMessageVo.getContent()));
|
||||||
dto.setRole(vo.getRole());
|
case "assistant" -> chatMessageList.add(AiMessage.from(chatMessageVo.getContent()));
|
||||||
dto.setContent(vo.getContent());
|
}
|
||||||
return dto;
|
}
|
||||||
})
|
return chatMessageList;
|
||||||
.collect(java.util.stream.Collectors.toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据会话ID删除所有消息
|
* 根据会话ID删除所有消息
|
||||||
* 用于清理会话历史
|
* 用于清理会话历史
|
||||||
@@ -180,4 +184,35 @@ public class ChatMessageServiceImpl implements IChatMessageService {
|
|||||||
lqw.eq(ChatMessage::getSessionId, sessionId);
|
lqw.eq(ChatMessage::getSessionId, sessionId);
|
||||||
return baseMapper.delete(lqw) > 0;
|
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;
|
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.RequiredArgsConstructor;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||||
import org.ruoyi.common.chat.entity.chat.ChatContext;
|
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||||
import org.ruoyi.service.chat.handler.ChatContextBuilder;
|
import org.ruoyi.common.chat.enums.RoleType;
|
||||||
import org.ruoyi.service.chat.handler.ChatHandler;
|
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.stereotype.Service;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 聊天服务门面层
|
* 聊天服务门面层
|
||||||
@@ -25,27 +46,324 @@ import java.util.List;
|
|||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ChatServiceFacade {
|
public class ChatServiceFacade implements IChatService {
|
||||||
|
|
||||||
private final ChatContextBuilder contextBuilder;
|
private static final Integer DEFAULT_MAX_MESSAGES = 20;
|
||||||
private final List<ChatHandler> handlers;
|
|
||||||
|
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流式响应
|
* 统一聊天入口 - SSE流式响应
|
||||||
*
|
*
|
||||||
* @param chatRequest 聊天请求
|
* @param chatRequest 聊天请求
|
||||||
* @param request HTTP请求对象
|
|
||||||
* @return SseEmitter
|
* @return SseEmitter
|
||||||
*/
|
*/
|
||||||
public SseEmitter sseChat(ChatRequest chatRequest, HttpServletRequest request) {
|
public SseEmitter sseChat(ChatRequest chatRequest) {
|
||||||
// 1. 构建对话上下文
|
|
||||||
ChatContext context = contextBuilder.build(chatRequest);
|
|
||||||
|
|
||||||
// 2. 路由到对应的处理器
|
// 1. 根据模型名称查询完整配置
|
||||||
return handlers.stream()
|
ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
|
||||||
.filter(handler -> handler.supports(context))
|
if (chatModelVo == null) {
|
||||||
.findFirst()
|
throw new IllegalArgumentException("模型不存在: " + chatRequest.getModel());
|
||||||
.orElseThrow(() -> new IllegalStateException("无可用对话处理器"))
|
}
|
||||||
.handle(context);
|
|
||||||
|
// 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.data.message.ChatMessage;
|
||||||
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
|
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.common.chat.domain.dto.ChatMessageDTO;
|
import org.ruoyi.service.chat.IChatMessageService;
|
||||||
import org.ruoyi.common.chat.service.chatMessage.IChatMessageService;
|
|
||||||
import org.ruoyi.common.core.utils.SpringUtils;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -18,13 +17,11 @@ import java.util.List;
|
|||||||
* @date 2025/01/10
|
* @date 2025/01/10
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class PersistentChatMemoryStore implements ChatMemoryStore {
|
public class PersistentChatMemoryStore implements ChatMemoryStore {
|
||||||
|
|
||||||
private final IChatMessageService chatMessageService;
|
private final IChatMessageService chatMessageService;
|
||||||
|
|
||||||
public PersistentChatMemoryStore() {
|
|
||||||
this.chatMessageService = SpringUtils.getBean(IChatMessageService.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据会话ID获取历史消息
|
* 根据会话ID获取历史消息
|
||||||
@@ -38,16 +35,8 @@ public class PersistentChatMemoryStore implements ChatMemoryStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Long sessionId = Long.parseLong(memoryId.toString());
|
Long sessionId = Long.parseLong(memoryId.toString());
|
||||||
|
|
||||||
// 从数据库获取该会话的所有消息
|
// 从数据库获取该会话的所有消息
|
||||||
List<ChatMessageDTO> dtoList = chatMessageService.getMessagesBySessionId(sessionId);
|
return chatMessageService.getMessagesBySessionId(sessionId);
|
||||||
|
|
||||||
if (dtoList == null || dtoList.isEmpty()) {
|
|
||||||
return new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换为LangChain4j格式
|
|
||||||
return convertToLangChainMessages(dtoList);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("获取会话 {} 的消息失败: {}", memoryId, e.getMessage(), e);
|
log.error("获取会话 {} 的消息失败: {}", memoryId, e.getMessage(), e);
|
||||||
return new ArrayList<>();
|
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;
|
package org.ruoyi.service.chat.impl.provider;
|
||||||
|
|
||||||
import dev.langchain4j.data.message.ChatMessage;
|
|
||||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
|
||||||
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||||
import org.ruoyi.enums.ChatModeType;
|
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.stereotype.Service;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author: xiaoen
|
* @Author: xiaoen
|
||||||
@@ -20,7 +17,7 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class DeepseekServiceImpl extends AbstractStreamingChatService {
|
public class DeepseekServiceImpl implements AbstractChatService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
||||||
@@ -32,11 +29,6 @@ public class DeepseekServiceImpl extends AbstractStreamingChatService {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List<ChatMessage> messagesWithMemory, StreamingChatResponseHandler handler) {
|
|
||||||
StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo, chatRequest);
|
|
||||||
streamingChatModel.chat(messagesWithMemory, handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getProviderName() {
|
public String getProviderName() {
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
package org.ruoyi.service.chat.impl.provider;
|
package org.ruoyi.service.chat.impl.provider;
|
||||||
|
|
||||||
import dev.langchain4j.data.message.ChatMessage;
|
|
||||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
|
||||||
import dev.langchain4j.model.ollama.OllamaStreamingChatModel;
|
import dev.langchain4j.model.ollama.OllamaStreamingChatModel;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.enums.ChatModeType;
|
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.stereotype.Service;
|
||||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OllamaAI服务调用
|
* OllamaAI服务调用
|
||||||
*
|
*
|
||||||
@@ -21,7 +17,7 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class OllamaServiceImpl extends AbstractStreamingChatService {
|
public class OllamaServiceImpl implements AbstractChatService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
||||||
@@ -31,16 +27,6 @@ public class OllamaServiceImpl extends AbstractStreamingChatService {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List<ChatMessage> messagesWithMemory,StreamingChatResponseHandler handler) {
|
|
||||||
StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo, chatRequest);
|
|
||||||
streamingChatModel.chat(messagesWithMemory, handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getProviderName() {
|
public String getProviderName() {
|
||||||
return ChatModeType.OLLAMA.getCode();
|
return ChatModeType.OLLAMA.getCode();
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
package org.ruoyi.service.chat.impl.provider;
|
package org.ruoyi.service.chat.impl.provider;
|
||||||
|
|
||||||
|
|
||||||
import dev.langchain4j.data.message.ChatMessage;
|
|
||||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
|
||||||
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||||
import org.ruoyi.enums.ChatModeType;
|
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.stereotype.Service;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OPENAI服务调用
|
* OPENAI服务调用
|
||||||
@@ -23,7 +19,7 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class OpenAIServiceImpl extends AbstractStreamingChatService {
|
public class OpenAIServiceImpl implements AbstractChatService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
||||||
@@ -35,13 +31,6 @@ public class OpenAIServiceImpl extends AbstractStreamingChatService {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List<ChatMessage> messagesWithMemory, StreamingChatResponseHandler handler) {
|
|
||||||
StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo, chatRequest);
|
|
||||||
streamingChatModel.chat(messagesWithMemory, handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getProviderName() {
|
public String getProviderName() {
|
||||||
return ChatModeType.OPEN_AI.getCode();
|
return ChatModeType.OPEN_AI.getCode();
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
package org.ruoyi.service.chat.impl.provider;
|
package org.ruoyi.service.chat.impl.provider;
|
||||||
|
|
||||||
|
|
||||||
import dev.langchain4j.data.message.ChatMessage;
|
|
||||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
|
||||||
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||||
import org.ruoyi.enums.ChatModeType;
|
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.stereotype.Service;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OPENAI服务调用
|
* OPENAI服务调用
|
||||||
*
|
*
|
||||||
@@ -23,7 +17,7 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class PPIOServiceImpl extends AbstractStreamingChatService {
|
public class PPIOServiceImpl implements AbstractChatService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
||||||
@@ -35,13 +29,6 @@ public class PPIOServiceImpl extends AbstractStreamingChatService {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List<ChatMessage> messagesWithMemory, StreamingChatResponseHandler handler) {
|
|
||||||
StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo, chatRequest);
|
|
||||||
streamingChatModel.chat(messagesWithMemory, handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getProviderName() {
|
public String getProviderName() {
|
||||||
return ChatModeType.PPIO.getCode();
|
return ChatModeType.PPIO.getCode();
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
package org.ruoyi.service.chat.impl.provider;
|
package org.ruoyi.service.chat.impl.provider;
|
||||||
|
|
||||||
import dev.langchain4j.community.model.dashscope.QwenStreamingChatModel;
|
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.StreamingChatModel;
|
||||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||||
import org.ruoyi.enums.ChatModeType;
|
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.stereotype.Service;
|
||||||
import org.springframework.util.CollectionUtils;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* qianWenAI服务调用
|
* qianWenAI服务调用
|
||||||
@@ -25,11 +18,9 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class QianWenChatServiceImpl extends AbstractStreamingChatService {
|
public class QianWenChatServiceImpl implements AbstractChatService {
|
||||||
|
|
||||||
// 添加文档解析的前缀字段
|
// 添加文档解析的前缀字段
|
||||||
private static final String UPLOAD_FILE_API_PREFIX = "fileid";
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo,ChatRequest chatRequest) {
|
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo,ChatRequest chatRequest) {
|
||||||
return QwenStreamingChatModel.builder()
|
return QwenStreamingChatModel.builder()
|
||||||
@@ -38,47 +29,6 @@ public class QianWenChatServiceImpl extends AbstractStreamingChatService {
|
|||||||
.build();
|
.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
|
@Override
|
||||||
public String getProviderName() {
|
public String getProviderName() {
|
||||||
return ChatModeType.QIAN_WEN.getCode();
|
return ChatModeType.QIAN_WEN.getCode();
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
package org.ruoyi.service.chat.impl.provider;
|
package org.ruoyi.service.chat.impl.provider;
|
||||||
|
|
||||||
import dev.langchain4j.community.model.zhipu.ZhipuAiStreamingChatModel;
|
import dev.langchain4j.community.model.zhipu.ZhipuAiStreamingChatModel;
|
||||||
import dev.langchain4j.data.message.ChatMessage;
|
|
||||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||||
import org.ruoyi.enums.ChatModeType;
|
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.stereotype.Service;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 智谱AI服务调用
|
* 智谱AI服务调用
|
||||||
@@ -21,12 +18,7 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ZhiPuChatServiceImpl extends AbstractStreamingChatService {
|
public class ZhiPuChatServiceImpl implements AbstractChatService {
|
||||||
@Override
|
|
||||||
public void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List<ChatMessage> messagesWithMemory, StreamingChatResponseHandler handler) {
|
|
||||||
StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo,chatRequest);
|
|
||||||
streamingChatModel.chat(messagesWithMemory, handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
||||||
|
|||||||
Reference in New Issue
Block a user