mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-04-15 12:53:42 +00:00
v3.0.0 init
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
package org.ruoyi.service.chat;
|
||||
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
import org.ruoyi.domain.bo.chat.ChatConfigBo;
|
||||
import org.ruoyi.domain.vo.chat.ChatConfigVo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 配置信息Service接口
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-14
|
||||
*/
|
||||
public interface IChatConfigService {
|
||||
|
||||
/**
|
||||
* 查询配置信息
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 配置信息
|
||||
*/
|
||||
ChatConfigVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 分页查询配置信息列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @return 配置信息分页列表
|
||||
*/
|
||||
TableDataInfo<ChatConfigVo> queryPageList(ChatConfigBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询符合条件的配置信息列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 配置信息列表
|
||||
*/
|
||||
List<ChatConfigVo> queryList(ChatConfigBo bo);
|
||||
|
||||
/**
|
||||
* 新增配置信息
|
||||
*
|
||||
* @param bo 配置信息
|
||||
* @return 是否新增成功
|
||||
*/
|
||||
Boolean insertByBo(ChatConfigBo bo);
|
||||
|
||||
/**
|
||||
* 修改配置信息
|
||||
*
|
||||
* @param bo 配置信息
|
||||
* @return 是否修改成功
|
||||
*/
|
||||
Boolean updateByBo(ChatConfigBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除配置信息信息
|
||||
*
|
||||
* @param ids 待删除的主键集合
|
||||
* @param isValid 是否进行有效性校验
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.ruoyi.service.chat;
|
||||
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
import org.ruoyi.domain.bo.chat.ChatMessageBo;
|
||||
import org.ruoyi.domain.dto.ChatMessageDTO;
|
||||
import org.ruoyi.domain.vo.chat.ChatMessageVo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 聊天消息Service接口
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-14
|
||||
*/
|
||||
public interface IChatMessageService {
|
||||
|
||||
/**
|
||||
* 查询聊天消息
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 聊天消息
|
||||
*/
|
||||
ChatMessageVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 分页查询聊天消息列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @return 聊天消息分页列表
|
||||
*/
|
||||
TableDataInfo<ChatMessageVo> queryPageList(ChatMessageBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询符合条件的聊天消息列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 聊天消息列表
|
||||
*/
|
||||
List<ChatMessageVo> queryList(ChatMessageBo bo);
|
||||
|
||||
/**
|
||||
* 新增聊天消息
|
||||
*
|
||||
* @param bo 聊天消息
|
||||
* @return 是否新增成功
|
||||
*/
|
||||
Boolean insertByBo(ChatMessageBo bo);
|
||||
|
||||
/**
|
||||
* 修改聊天消息
|
||||
*
|
||||
* @param bo 聊天消息
|
||||
* @return 是否修改成功
|
||||
*/
|
||||
Boolean updateByBo(ChatMessageBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除聊天消息信息
|
||||
*
|
||||
* @param ids 待删除的主键集合
|
||||
* @param isValid 是否进行有效性校验
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
|
||||
/**
|
||||
* 根据会话ID获取所有消息
|
||||
* 用于长期记忆功能
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @return 消息DTO列表
|
||||
*/
|
||||
List<ChatMessageDTO> getMessagesBySessionId(Long sessionId);
|
||||
|
||||
/**
|
||||
* 根据会话ID删除所有消息
|
||||
* 用于清理会话历史
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
Boolean deleteBySessionId(Long sessionId);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package org.ruoyi.service.chat;
|
||||
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
import org.ruoyi.domain.bo.chat.ChatModelBo;
|
||||
import org.ruoyi.domain.vo.chat.ChatModelVo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 模型管理Service接口
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-14
|
||||
*/
|
||||
public interface IChatModelService {
|
||||
|
||||
/**
|
||||
* 查询模型管理
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 模型管理
|
||||
*/
|
||||
ChatModelVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 根据模型名称查询模型
|
||||
*
|
||||
* @param modelName 模型名称
|
||||
* @return 模型管理
|
||||
*/
|
||||
ChatModelVo selectModelByName(String modelName);
|
||||
|
||||
/**
|
||||
* 分页查询模型管理列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @return 模型管理分页列表
|
||||
*/
|
||||
TableDataInfo<ChatModelVo> queryPageList(ChatModelBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询符合条件的模型管理列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 模型管理列表
|
||||
*/
|
||||
List<ChatModelVo> queryList(ChatModelBo bo);
|
||||
|
||||
/**
|
||||
* 新增模型管理
|
||||
*
|
||||
* @param bo 模型管理
|
||||
* @return 是否新增成功
|
||||
*/
|
||||
Boolean insertByBo(ChatModelBo bo);
|
||||
|
||||
/**
|
||||
* 修改模型管理
|
||||
*
|
||||
* @param bo 模型管理
|
||||
* @return 是否修改成功
|
||||
*/
|
||||
Boolean updateByBo(ChatModelBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除模型管理信息
|
||||
*
|
||||
* @param ids 待删除的主键集合
|
||||
* @param isValid 是否进行有效性校验
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.ruoyi.service.chat;
|
||||
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
import org.ruoyi.domain.bo.chat.ChatProviderBo;
|
||||
import org.ruoyi.domain.vo.chat.ChatProviderVo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 厂商管理Service接口
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-14
|
||||
*/
|
||||
public interface IChatProviderService {
|
||||
|
||||
/**
|
||||
* 查询厂商管理
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 厂商管理
|
||||
*/
|
||||
ChatProviderVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 分页查询厂商管理列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @return 厂商管理分页列表
|
||||
*/
|
||||
TableDataInfo<ChatProviderVo> queryPageList(ChatProviderBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询符合条件的厂商管理列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 厂商管理列表
|
||||
*/
|
||||
List<ChatProviderVo> queryList(ChatProviderBo bo);
|
||||
|
||||
/**
|
||||
* 新增厂商管理
|
||||
*
|
||||
* @param bo 厂商管理
|
||||
* @return 是否新增成功
|
||||
*/
|
||||
Boolean insertByBo(ChatProviderBo bo);
|
||||
|
||||
/**
|
||||
* 修改厂商管理
|
||||
*
|
||||
* @param bo 厂商管理
|
||||
* @return 是否修改成功
|
||||
*/
|
||||
Boolean updateByBo(ChatProviderBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除厂商管理信息
|
||||
*
|
||||
* @param ids 待删除的主键集合
|
||||
* @param isValid 是否进行有效性校验
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.ruoyi.service.chat;
|
||||
|
||||
import org.ruoyi.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.domain.vo.chat.ChatModelVo;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
/**
|
||||
* 对话Service接口
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-04-08
|
||||
*/
|
||||
public interface IChatService {
|
||||
|
||||
/**
|
||||
* 客户端发送对话消息到服务端
|
||||
*/
|
||||
SseEmitter chat(ChatModelVo chatModelVo, ChatRequest chatRequest,SseEmitter emitter,Long userId,String tokenValue);
|
||||
|
||||
|
||||
/**
|
||||
* 获取服务提供商名称
|
||||
*/
|
||||
String getProviderName();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.ruoyi.service.chat;
|
||||
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import org.ruoyi.domain.bo.chat.ChatSessionBo;
|
||||
import org.ruoyi.domain.vo.chat.ChatSessionVo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 会话管理Service接口
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-30
|
||||
*/
|
||||
public interface IChatSessionService {
|
||||
|
||||
/**
|
||||
* 查询会话管理
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 会话管理
|
||||
*/
|
||||
ChatSessionVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 分页查询会话管理列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @return 会话管理分页列表
|
||||
*/
|
||||
TableDataInfo<ChatSessionVo> queryPageList(ChatSessionBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询符合条件的会话管理列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 会话管理列表
|
||||
*/
|
||||
List<ChatSessionVo> queryList(ChatSessionBo bo);
|
||||
|
||||
/**
|
||||
* 新增会话管理
|
||||
*
|
||||
* @param bo 会话管理
|
||||
* @return 是否新增成功
|
||||
*/
|
||||
Boolean insertByBo(ChatSessionBo bo);
|
||||
|
||||
/**
|
||||
* 修改会话管理
|
||||
*
|
||||
* @param bo 会话管理
|
||||
* @return 是否修改成功
|
||||
*/
|
||||
Boolean updateByBo(ChatSessionBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除会话管理信息
|
||||
*
|
||||
* @param ids 待删除的主键集合
|
||||
* @param isValid 是否进行有效性校验
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
package org.ruoyi.service.chat.impl;
|
||||
|
||||
import dev.langchain4j.agentic.AgenticServices;
|
||||
import dev.langchain4j.agentic.supervisor.SupervisorAgent;
|
||||
import dev.langchain4j.agentic.supervisor.SupervisorResponseStrategy;
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.mcp.McpToolProvider;
|
||||
import dev.langchain4j.mcp.client.DefaultMcpClient;
|
||||
import dev.langchain4j.mcp.client.McpClient;
|
||||
import dev.langchain4j.mcp.client.transport.McpTransport;
|
||||
import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport;
|
||||
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 dev.langchain4j.model.openai.OpenAiChatModel;
|
||||
import dev.langchain4j.service.tool.ToolProvider;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.agent.ChartGenerationAgent;
|
||||
import org.ruoyi.agent.SqlAgent;
|
||||
import org.ruoyi.agent.WebSearchAgent;
|
||||
import org.ruoyi.agent.tool.ExecuteSqlQueryTool;
|
||||
import org.ruoyi.agent.tool.QueryAllTablesTool;
|
||||
import org.ruoyi.agent.tool.QueryTableSchemaTool;
|
||||
import org.ruoyi.common.core.utils.SpringUtils;
|
||||
import org.ruoyi.common.sse.utils.SseMessageUtils;
|
||||
import org.ruoyi.domain.bo.chat.ChatMessageBo;
|
||||
import org.ruoyi.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.enums.RoleType;
|
||||
import org.ruoyi.service.chat.IChatMessageService;
|
||||
import org.ruoyi.service.chat.IChatService;
|
||||
import org.ruoyi.service.chat.impl.memory.PersistentChatMemoryStore;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 流式聊天服务抽象基类 - 支持上下文和长期记忆
|
||||
* 使用模板方法模式,抽取公共逻辑
|
||||
* <p>
|
||||
* 设计原则:
|
||||
* 1. 抽象层只依赖业务模型,不依赖具体SDK
|
||||
* 2. 子类负责将业务模型转换为厂商SDK格式
|
||||
* 3. 提供钩子方法,子类可灵活覆盖
|
||||
* 4. 支持长期记忆 - 自动维护会话的消息历史
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/12/13
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractStreamingChatService implements IChatService {
|
||||
|
||||
/**
|
||||
* 默认保留的消息窗口大小(用于长期记忆)
|
||||
*/
|
||||
private static final int DEFAULT_MAX_MESSAGES = 20;
|
||||
|
||||
/**
|
||||
* 是否启用长期记忆功能
|
||||
*/
|
||||
private static final boolean enablePersistentMemory = true;
|
||||
|
||||
/**
|
||||
* 内存实例缓存,避免同一会话重复创建
|
||||
* Key: sessionId, Value: MessageWindowChatMemory实例
|
||||
*/
|
||||
private static final Map<Object, MessageWindowChatMemory> memoryCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 定义聊天流程骨架
|
||||
*/
|
||||
@Override
|
||||
public SseEmitter chat(ChatModelVo chatModelVo, ChatRequest chatRequest, SseEmitter emitter, Long userId, String tokenValue) {
|
||||
try {
|
||||
String content = chatRequest.getMessages().get(0).getContent();
|
||||
// 保存用户消息
|
||||
saveChatMessage(chatRequest, userId, content, RoleType.USER.getName(), chatModelVo);
|
||||
|
||||
// 使用长期记忆增强的消息列表
|
||||
List<ChatMessage> messagesWithMemory = buildMessagesWithMemory(chatRequest);
|
||||
|
||||
if(chatRequest.getEnableThinking()){
|
||||
String msg = doAgent(content,chatModelVo);
|
||||
SseMessageUtils.sendMessage(userId, msg);
|
||||
SseMessageUtils.completeConnection(userId, tokenValue);
|
||||
// 保存助手回复消息
|
||||
saveChatMessage(chatRequest, userId, msg, RoleType.ASSISTANT.getName(), chatModelVo);
|
||||
}else {
|
||||
// 创建包含内存管理的响应处理器
|
||||
StreamingChatResponseHandler handler = createResponseHandler(chatRequest, userId, tokenValue, chatModelVo);
|
||||
// 调用具体实现的聊天方法
|
||||
doChat(chatModelVo, chatRequest, messagesWithMemory, handler);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("{}请求失败:{}", getProviderName(), e.getMessage(), e);
|
||||
}
|
||||
return emitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建包含历史消息和当前请求的完整消息列表(长期记忆)
|
||||
* 返回: 历史消息 + 当前请求消息
|
||||
* 确保即使第一次对话也有消息上下文
|
||||
*
|
||||
* @param chatRequest 聊天请求
|
||||
* @return 包含历史消息和当前请求消息的完整消息列表
|
||||
*/
|
||||
protected List<ChatMessage> buildMessagesWithMemory(ChatRequest chatRequest) {
|
||||
List<ChatMessage> messages = new ArrayList<>();
|
||||
|
||||
// 加载历史消息
|
||||
if (enablePersistentMemory && 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建或获取聊天内存实例(缓存机制)
|
||||
* 同一个会话ID会返回同一个内存实例,避免重复创建和消息丢失
|
||||
*
|
||||
* @param memoryId 内存ID(会话ID)
|
||||
* @return MessageWindowChatMemory实例
|
||||
*/
|
||||
private MessageWindowChatMemory createChatMemory(Object memoryId) {
|
||||
// 先从缓存中获取
|
||||
return memoryCache.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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理指定会话的内存缓存(可选)
|
||||
* 在会话结束时调用,释放内存资源
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
*/
|
||||
public static void clearChatMemory(Object sessionId) {
|
||||
memoryCache.remove(sessionId);
|
||||
log.debug("已清理会话 {} 的内存缓存", sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行聊天(钩子方法 - 子类必须实现)
|
||||
* 注意:messages 已包含完整的历史上下文和当前消息
|
||||
*
|
||||
* @param chatModelVo 模型配置
|
||||
* @param chatRequest 聊天请求
|
||||
* @param handler 响应处理器
|
||||
*/
|
||||
protected abstract void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List<ChatMessage> messagesWithMemory,StreamingChatResponseHandler handler);
|
||||
|
||||
/**
|
||||
* 创建标准的响应处理器
|
||||
*
|
||||
* @param chatRequest 聊天请求,包含sessionId等上下文信息
|
||||
* @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()) {
|
||||
log.warn("{}接收到空消息", getProviderName());
|
||||
} else {
|
||||
// 保存助手回复消息
|
||||
saveChatMessage(chatRequest, userId, fullMessage, RoleType.ASSISTANT.getName(), chatModelVo);
|
||||
}
|
||||
|
||||
// 关闭SSE连接
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存聊天消息到数据库
|
||||
*
|
||||
* @param chatRequest 聊天请求
|
||||
* @param userId 用户ID
|
||||
* @param content 消息内容
|
||||
* @param role 消息角色
|
||||
* @param chatModelVo 模型配置
|
||||
*/
|
||||
private 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.setBillingType(chatModelVo.getModelType());
|
||||
messageBO.setRemark(null);
|
||||
|
||||
IChatMessageService chatMessageService = SpringUtils.getBean(IChatMessageService.class);
|
||||
chatMessageService.insertByBo(messageBO);
|
||||
} catch (Exception e) {
|
||||
log.error("保存{}聊天消息时出错: {}", getProviderName(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建具体厂商的 StreamingChatModel
|
||||
* 子类必须实现此方法,返回对应厂商的模型实例
|
||||
*/
|
||||
protected abstract StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest);
|
||||
|
||||
|
||||
/**
|
||||
* 获取提供者名称(子类必须实现)
|
||||
*/
|
||||
public abstract String getProviderName();
|
||||
|
||||
protected String doAgent(String userMessage,ChatModelVo chatModelVo) {
|
||||
// 步骤1: 配置MCP传输层 - 连接到bing-cn-mcp服务器
|
||||
// 该服务提供两个工具: bing_search (必应搜索) 和 crawl_webpage (网页抓取)
|
||||
McpTransport transport = new StdioMcpTransport.Builder()
|
||||
.command(List.of("C:\\Program Files\\nodejs\\npx.cmd", "-y",
|
||||
"bing-cn-mcp"
|
||||
))
|
||||
.logEvents(true)
|
||||
.build();
|
||||
|
||||
// 步骤2: 创建MCP客户端
|
||||
McpClient mcpClient = new DefaultMcpClient.Builder()
|
||||
.transport(transport)
|
||||
.build();
|
||||
|
||||
// 步骤3: 配置工具提供者
|
||||
ToolProvider toolProvider = McpToolProvider.builder()
|
||||
.mcpClients(List.of(mcpClient))
|
||||
.build();
|
||||
|
||||
|
||||
McpTransport transport1 = new StdioMcpTransport.Builder()
|
||||
.command(List.of("C:\\Program Files\\nodejs\\npx.cmd", "-y",
|
||||
"mcp-echarts"
|
||||
))
|
||||
.logEvents(true)
|
||||
.build();
|
||||
|
||||
// 步骤2: 创建MCP客户端
|
||||
McpClient mcpClient1 = new DefaultMcpClient.Builder()
|
||||
.transport(transport1)
|
||||
.build();
|
||||
|
||||
// 步骤3: 配置工具提供者
|
||||
ToolProvider toolProvider1 = McpToolProvider.builder()
|
||||
.mcpClients(List.of(mcpClient1))
|
||||
.build();
|
||||
|
||||
// 步骤4: 配置OpenAI模型
|
||||
OpenAiChatModel PLANNER_MODEL = OpenAiChatModel.builder()
|
||||
.baseUrl(chatModelVo.getApiHost())
|
||||
.apiKey(chatModelVo.getApiKey())
|
||||
.modelName(chatModelVo.getModelName())
|
||||
.build();
|
||||
|
||||
SqlAgent sqlAgent = AgenticServices.agentBuilder(SqlAgent.class)
|
||||
.chatModel(PLANNER_MODEL)
|
||||
.tools(
|
||||
new QueryAllTablesTool(),
|
||||
new QueryTableSchemaTool(),
|
||||
new ExecuteSqlQueryTool()
|
||||
)
|
||||
.build();
|
||||
|
||||
WebSearchAgent searchAgent = AgenticServices.agentBuilder(WebSearchAgent.class)
|
||||
.chatModel(PLANNER_MODEL)
|
||||
.toolProvider(toolProvider)
|
||||
.build();
|
||||
|
||||
ChartGenerationAgent chartGenerationAgent = AgenticServices.agentBuilder(ChartGenerationAgent.class)
|
||||
.chatModel(PLANNER_MODEL)
|
||||
.toolProvider(toolProvider1)
|
||||
.build();
|
||||
|
||||
SupervisorAgent supervisor = AgenticServices
|
||||
.supervisorBuilder()
|
||||
.chatModel(PLANNER_MODEL)
|
||||
.subAgents(sqlAgent,chartGenerationAgent)
|
||||
.responseStrategy(SupervisorResponseStrategy.LAST)
|
||||
.build();
|
||||
|
||||
String invoke = supervisor.invoke(userMessage);
|
||||
System.out.println(invoke);
|
||||
return invoke;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package org.ruoyi.service.chat.impl;
|
||||
|
||||
import org.ruoyi.common.core.utils.MapstructUtils;
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.service.chat.IChatConfigService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.ruoyi.domain.bo.chat.ChatConfigBo;
|
||||
import org.ruoyi.domain.vo.chat.ChatConfigVo;
|
||||
import org.ruoyi.domain.entity.chat.ChatConfig;
|
||||
import org.ruoyi.mapper.chat.ChatConfigMapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 配置信息Service业务层处理
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-14
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class ChatConfigServiceImpl implements IChatConfigService {
|
||||
|
||||
private final ChatConfigMapper baseMapper;
|
||||
|
||||
/**
|
||||
* 查询配置信息
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 配置信息
|
||||
*/
|
||||
@Override
|
||||
public ChatConfigVo queryById(Long id){
|
||||
return baseMapper.selectVoById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询配置信息列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @return 配置信息分页列表
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<ChatConfigVo> queryPageList(ChatConfigBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<ChatConfig> lqw = buildQueryWrapper(bo);
|
||||
Page<ChatConfigVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询符合条件的配置信息列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 配置信息列表
|
||||
*/
|
||||
@Override
|
||||
public List<ChatConfigVo> queryList(ChatConfigBo bo) {
|
||||
LambdaQueryWrapper<ChatConfig> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<ChatConfig> buildQueryWrapper(ChatConfigBo bo) {
|
||||
Map<String, Object> params = bo.getParams();
|
||||
LambdaQueryWrapper<ChatConfig> lqw = Wrappers.lambdaQuery();
|
||||
lqw.orderByAsc(ChatConfig::getId);
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getCategory()), ChatConfig::getCategory, bo.getCategory());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getConfigName()), ChatConfig::getConfigName, bo.getConfigName());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getConfigValue()), ChatConfig::getConfigValue, bo.getConfigValue());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getConfigDict()), ChatConfig::getConfigDict, bo.getConfigDict());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getUpdateIp()), ChatConfig::getUpdateIp, bo.getUpdateIp());
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增配置信息
|
||||
*
|
||||
* @param bo 配置信息
|
||||
* @return 是否新增成功
|
||||
*/
|
||||
@Override
|
||||
public Boolean insertByBo(ChatConfigBo bo) {
|
||||
ChatConfig add = MapstructUtils.convert(bo, ChatConfig.class);
|
||||
validEntityBeforeSave(add);
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setId(add.getId());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改配置信息
|
||||
*
|
||||
* @param bo 配置信息
|
||||
* @return 是否修改成功
|
||||
*/
|
||||
@Override
|
||||
public Boolean updateByBo(ChatConfigBo bo) {
|
||||
ChatConfig update = MapstructUtils.convert(bo, ChatConfig.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前的数据校验
|
||||
*/
|
||||
private void validEntityBeforeSave(ChatConfig entity){
|
||||
//TODO 做一些数据校验,如唯一约束
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并批量删除配置信息信息
|
||||
*
|
||||
* @param ids 待删除的主键集合
|
||||
* @param isValid 是否进行有效性校验
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
@Override
|
||||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||||
if(isValid){
|
||||
//TODO 做一些业务上的校验,判断是否需要校验
|
||||
}
|
||||
return baseMapper.deleteByIds(ids) > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package org.ruoyi.service.chat.impl;
|
||||
|
||||
import org.ruoyi.common.core.utils.MapstructUtils;
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.domain.dto.ChatMessageDTO;
|
||||
import org.ruoyi.service.chat.IChatMessageService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.ruoyi.domain.bo.chat.ChatMessageBo;
|
||||
import org.ruoyi.domain.vo.chat.ChatMessageVo;
|
||||
import org.ruoyi.domain.entity.chat.ChatMessage;
|
||||
import org.ruoyi.mapper.chat.ChatMessageMapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 聊天消息Service业务层处理
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-14
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class ChatMessageServiceImpl implements IChatMessageService {
|
||||
|
||||
private final ChatMessageMapper baseMapper;
|
||||
|
||||
/**
|
||||
* 查询聊天消息
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 聊天消息
|
||||
*/
|
||||
@Override
|
||||
public ChatMessageVo queryById(Long id){
|
||||
return baseMapper.selectVoById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询聊天消息列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @return 聊天消息分页列表
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<ChatMessageVo> queryPageList(ChatMessageBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<ChatMessage> lqw = buildQueryWrapper(bo);
|
||||
Page<ChatMessageVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询符合条件的聊天消息列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 聊天消息列表
|
||||
*/
|
||||
@Override
|
||||
public List<ChatMessageVo> queryList(ChatMessageBo bo) {
|
||||
LambdaQueryWrapper<ChatMessage> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<ChatMessage> buildQueryWrapper(ChatMessageBo bo) {
|
||||
Map<String, Object> params = bo.getParams();
|
||||
LambdaQueryWrapper<ChatMessage> lqw = Wrappers.lambdaQuery();
|
||||
lqw.orderByAsc(ChatMessage::getId);
|
||||
lqw.eq(bo.getSessionId() != null, ChatMessage::getSessionId, bo.getSessionId());
|
||||
lqw.eq(bo.getUserId() != null, ChatMessage::getUserId, bo.getUserId());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getContent()), ChatMessage::getContent, bo.getContent());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getRole()), ChatMessage::getRole, bo.getRole());
|
||||
lqw.eq(bo.getDeductCost() != null, ChatMessage::getDeductCost, bo.getDeductCost());
|
||||
lqw.eq(bo.getTotalTokens() != null, ChatMessage::getTotalTokens, bo.getTotalTokens());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getModelName()), ChatMessage::getModelName, bo.getModelName());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getBillingType()), ChatMessage::getBillingType, bo.getBillingType());
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增聊天消息
|
||||
*
|
||||
* @param bo 聊天消息
|
||||
* @return 是否新增成功
|
||||
*/
|
||||
@Override
|
||||
public Boolean insertByBo(ChatMessageBo bo) {
|
||||
ChatMessage add = MapstructUtils.convert(bo, ChatMessage.class);
|
||||
validEntityBeforeSave(add);
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setId(add.getId());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改聊天消息
|
||||
*
|
||||
* @param bo 聊天消息
|
||||
* @return 是否修改成功
|
||||
*/
|
||||
@Override
|
||||
public Boolean updateByBo(ChatMessageBo bo) {
|
||||
ChatMessage update = MapstructUtils.convert(bo, ChatMessage.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前的数据校验
|
||||
*/
|
||||
private void validEntityBeforeSave(ChatMessage entity){
|
||||
//TODO 做一些数据校验,如唯一约束
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并批量删除聊天消息信息
|
||||
*
|
||||
* @param ids 待删除的主键集合
|
||||
* @param isValid 是否进行有效性校验
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
@Override
|
||||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||||
if(isValid){
|
||||
//TODO 做一些业务上的校验,判断是否需要校验
|
||||
}
|
||||
return baseMapper.deleteByIds(ids) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据会话ID获取所有消息
|
||||
* 用于长期记忆功能
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @return 消息DTO列表
|
||||
*/
|
||||
@Override
|
||||
public List<org.ruoyi.domain.dto.ChatMessageDTO> getMessagesBySessionId(Long sessionId) {
|
||||
if (sessionId == null) {
|
||||
return new java.util.ArrayList<>();
|
||||
}
|
||||
|
||||
ChatMessageBo bo = new ChatMessageBo();
|
||||
bo.setSessionId(sessionId);
|
||||
List<ChatMessageVo> voList = queryList(bo);
|
||||
|
||||
return voList.stream()
|
||||
.map(vo -> {
|
||||
ChatMessageDTO dto = new ChatMessageDTO();
|
||||
dto.setRole(vo.getRole());
|
||||
dto.setContent(vo.getContent());
|
||||
return dto;
|
||||
})
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据会话ID删除所有消息
|
||||
* 用于清理会话历史
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
@Override
|
||||
public Boolean deleteBySessionId(Long sessionId) {
|
||||
if (sessionId == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<ChatMessage> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(ChatMessage::getSessionId, sessionId);
|
||||
return baseMapper.delete(lqw) > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package org.ruoyi.service.chat.impl;
|
||||
|
||||
import org.ruoyi.common.core.utils.MapstructUtils;
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.service.chat.IChatModelService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.ruoyi.domain.bo.chat.ChatModelBo;
|
||||
import org.ruoyi.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.domain.entity.chat.ChatModel;
|
||||
import org.ruoyi.mapper.chat.ChatModelMapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 模型管理Service业务层处理
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-14
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class ChatModelServiceImpl implements IChatModelService {
|
||||
|
||||
private final ChatModelMapper baseMapper;
|
||||
|
||||
/**
|
||||
* 查询模型管理
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 模型管理
|
||||
*/
|
||||
@Override
|
||||
public ChatModelVo queryById(Long id){
|
||||
return baseMapper.selectVoById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模型名称查询模型
|
||||
*
|
||||
* @param modelName 模型名称
|
||||
* @return 模型管理
|
||||
*/
|
||||
@Override
|
||||
public ChatModelVo selectModelByName(String modelName) {
|
||||
LambdaQueryWrapper<ChatModel> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(ChatModel::getModelName, modelName);
|
||||
lqw.last("LIMIT 1");
|
||||
return baseMapper.selectVoOne(lqw);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询模型管理列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @return 模型管理分页列表
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<ChatModelVo> queryPageList(ChatModelBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<ChatModel> lqw = buildQueryWrapper(bo);
|
||||
Page<ChatModelVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询符合条件的模型管理列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 模型管理列表
|
||||
*/
|
||||
@Override
|
||||
public List<ChatModelVo> queryList(ChatModelBo bo) {
|
||||
LambdaQueryWrapper<ChatModel> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<ChatModel> buildQueryWrapper(ChatModelBo bo) {
|
||||
Map<String, Object> params = bo.getParams();
|
||||
LambdaQueryWrapper<ChatModel> lqw = Wrappers.lambdaQuery();
|
||||
lqw.orderByAsc(ChatModel::getId);
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getCategory()), ChatModel::getCategory, bo.getCategory());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getModelName()), ChatModel::getModelName, bo.getModelName());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getProviderCode()), ChatModel::getProviderCode, bo.getProviderCode());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getModelDescribe()), ChatModel::getModelDescribe, bo.getModelDescribe());
|
||||
lqw.eq(bo.getModelPrice() != null, ChatModel::getModelPrice, bo.getModelPrice());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getModelType()), ChatModel::getModelType, bo.getModelType());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getModelShow()), ChatModel::getModelShow, bo.getModelShow());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getModelFree()), ChatModel::getModelFree, bo.getModelFree());
|
||||
lqw.eq(bo.getPriority() != null, ChatModel::getPriority, bo.getPriority());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getApiHost()), ChatModel::getApiHost, bo.getApiHost());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getApiKey()), ChatModel::getApiKey, bo.getApiKey());
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增模型管理
|
||||
*
|
||||
* @param bo 模型管理
|
||||
* @return 是否新增成功
|
||||
*/
|
||||
@Override
|
||||
public Boolean insertByBo(ChatModelBo bo) {
|
||||
ChatModel add = MapstructUtils.convert(bo, ChatModel.class);
|
||||
validEntityBeforeSave(add);
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setId(add.getId());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改模型管理
|
||||
*
|
||||
* @param bo 模型管理
|
||||
* @return 是否修改成功
|
||||
*/
|
||||
@Override
|
||||
public Boolean updateByBo(ChatModelBo bo) {
|
||||
ChatModel update = MapstructUtils.convert(bo, ChatModel.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前的数据校验
|
||||
*/
|
||||
private void validEntityBeforeSave(ChatModel entity){
|
||||
//TODO 做一些数据校验,如唯一约束
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并批量删除模型管理信息
|
||||
*
|
||||
* @param ids 待删除的主键集合
|
||||
* @param isValid 是否进行有效性校验
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
@Override
|
||||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||||
if(isValid){
|
||||
//TODO 做一些业务上的校验,判断是否需要校验
|
||||
}
|
||||
return baseMapper.deleteByIds(ids) > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package org.ruoyi.service.chat.impl;
|
||||
|
||||
import org.ruoyi.common.core.utils.MapstructUtils;
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.service.chat.IChatProviderService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.ruoyi.domain.bo.chat.ChatProviderBo;
|
||||
import org.ruoyi.domain.vo.chat.ChatProviderVo;
|
||||
import org.ruoyi.domain.entity.chat.ChatProvider;
|
||||
import org.ruoyi.mapper.chat.ChatProviderMapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 厂商管理Service业务层处理
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-14
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class ChatProviderServiceImpl implements IChatProviderService {
|
||||
|
||||
private final ChatProviderMapper baseMapper;
|
||||
|
||||
/**
|
||||
* 查询厂商管理
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 厂商管理
|
||||
*/
|
||||
@Override
|
||||
public ChatProviderVo queryById(Long id){
|
||||
return baseMapper.selectVoById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询厂商管理列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @return 厂商管理分页列表
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<ChatProviderVo> queryPageList(ChatProviderBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<ChatProvider> lqw = buildQueryWrapper(bo);
|
||||
Page<ChatProviderVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询符合条件的厂商管理列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 厂商管理列表
|
||||
*/
|
||||
@Override
|
||||
public List<ChatProviderVo> queryList(ChatProviderBo bo) {
|
||||
LambdaQueryWrapper<ChatProvider> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<ChatProvider> buildQueryWrapper(ChatProviderBo bo) {
|
||||
Map<String, Object> params = bo.getParams();
|
||||
LambdaQueryWrapper<ChatProvider> lqw = Wrappers.lambdaQuery();
|
||||
lqw.orderByAsc(ChatProvider::getId);
|
||||
lqw.like(StringUtils.isNotBlank(bo.getProviderName()), ChatProvider::getProviderName, bo.getProviderName());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getProviderCode()), ChatProvider::getProviderCode, bo.getProviderCode());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getProviderIcon()), ChatProvider::getProviderIcon, bo.getProviderIcon());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getProviderDesc()), ChatProvider::getProviderDesc, bo.getProviderDesc());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getApiHost()), ChatProvider::getApiHost, bo.getApiHost());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getStatus()), ChatProvider::getStatus, bo.getStatus());
|
||||
lqw.eq(bo.getSortOrder() != null, ChatProvider::getSortOrder, bo.getSortOrder());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getUpdateIp()), ChatProvider::getUpdateIp, bo.getUpdateIp());
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增厂商管理
|
||||
*
|
||||
* @param bo 厂商管理
|
||||
* @return 是否新增成功
|
||||
*/
|
||||
@Override
|
||||
public Boolean insertByBo(ChatProviderBo bo) {
|
||||
ChatProvider add = MapstructUtils.convert(bo, ChatProvider.class);
|
||||
validEntityBeforeSave(add);
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setId(add.getId());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改厂商管理
|
||||
*
|
||||
* @param bo 厂商管理
|
||||
* @return 是否修改成功
|
||||
*/
|
||||
@Override
|
||||
public Boolean updateByBo(ChatProviderBo bo) {
|
||||
ChatProvider update = MapstructUtils.convert(bo, ChatProvider.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前的数据校验
|
||||
*/
|
||||
private void validEntityBeforeSave(ChatProvider entity){
|
||||
//TODO 做一些数据校验,如唯一约束
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并批量删除厂商管理信息
|
||||
*
|
||||
* @param ids 待删除的主键集合
|
||||
* @param isValid 是否进行有效性校验
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
@Override
|
||||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||||
if(isValid){
|
||||
//TODO 做一些业务上的校验,判断是否需要校验
|
||||
}
|
||||
return baseMapper.deleteByIds(ids) > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package org.ruoyi.service.chat.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.dto.ChatMessageDTO;
|
||||
import org.ruoyi.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.domain.vo.knowledge.KnowledgeInfoVo;
|
||||
import org.ruoyi.factory.ChatServiceFactory;
|
||||
import org.ruoyi.service.chat.IChatModelService;
|
||||
import org.ruoyi.service.chat.IChatService;
|
||||
import org.ruoyi.service.knowledge.IKnowledgeInfoService;
|
||||
import org.ruoyi.service.vector.VectorStoreService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 聊天服务业务实现
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/12/13
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class ChatServiceFacade {
|
||||
|
||||
private final IChatModelService chatModelService;
|
||||
|
||||
private final ChatServiceFactory chatServiceFactory;
|
||||
|
||||
private final IKnowledgeInfoService knowledgeInfoService;
|
||||
|
||||
private final VectorStoreService vectorStoreService;
|
||||
|
||||
private final SseEmitterManager sseEmitterManager;
|
||||
|
||||
/**
|
||||
* 统一聊天入口 - SSE流式响应
|
||||
*
|
||||
* @param chatRequest 聊天请求
|
||||
* @param request HTTP 请求对象
|
||||
* @return SseEmitter
|
||||
*/
|
||||
public SseEmitter sseChat(ChatRequest chatRequest, HttpServletRequest request) {
|
||||
|
||||
// 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. 路由服务提供商
|
||||
String category = chatModelVo.getProviderCode();
|
||||
log.info("路由到服务提供商: {}, 模型: {}", category, chatRequest.getModel());
|
||||
IChatService chatService = chatServiceFactory.getOriginalService(category);
|
||||
|
||||
// 4. 具体的服务实现
|
||||
Long userId = LoginHelper.getUserId();
|
||||
String tokenValue = StpUtil.getTokenValue();
|
||||
SseEmitter emitter = sseEmitterManager.connect(userId, tokenValue);
|
||||
return chatService.chat(chatModelVo, chatRequest,emitter,userId, tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建上下文消息列表
|
||||
*
|
||||
* @param chatRequest 聊天请求
|
||||
* @return 上下文消息列表
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package org.ruoyi.service.chat.impl;
|
||||
|
||||
import org.ruoyi.common.core.utils.MapstructUtils;
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.domain.bo.chat.ChatSessionBo;
|
||||
import org.ruoyi.domain.entity.chat.ChatSession;
|
||||
import org.ruoyi.mapper.chat.ChatSessionMapper;
|
||||
import org.ruoyi.service.chat.IChatSessionService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.ruoyi.domain.vo.chat.ChatSessionVo;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 会话管理Service业务层处理
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-30
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class ChatSessionServiceImpl implements IChatSessionService {
|
||||
|
||||
private final ChatSessionMapper baseMapper;
|
||||
|
||||
/**
|
||||
* 查询会话管理
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 会话管理
|
||||
*/
|
||||
@Override
|
||||
public ChatSessionVo queryById(Long id){
|
||||
return baseMapper.selectVoById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询会话管理列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @return 会话管理分页列表
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<ChatSessionVo> queryPageList(ChatSessionBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<ChatSession> lqw = buildQueryWrapper(bo);
|
||||
Page<ChatSessionVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询符合条件的会话管理列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 会话管理列表
|
||||
*/
|
||||
@Override
|
||||
public List<ChatSessionVo> queryList(ChatSessionBo bo) {
|
||||
LambdaQueryWrapper<ChatSession> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<ChatSession> buildQueryWrapper(ChatSessionBo bo) {
|
||||
Map<String, Object> params = bo.getParams();
|
||||
LambdaQueryWrapper<ChatSession> lqw = Wrappers.lambdaQuery();
|
||||
lqw.orderByAsc(ChatSession::getId);
|
||||
lqw.eq(bo.getUserId() != null, ChatSession::getUserId, bo.getUserId());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getSessionTitle()), ChatSession::getSessionTitle, bo.getSessionTitle());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getSessionContent()), ChatSession::getSessionContent, bo.getSessionContent());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getConversationId()), ChatSession::getConversationId, bo.getConversationId());
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增会话管理
|
||||
*
|
||||
* @param bo 会话管理
|
||||
* @return 是否新增成功
|
||||
*/
|
||||
@Override
|
||||
public Boolean insertByBo(ChatSessionBo bo) {
|
||||
ChatSession add = MapstructUtils.convert(bo, ChatSession.class);
|
||||
validEntityBeforeSave(add);
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setId(add.getId());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改会话管理
|
||||
*
|
||||
* @param bo 会话管理
|
||||
* @return 是否修改成功
|
||||
*/
|
||||
@Override
|
||||
public Boolean updateByBo(ChatSessionBo bo) {
|
||||
ChatSession update = MapstructUtils.convert(bo, ChatSession.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前的数据校验
|
||||
*/
|
||||
private void validEntityBeforeSave(ChatSession entity){
|
||||
//TODO 做一些数据校验,如唯一约束
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并批量删除会话管理信息
|
||||
*
|
||||
* @param ids 待删除的主键集合
|
||||
* @param isValid 是否进行有效性校验
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
@Override
|
||||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||||
if(isValid){
|
||||
//TODO 做一些业务上的校验,判断是否需要校验
|
||||
}
|
||||
return baseMapper.deleteByIds(ids) > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.ruoyi.service.chat.impl.memory;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 聊天长期记忆配置属性
|
||||
* 支持通过 application.yml 配置长期记忆行为
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2026/01/10
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "chat.memory")
|
||||
public class ChatMemoryProperties {
|
||||
|
||||
/**
|
||||
* 是否启用长期记忆功能(默认启用)
|
||||
*/
|
||||
private Boolean enabled = true;
|
||||
|
||||
/**
|
||||
* 消息窗口大小 - 最多保留的消息条数(默认20)
|
||||
* 用于控制每次聊天请求中包含的历史消息数量
|
||||
*/
|
||||
private Integer maxMessages = 20;
|
||||
|
||||
/**
|
||||
* 是否启用消息持久化(默认启用)
|
||||
* 关闭后消息仅保存在内存中,重启后丢失
|
||||
*/
|
||||
private Boolean persistenceEnabled = true;
|
||||
|
||||
/**
|
||||
* 自动清理过期消息的时间间隔(天数,默认不清理)
|
||||
* 设为 0 表示禁用自动清理
|
||||
*/
|
||||
private Integer autoCleanupDays = 0;
|
||||
|
||||
/**
|
||||
* 消息摘要是否启用(默认禁用)
|
||||
* 启用后,超过消息窗口的旧消息会被摘要处理
|
||||
*/
|
||||
private Boolean summarizeEnabled = false;
|
||||
|
||||
/**
|
||||
* 摘要缓冲区大小 - 触发摘要的消息数量阈值(默认50)
|
||||
*/
|
||||
private Integer summarizeThreshold = 50;
|
||||
|
||||
/**
|
||||
* 是否在日志中记录内存加载情况(默认启用,用于调试)
|
||||
*/
|
||||
private Boolean debugLoggingEnabled = true;
|
||||
|
||||
/**
|
||||
* 数据库查询超时时间(毫秒,默认5000)
|
||||
*/
|
||||
private Integer queryTimeoutMs = 5000;
|
||||
|
||||
/**
|
||||
* 最大并发内存访问数(默认100)
|
||||
*/
|
||||
private Integer maxConcurrentMemories = 100;
|
||||
|
||||
/**
|
||||
* 获取格式化的配置信息
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChatMemoryProperties{" +
|
||||
"enabled=" + enabled +
|
||||
", maxMessages=" + maxMessages +
|
||||
", persistenceEnabled=" + persistenceEnabled +
|
||||
", autoCleanupDays=" + autoCleanupDays +
|
||||
", summarizeEnabled=" + summarizeEnabled +
|
||||
", summarizeThreshold=" + summarizeThreshold +
|
||||
", debugLoggingEnabled=" + debugLoggingEnabled +
|
||||
", queryTimeoutMs=" + queryTimeoutMs +
|
||||
", maxConcurrentMemories=" + maxConcurrentMemories +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
package org.ruoyi.service.chat.impl.memory;
|
||||
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.domain.dto.ChatMessageDTO;
|
||||
import org.ruoyi.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.domain.vo.chat.ChatModelVo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 长期记忆使用示例
|
||||
* 演示如何在实际项目中集成和使用长期记忆功能
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/01/10
|
||||
*/
|
||||
@Slf4j
|
||||
public class ChatMemoryUsageExample {
|
||||
|
||||
/**
|
||||
* 示例1:基础使用 - 自动长期记忆
|
||||
* <p>
|
||||
* 系统自动加载历史消息,无需手动处理
|
||||
*/
|
||||
public void example1_BasicUsage() {
|
||||
log.info("=== 示例1:基础使用(自动长期记忆)===");
|
||||
/*
|
||||
// 假设已经有ChatService实例
|
||||
ChatService chatService = getBean(ChatService.class);
|
||||
|
||||
// 构建请求 - 只需提供当前消息
|
||||
ChatRequest request = new ChatRequest();
|
||||
request.setSessionId(123L); // 重要:设置会话ID以启用长期记忆
|
||||
request.setModel("gpt-4o-mini");
|
||||
request.setMessages(Arrays.asList(
|
||||
ChatMessageDTO.user("我之前告诉过你我的名字是Alice,对吧?")
|
||||
));
|
||||
|
||||
// 系统自动:
|
||||
// 1. 加载会话123的历史消息
|
||||
// 2. 合并历史消息与当前消息
|
||||
// 3. 发送给LLM
|
||||
// 4. 保存新消息到数据库和内存
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例2:多轮对话场景
|
||||
* <p>
|
||||
* 展示长期记忆如何维护多轮对话的连续性
|
||||
*/
|
||||
public void example2_MultiTurnConversation() {
|
||||
log.info("=== 示例2:多轮对话场景 ===");
|
||||
/*
|
||||
第一轮:
|
||||
User: "我是Alice,我来自纽约,我是一个数据科学家"
|
||||
AI: "很高兴认识你,Alice!听起来你在数据科学领域很专业。"
|
||||
-> 消息保存到数据库和内存
|
||||
|
||||
第二轮(5分钟后):
|
||||
User: "我现在想学习机器学习,你有什么建议吗?"
|
||||
AI: "作为一个来自纽约的数据科学家,你已经有很好的基础。..."
|
||||
-> 系统自动加载了第一轮的信息,AI能够提供更个性化的回应
|
||||
|
||||
第三轮(1小时后):
|
||||
User: "我的工作地点"
|
||||
AI: "你之前提到你来自纽约..."
|
||||
-> 即使经过很长时间,长期记忆也维持了上下文
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例3:系统提示词与长期记忆的结合
|
||||
* <p>
|
||||
* 演示如何结合系统提示词以获得更好的效果
|
||||
*/
|
||||
public void example3_SystemPromptWithMemory() {
|
||||
log.info("=== 示例3:系统提示词与长期记忆结合 ===");
|
||||
/*
|
||||
ChatRequest request = new ChatRequest();
|
||||
request.setSessionId(456L);
|
||||
request.setModel("gpt-4o-mini");
|
||||
|
||||
List<ChatMessageDTO> messages = new ArrayList<>();
|
||||
|
||||
// 第一条:系统角色定义
|
||||
messages.add(ChatMessageDTO.system(
|
||||
"你是一个技术助手。" +
|
||||
"你记得用户在之前的对话中分享的所有信息。" +
|
||||
"在回答时要体现出你对用户背景的了解。"
|
||||
));
|
||||
|
||||
// 第二条:当前用户消息
|
||||
messages.add(ChatMessageDTO.user("基于我之前提到的技术栈,推荐合适的框架"));
|
||||
|
||||
request.setMessages(messages);
|
||||
|
||||
// 系统处理流程:
|
||||
// 1. 加载会话456的历史记录(包括用户之前提到的技术栈)
|
||||
// 2. 合并系统提示词 + 历史消息 + 当前消息
|
||||
// 3. 发送完整上下文给LLM
|
||||
// 4. LLM基于完整上下文生成更准确的推荐
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例4:清理过期消息
|
||||
* <p>
|
||||
* 展示如何手动清理特定会话的消息(例如用户要求忘记对话)
|
||||
*/
|
||||
public void example4_CleanupMessages() {
|
||||
log.info("=== 示例4:清理过期消息 ===");
|
||||
/*
|
||||
// 假设已有IChatMessageService实例
|
||||
IChatMessageService chatMessageService = getBean(IChatMessageService.class);
|
||||
|
||||
// 场景:用户要求"忘记我们之前的对话"
|
||||
Long sessionId = 789L;
|
||||
|
||||
// 清理该会话的所有消息
|
||||
Boolean result = chatMessageService.deleteBySessionId(sessionId);
|
||||
|
||||
if (result) {
|
||||
log.info("已成功清理会话 {} 的所有消息", sessionId);
|
||||
// 之后的对话将从空白状态开始,无历史上下文
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例5:消息窗口管理
|
||||
* <p>
|
||||
* 展示消息窗口机制如何防止上下文爆炸
|
||||
*/
|
||||
public void example5_MessageWindowManagement() {
|
||||
log.info("=== 示例5:消息窗口管理 ===");
|
||||
/*
|
||||
配置: maxMessages = 20
|
||||
|
||||
场景:用户有100条消息的历史
|
||||
会话历史: [msg1, msg2, ..., msg100]
|
||||
|
||||
处理步骤:
|
||||
1. 系统从历史中加载所有消息
|
||||
2. MessageWindowChatMemory自动应用滑动窗口
|
||||
3. 只保留最近20条消息: [msg81, msg82, ..., msg100]
|
||||
4. 发送给LLM的上下文只包含20条消息
|
||||
|
||||
优点:
|
||||
- 保持上下文足够充分(20条)
|
||||
- 避免超长上下文导致的高成本和低效率
|
||||
- 减少token消耗
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例6:不同会话的隔离
|
||||
* <p>
|
||||
* 展示如何通过sessionId实现会话隔离
|
||||
*/
|
||||
public void example6_SessionIsolation() {
|
||||
log.info("=== 示例6:会话隔离 ===");
|
||||
/*
|
||||
同一用户(userId = 1)有多个会话:
|
||||
|
||||
会话A (sessionId = 101):
|
||||
- 讨论话题:"如何学习Java"
|
||||
- 消息历史: [Q1: 学习资源, A1: 推荐书籍...]
|
||||
|
||||
会话B (sessionId = 102):
|
||||
- 讨论话题:"Python项目实践"
|
||||
- 消息历史: [Q1: 项目建议, A1: 推荐Django...]
|
||||
|
||||
会话C (sessionId = 103):
|
||||
- 讨论话题:"数据库设计"
|
||||
- 消息历史: [Q1: 选择数据库, A1: MySQL vs PostgreSQL...]
|
||||
|
||||
每个会话只加载自己的历史消息,完全隔离:
|
||||
- 在会话A中,AI无法访问会话B或C的消息
|
||||
- 用户可以在不同会话间自由切换而不会混淆
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例7:添加新的Provider实现
|
||||
* <p>
|
||||
* 如何扩展系统以支持新的LLM提供商
|
||||
*/
|
||||
public void example7_AddNewProvider() {
|
||||
log.info("=== 示例7:添加新Provider ===");
|
||||
/*
|
||||
步骤:
|
||||
|
||||
1. 创建新的Service类:
|
||||
@Service
|
||||
@Slf4j
|
||||
public class ClaudeServiceImpl extends AbstractStreamingChatService {
|
||||
|
||||
@Override
|
||||
protected StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
||||
return ClaudeStreamingChatModel.builder()
|
||||
.apiKey(chatModelVo.getApiKey())
|
||||
.modelName(chatModelVo.getModelName())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest,
|
||||
List<ChatMessage> messages, StreamingChatResponseHandler handler) {
|
||||
// messages 已包含完整的历史上下文
|
||||
StreamingChatModel model = buildStreamingChatModel(chatModelVo, chatRequest);
|
||||
model.chat(messages, handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProviderName() {
|
||||
return "claude";
|
||||
}
|
||||
}
|
||||
|
||||
2. 系统自动继承长期记忆能力 - 无需额外处理
|
||||
3. 所有会话数据共享同一个ChatMessage表
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例8:自定义长期记忆行为
|
||||
* <p>
|
||||
* 展示如何在Service中自定义记忆行为
|
||||
*/
|
||||
public void example8_CustomMemoryBehavior() {
|
||||
log.info("=== 示例8:自定义长期记忆行为 ===");
|
||||
/*
|
||||
在Service初始化时:
|
||||
|
||||
@Service
|
||||
public class MyCustomChatService extends AbstractStreamingChatService {
|
||||
|
||||
public MyCustomChatService() {
|
||||
// 禁用长期记忆(如果需要)
|
||||
setEnablePersistentMemory(false);
|
||||
}
|
||||
|
||||
// 或在特定业务逻辑中
|
||||
public void processRequest(ChatRequest request) {
|
||||
if (isTemporaryChatSession(request)) {
|
||||
setEnablePersistentMemory(false); // 临时会话不记忆
|
||||
} else {
|
||||
setEnablePersistentMemory(true); // 正式会话记忆
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例9:配置文件使用
|
||||
* <p>
|
||||
* 展示如何通过 application.yml 配置长期记忆
|
||||
*/
|
||||
public void example9_ConfigurationFile() {
|
||||
log.info("=== 示例9:配置文件使用 ===");
|
||||
/*
|
||||
application.yml 配置示例:
|
||||
|
||||
chat:
|
||||
memory:
|
||||
enabled: true # 启用长期记忆
|
||||
maxMessages: 20 # 消息窗口大小
|
||||
persistenceEnabled: true # 启用数据库持久化
|
||||
autoCleanupDays: 30 # 30天后自动清理
|
||||
summarizeEnabled: false # 禁用消息摘要
|
||||
debugLoggingEnabled: true # 启用调试日志
|
||||
queryTimeoutMs: 5000 # 查询超时5秒
|
||||
maxConcurrentMemories: 100 # 最多100个并发内存
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例10:监控和调试
|
||||
* <p>
|
||||
* 展示如何监控长期记忆的运行状态
|
||||
*/
|
||||
public void example10_MonitoringAndDebugging() {
|
||||
log.info("=== 示例10:监控和调试 ===");
|
||||
/*
|
||||
关键日志信息:
|
||||
|
||||
1. 加载历史消息:
|
||||
"已加载 15 条历史消息用于会话 12345"
|
||||
-> 表示系统成功加载了历史记录
|
||||
|
||||
2. 创建失败:
|
||||
"创建聊天内存失败: ..."
|
||||
-> 需要检查数据库连接和权限
|
||||
|
||||
3. 更新失败:
|
||||
"更新聊天内存失败: ..."
|
||||
-> 检查数据库是否正确配置
|
||||
|
||||
4. 性能监控:
|
||||
如果加载消息时间过长,考虑:
|
||||
- 减少maxMessages值
|
||||
- 启用消息摘要
|
||||
- 定期清理过期消息
|
||||
- 为session_id列添加索引
|
||||
*/
|
||||
}
|
||||
|
||||
// 辅助方法
|
||||
private Object getBean(Class<?> clazz) {
|
||||
// 实现Spring Bean获取逻辑
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isTemporaryChatSession(ChatRequest request) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package org.ruoyi.service.chat.impl.memory;
|
||||
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.core.utils.SpringUtils;
|
||||
import org.ruoyi.domain.dto.ChatMessageDTO;
|
||||
import org.ruoyi.service.chat.IChatMessageService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 持久化聊天内存存储 - 将消息存储到数据库
|
||||
* 支持每个用户/会话的独立消息历史
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/01/10
|
||||
*/
|
||||
@Slf4j
|
||||
public class PersistentChatMemoryStore implements ChatMemoryStore {
|
||||
|
||||
private final IChatMessageService chatMessageService;
|
||||
|
||||
public PersistentChatMemoryStore() {
|
||||
this.chatMessageService = SpringUtils.getBean(IChatMessageService.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据会话ID获取历史消息
|
||||
* 转换为LangChain4j的ChatMessage格式
|
||||
*/
|
||||
@Override
|
||||
public List<ChatMessage> getMessages(Object memoryId) {
|
||||
try {
|
||||
if (memoryId == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
Long sessionId = Long.parseLong(memoryId.toString());
|
||||
|
||||
// 从数据库获取该会话的所有消息
|
||||
List<ChatMessageDTO> dtoList = chatMessageService.getMessagesBySessionId(sessionId);
|
||||
|
||||
if (dtoList == null || dtoList.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 转换为LangChain4j格式
|
||||
return convertToLangChainMessages(dtoList);
|
||||
} catch (Exception e) {
|
||||
log.error("获取会话 {} 的消息失败: {}", memoryId, e.getMessage(), e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话的消息历史
|
||||
*/
|
||||
@Override
|
||||
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
|
||||
try {
|
||||
if (memoryId == null || messages == null || messages.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Long sessionId = Long.parseLong(memoryId.toString());
|
||||
log.debug("成功更新会话 {} 的消息,共 {} 条", sessionId, messages.size());
|
||||
} catch (Exception e) {
|
||||
log.error("更新会话 {} 的消息失败: {}", memoryId, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话的所有消息
|
||||
*/
|
||||
@Override
|
||||
public void deleteMessages(Object memoryId) {
|
||||
try {
|
||||
if (memoryId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Long sessionId = Long.parseLong(memoryId.toString());
|
||||
chatMessageService.deleteBySessionId(sessionId);
|
||||
|
||||
log.info("成功删除会话 {} 的所有消息", sessionId);
|
||||
} catch (Exception e) {
|
||||
log.error("删除会话 {} 的消息失败: {}", memoryId, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.ruoyi.service.chat.impl.provider;
|
||||
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import dev.langchain4j.model.ollama.OllamaStreamingChatModel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.enums.ChatModeType;
|
||||
import org.ruoyi.service.chat.impl.AbstractStreamingChatService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* OllamaAI服务调用
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/12/13
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class OllamaServiceImpl extends AbstractStreamingChatService {
|
||||
|
||||
@Override
|
||||
protected StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
||||
return OllamaStreamingChatModel.builder()
|
||||
.baseUrl(chatModelVo.getApiHost())
|
||||
.modelName(chatModelVo.getModelName())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List<ChatMessage> messagesWithMemory,StreamingChatResponseHandler handler) {
|
||||
StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo, chatRequest);
|
||||
streamingChatModel.chat(messagesWithMemory, handler);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public String getProviderName() {
|
||||
return ChatModeType.OLLAMA.getCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package org.ruoyi.service.chat.impl.provider;
|
||||
|
||||
import dev.langchain4j.agentic.AgenticServices;
|
||||
import dev.langchain4j.agentic.supervisor.SupervisorAgent;
|
||||
import dev.langchain4j.agentic.supervisor.SupervisorResponseStrategy;
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.mcp.McpToolProvider;
|
||||
import dev.langchain4j.mcp.client.DefaultMcpClient;
|
||||
import dev.langchain4j.mcp.client.McpClient;
|
||||
import dev.langchain4j.mcp.client.transport.McpTransport;
|
||||
import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport;
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import dev.langchain4j.model.openai.OpenAiChatModel;
|
||||
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
||||
import dev.langchain4j.service.tool.ToolProvider;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.agent.ChartGenerationAgent;
|
||||
import org.ruoyi.agent.SqlAgent;
|
||||
import org.ruoyi.agent.WebSearchAgent;
|
||||
import org.ruoyi.agent.tool.ExecuteSqlQueryTool;
|
||||
import org.ruoyi.agent.tool.QueryAllTablesTool;
|
||||
import org.ruoyi.agent.tool.QueryTableSchemaTool;
|
||||
import org.ruoyi.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.enums.ChatModeType;
|
||||
import org.ruoyi.service.chat.impl.AbstractStreamingChatService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* OPENAI服务调用
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/12/13
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class OpenAIServiceImpl extends AbstractStreamingChatService {
|
||||
|
||||
@Override
|
||||
protected StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
||||
return OpenAiStreamingChatModel.builder()
|
||||
.baseUrl(chatModelVo.getApiHost())
|
||||
.apiKey(chatModelVo.getApiKey())
|
||||
.modelName(chatModelVo.getModelName())
|
||||
.returnThinking(chatRequest.getEnableThinking())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List<ChatMessage> messagesWithMemory, StreamingChatResponseHandler handler) {
|
||||
StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo, chatRequest);
|
||||
streamingChatModel.chat(messagesWithMemory, handler);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public String getProviderName() {
|
||||
return ChatModeType.OPEN_AI.getCode();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.ruoyi.service.chat.impl.provider;
|
||||
|
||||
import dev.langchain4j.community.model.dashscope.QwenStreamingChatModel;
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.enums.ChatModeType;
|
||||
import org.ruoyi.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.service.chat.impl.AbstractStreamingChatService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* qianWenAI服务调用
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/12/13
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class QianWenChatServiceImpl extends AbstractStreamingChatService {
|
||||
|
||||
@Override
|
||||
protected StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo,ChatRequest chatRequest) {
|
||||
return QwenStreamingChatModel.builder()
|
||||
.apiKey(chatModelVo.getApiKey())
|
||||
.modelName(chatModelVo.getModelName())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doChat(ChatModelVo chatModelVo,ChatRequest chatRequest,List<ChatMessage> messagesWithMemory,
|
||||
StreamingChatResponseHandler handler) {
|
||||
StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo,chatRequest);
|
||||
streamingChatModel.chat(messagesWithMemory, handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProviderName() {
|
||||
return ChatModeType.QIAN_WEN.getCode();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.ruoyi.service.embed;
|
||||
|
||||
import dev.langchain4j.model.embedding.EmbeddingModel;
|
||||
import org.ruoyi.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.enums.ModalityType;
|
||||
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* BaseEmbedModelService 接口,扩展了 EmbeddingModel 接口
|
||||
* 该接口定义了嵌入模型服务的基本配置和功能方法
|
||||
*/
|
||||
public interface BaseEmbedModelService extends EmbeddingModel {
|
||||
/**
|
||||
* 根据配置信息配置嵌入模型
|
||||
*
|
||||
* @param config 包含模型配置信息的 ChatModelVo 对象
|
||||
*/
|
||||
void configure(ChatModelVo config);
|
||||
|
||||
/**
|
||||
* 获取当前嵌入模型支持的所有模态类型
|
||||
*
|
||||
* @return 返回支持的模态类型集合
|
||||
*/
|
||||
Set<ModalityType> getSupportedModalities();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.ruoyi.service.embed;
|
||||
|
||||
import dev.langchain4j.data.embedding.Embedding;
|
||||
import dev.langchain4j.model.output.Response;
|
||||
import org.ruoyi.domain.dto.MultiModalInput;
|
||||
|
||||
|
||||
/**
|
||||
* 多模态嵌入模型服务接口,继承自基础嵌入模型服务
|
||||
* 该接口提供了处理图像、视频以及多模态数据并转换为嵌入向量的功能
|
||||
*/
|
||||
public interface MultiModalEmbedModelService extends BaseEmbedModelService {
|
||||
/**
|
||||
* 将图像数据转换为嵌入向量
|
||||
*
|
||||
* @param imageDataUrl 图像的地址,必须是公开可访问的URL
|
||||
* @return 包含嵌入向量的响应对象,可能包含状态信息和嵌入结果
|
||||
*/
|
||||
Response<Embedding> embedImage(String imageDataUrl);
|
||||
|
||||
/**
|
||||
* 将视频数据转换为嵌入向量
|
||||
*
|
||||
* @param videoDataUrl 视频的地址,必须是公开可访问的URL
|
||||
* @return 包含嵌入向量的响应对象,可能包含状态信息和嵌入结果
|
||||
*/
|
||||
Response<Embedding> embedVideo(String videoDataUrl);
|
||||
|
||||
|
||||
/**
|
||||
* 处理多模态输入并返回嵌入向量的方法
|
||||
*
|
||||
* @param input 包含多种模态信息(如图像、文本等)的输入对象
|
||||
* @return Response<Embedding> 包含嵌入向量的响应对象,Embedding通常表示输入数据的向量表示
|
||||
*/
|
||||
Response<Embedding> embedMultiModal(MultiModalInput input);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.ruoyi.service.embed.impl;
|
||||
|
||||
|
||||
import dev.langchain4j.community.model.dashscope.QwenEmbeddingModel;
|
||||
import dev.langchain4j.data.embedding.Embedding;
|
||||
import dev.langchain4j.data.segment.TextSegment;
|
||||
import dev.langchain4j.model.output.Response;
|
||||
import org.ruoyi.domain.vo.chat.ChatModelVo;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.ruoyi.enums.ModalityType;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @Author: Robust_H
|
||||
* @Date: 2025-09-30-下午3:00
|
||||
* @Description: 阿里百炼基础嵌入模型(兼容openai)
|
||||
*/
|
||||
@Component("alibailian")
|
||||
public class AliBaiLianBaseEmbedProvider extends OpenAiEmbeddingProvider {
|
||||
|
||||
|
||||
private ChatModelVo chatModelVo;
|
||||
|
||||
@Override
|
||||
public void configure(ChatModelVo config) {
|
||||
this.chatModelVo = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ModalityType> getSupportedModalities() {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response<List<Embedding>> embedAll(List<TextSegment> textSegments) {
|
||||
return QwenEmbeddingModel.builder()
|
||||
.apiKey(chatModelVo.getApiKey())
|
||||
.modelName(chatModelVo.getModelName())
|
||||
.dimension(1024)
|
||||
.build()
|
||||
.embedAll(textSegments);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
package org.ruoyi.service.embed.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import dev.langchain4j.data.embedding.Embedding;
|
||||
import dev.langchain4j.data.segment.TextSegment;
|
||||
import dev.langchain4j.model.output.Response;
|
||||
import dev.langchain4j.model.output.TokenUsage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.*;
|
||||
import org.ruoyi.domain.dto.MultiModalInput;
|
||||
import org.ruoyi.domain.dto.request.AliyunMultiModalEmbedRequest;
|
||||
import org.ruoyi.domain.dto.response.AliyunMultiModalEmbedResponse;
|
||||
import org.ruoyi.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.enums.ModalityType;
|
||||
import org.ruoyi.service.embed.MultiModalEmbedModelService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 阿里云百炼多模态嵌入模型服务实现类
|
||||
* 实现了MultiModalEmbedModelService接口,提供文本、图像和视频的嵌入向量生成服务
|
||||
*/
|
||||
@Component("bailianMultiModel")
|
||||
@Slf4j
|
||||
public class AliBaiLianMultiEmbeddingProvider implements MultiModalEmbedModelService {
|
||||
private final OkHttpClient okHttpClient;
|
||||
private ChatModelVo chatModelVo;
|
||||
|
||||
/**
|
||||
* 构造函数,初始化HTTP客户端
|
||||
* 设置连接超时、读取超时和写入超时时间
|
||||
*/
|
||||
public AliBaiLianMultiEmbeddingProvider() {
|
||||
this.okHttpClient = new OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.SECONDS)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 图像嵌入向量生成
|
||||
*
|
||||
* @param imageDataUrl 图像数据的URL
|
||||
* @return 包含图像嵌入向量的Response对象
|
||||
*/
|
||||
@Override
|
||||
public Response<Embedding> embedImage(String imageDataUrl) {
|
||||
return embedSingleModality("image", imageDataUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 视频嵌入向量生成
|
||||
*
|
||||
* @param videoDataUrl 视频数据的URL
|
||||
* @return 包含视频嵌入向量的Response对象
|
||||
*/
|
||||
@Override
|
||||
public Response<Embedding> embedVideo(String videoDataUrl) {
|
||||
return embedSingleModality("video", videoDataUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 多模态嵌入向量生成
|
||||
* 支持同时处理文本、图像和视频等多种模态的数据
|
||||
*
|
||||
* @param input 包含多种模态输入的对象
|
||||
* @return 包含多模态嵌入向量的Response对象
|
||||
*/
|
||||
@Override
|
||||
public Response<Embedding> embedMultiModal(MultiModalInput input) {
|
||||
try {
|
||||
// 构建请求内容
|
||||
List<Map<String, Object>> contents = buildContentMap(input);
|
||||
if (contents.isEmpty()) {
|
||||
throw new IllegalArgumentException("至少提供一种模态的内容");
|
||||
}
|
||||
|
||||
// 构建请求
|
||||
AliyunMultiModalEmbedRequest request = buildRequest(contents, chatModelVo);
|
||||
AliyunMultiModalEmbedResponse resp = executeRequest(request, chatModelVo);
|
||||
|
||||
// 转换为 embeddings
|
||||
Response<List<Embedding>> response = toEmbeddings(resp);
|
||||
List<Embedding> embeddings = response.content();
|
||||
|
||||
if (embeddings.isEmpty()) {
|
||||
log.warn("阿里云混合模态嵌入返回为空");
|
||||
return Response.from(Embedding.from(new float[0]), response.tokenUsage());
|
||||
}
|
||||
|
||||
// 多模态通常取第一个向量作为代表,也可以根据业务场景返回多个
|
||||
return Response.from(embeddings.get(0), response.tokenUsage());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("阿里云混合模态嵌入失败", e);
|
||||
throw new IllegalArgumentException("阿里云混合模态嵌入失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置模型参数
|
||||
*
|
||||
* @param config 模型配置信息
|
||||
*/
|
||||
@Override
|
||||
public void configure(ChatModelVo config) {
|
||||
this.chatModelVo = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支持的模态类型
|
||||
*
|
||||
* @return 支持的模态类型集合
|
||||
*/
|
||||
@Override
|
||||
public Set<ModalityType> getSupportedModalities() {
|
||||
return Set.of(ModalityType.TEXT, ModalityType.VIDEO, ModalityType.IMAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量文本嵌入向量生成
|
||||
*
|
||||
* @param textSegments 文本段列表
|
||||
* @return 包含所有文本嵌入向量的Response对象
|
||||
*/
|
||||
@Override
|
||||
public Response<List<Embedding>> embedAll(List<TextSegment> textSegments) {
|
||||
if (textSegments.isEmpty()) return Response.from(Collections.emptyList());
|
||||
|
||||
try {
|
||||
List<Map<String, Object>> contents = new ArrayList<>();
|
||||
for (TextSegment segment : textSegments) {
|
||||
contents.add(Map.of("text", segment.text()));
|
||||
}
|
||||
|
||||
AliyunMultiModalEmbedRequest request = buildRequest(contents, chatModelVo);
|
||||
AliyunMultiModalEmbedResponse resp = executeRequest(request, chatModelVo);
|
||||
|
||||
return toEmbeddings(resp);
|
||||
} catch (Exception e) {
|
||||
log.error("阿里云文本嵌入失败", e);
|
||||
throw new IllegalArgumentException("阿里云文本嵌入失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 单模态嵌入(图片/视频/单条文本)复用方法
|
||||
*
|
||||
* @param key 模态类型(image/video/text)
|
||||
* @param dataUrl 数据URL
|
||||
* @return 包含嵌入向量的Response对象
|
||||
*/
|
||||
|
||||
public Response<Embedding> embedSingleModality(String key, String dataUrl) {
|
||||
try {
|
||||
AliyunMultiModalEmbedRequest request = buildRequest(List.of(Map.of(key, dataUrl)), chatModelVo);
|
||||
AliyunMultiModalEmbedResponse resp = executeRequest(request, chatModelVo);
|
||||
|
||||
Response<List<Embedding>> response = toEmbeddings(resp);
|
||||
List<Embedding> embeddings = response.content();
|
||||
|
||||
if (embeddings.isEmpty()) {
|
||||
log.warn("阿里云 {} 嵌入返回为空", key);
|
||||
return Response.from(Embedding.from(new float[0]), response.tokenUsage());
|
||||
}
|
||||
|
||||
return Response.from(embeddings.get(0), response.tokenUsage());
|
||||
} catch (Exception e) {
|
||||
log.error("阿里云 {} 嵌入失败", key, e);
|
||||
throw new IllegalArgumentException("阿里云 " + key + " 嵌入失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建请求对象
|
||||
*
|
||||
* @param contents 请求内容列表
|
||||
* @param chatModelVo 模型配置信息
|
||||
* @return 构建好的请求对象
|
||||
*/
|
||||
private AliyunMultiModalEmbedRequest buildRequest(List<Map<String, Object>> contents, ChatModelVo chatModelVo) {
|
||||
if (contents.isEmpty()) throw new IllegalArgumentException("请求内容不能为空");
|
||||
return AliyunMultiModalEmbedRequest.create(chatModelVo.getModelName(), contents);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 HTTP 请求并解析响应
|
||||
*
|
||||
* @param request 请求对象
|
||||
* @param chatModelVo 模型配置信息
|
||||
* @return API响应对象
|
||||
* @throws IOException IO异常
|
||||
*/
|
||||
private AliyunMultiModalEmbedResponse executeRequest(AliyunMultiModalEmbedRequest request, ChatModelVo chatModelVo) throws IOException {
|
||||
String jsonBody = request.toJson();
|
||||
RequestBody body = RequestBody.create(jsonBody, MediaType.get("application/json"));
|
||||
|
||||
Request httpRequest = new Request.Builder()
|
||||
.url(chatModelVo.getApiHost())
|
||||
.addHeader("Authorization", "Bearer " + chatModelVo.getApiKey())
|
||||
.post(body)
|
||||
.build();
|
||||
|
||||
try (okhttp3.Response response = okHttpClient.newCall(httpRequest).execute()) {
|
||||
if (!response.isSuccessful()) {
|
||||
String err = response.body() != null ? response.body().string() : "无错误信息";
|
||||
throw new IllegalArgumentException("API调用失败: " + response.code() + " - " + err, null);
|
||||
}
|
||||
|
||||
ResponseBody responseBody = response.body();
|
||||
if (responseBody == null) throw new IllegalArgumentException("响应体为空", null);
|
||||
|
||||
return parseEmbeddingsFromResponse(responseBody.string());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析嵌入向量列表
|
||||
*
|
||||
* @param responseBody API响应的JSON字符串
|
||||
* @return 嵌入向量响应对象
|
||||
* @throws IOException IO异常
|
||||
*/
|
||||
private AliyunMultiModalEmbedResponse parseEmbeddingsFromResponse(String responseBody) throws IOException {
|
||||
ObjectMapper objectMapper1 = new ObjectMapper();
|
||||
return objectMapper1.readValue(responseBody, AliyunMultiModalEmbedResponse.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 API 请求内容 Map
|
||||
*
|
||||
* @param input 多模态输入对象
|
||||
* @return 包含各种模态内容的Map列表
|
||||
*/
|
||||
private List<Map<String, Object>> buildContentMap(MultiModalInput input) {
|
||||
List<Map<String, Object>> contents = new ArrayList<>();
|
||||
|
||||
if (input.getText() != null && !input.getText().isBlank()) {
|
||||
contents.add(Map.of("text", input.getText()));
|
||||
}
|
||||
if (input.getImageUrl() != null && !input.getImageUrl().isBlank()) {
|
||||
contents.add(Map.of("image", input.getImageUrl()));
|
||||
}
|
||||
if (input.getVideoUrl() != null && !input.getVideoUrl().isBlank()) {
|
||||
contents.add(Map.of("video", input.getVideoUrl()));
|
||||
}
|
||||
if (input.getMultiImageUrls() != null && input.getMultiImageUrls().length > 0) {
|
||||
contents.add(Map.of("multi_images", Arrays.asList(input.getMultiImageUrls())));
|
||||
}
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 API 原始响应解析为 LangChain4j 的 Response<Embedding>
|
||||
*
|
||||
* @param resp API原始响应对象
|
||||
* @return 包含嵌入向量和token使用情况的Response对象
|
||||
*/
|
||||
private Response<List<Embedding>> toEmbeddings(AliyunMultiModalEmbedResponse resp) {
|
||||
if (resp == null || resp.output() == null || resp.output().embeddings() == null) {
|
||||
return Response.from(Collections.emptyList());
|
||||
}
|
||||
|
||||
// 转换 double -> float
|
||||
List<Embedding> embeddings = resp.output().embeddings().stream()
|
||||
.map(item -> {
|
||||
float[] vector = new float[item.embedding().size()];
|
||||
for (int i = 0; i < item.embedding().size(); i++) {
|
||||
vector[i] = item.embedding().get(i).floatValue();
|
||||
}
|
||||
return Embedding.from(vector);
|
||||
})
|
||||
.toList();
|
||||
|
||||
// 构建 TokenUsage
|
||||
TokenUsage tokenUsage = null;
|
||||
if (resp.usage() != null) {
|
||||
tokenUsage = new TokenUsage(
|
||||
resp.usage().input_tokens(),
|
||||
resp.usage().image_tokens(),
|
||||
resp.usage().input_tokens() + resp.usage().image_tokens()
|
||||
);
|
||||
}
|
||||
|
||||
return Response.from(embeddings, tokenUsage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.ruoyi.service.embed.impl;
|
||||
|
||||
import dev.langchain4j.data.embedding.Embedding;
|
||||
import dev.langchain4j.data.segment.TextSegment;
|
||||
import dev.langchain4j.model.ollama.OllamaEmbeddingModel;
|
||||
import dev.langchain4j.model.output.Response;
|
||||
import org.ruoyi.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.enums.ModalityType;
|
||||
import org.ruoyi.service.embed.BaseEmbedModelService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @Author: Robust_H
|
||||
* @Date: 2025-09-30-下午3:00
|
||||
* @Description: Ollama嵌入模型
|
||||
*/
|
||||
@Component("ollama")
|
||||
public class OllamaEmbeddingProvider implements BaseEmbedModelService {
|
||||
private ChatModelVo chatModelVo;
|
||||
|
||||
@Override
|
||||
public void configure(ChatModelVo config) {
|
||||
this.chatModelVo = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ModalityType> getSupportedModalities() {
|
||||
return Set.of(ModalityType.TEXT);
|
||||
}
|
||||
|
||||
// ollama不能设置embedding维度,使用milvus时请注意!!创建向量表时需要先设定维度大小
|
||||
@Override
|
||||
public Response<List<Embedding>> embedAll(List<TextSegment> textSegments) {
|
||||
return OllamaEmbeddingModel.builder()
|
||||
.baseUrl(chatModelVo.getApiHost())
|
||||
.modelName(chatModelVo.getModelName())
|
||||
.build()
|
||||
.embedAll(textSegments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.ruoyi.service.embed.impl;
|
||||
|
||||
import dev.langchain4j.data.embedding.Embedding;
|
||||
import dev.langchain4j.data.segment.TextSegment;
|
||||
import dev.langchain4j.model.openai.OpenAiEmbeddingModel;
|
||||
import dev.langchain4j.model.output.Response;
|
||||
import org.ruoyi.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.enums.ModalityType;
|
||||
import org.ruoyi.service.embed.BaseEmbedModelService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @Author: Robust_H
|
||||
* @Date: 2025-09-30-下午3:59
|
||||
* @Description: OpenAi嵌入模型
|
||||
*/
|
||||
@Component("openai")
|
||||
public class OpenAiEmbeddingProvider implements BaseEmbedModelService {
|
||||
protected ChatModelVo chatModelVo;
|
||||
|
||||
@Override
|
||||
public void configure(ChatModelVo config) {
|
||||
this.chatModelVo = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ModalityType> getSupportedModalities() {
|
||||
return Set.of(ModalityType.TEXT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response<List<Embedding>> embedAll(List<TextSegment> textSegments) {
|
||||
return OpenAiEmbeddingModel.builder()
|
||||
.baseUrl(chatModelVo.getApiHost())
|
||||
.apiKey(chatModelVo.getApiKey())
|
||||
.modelName(chatModelVo.getModelName())
|
||||
.dimensions(chatModelVo.getDimension())
|
||||
.build()
|
||||
.embedAll(textSegments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.ruoyi.service.embed.impl;
|
||||
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @Author: Robust_H
|
||||
* @Date: 2025-09-30-下午3:59
|
||||
* @Description: 硅基流动(兼容 OpenAi)
|
||||
*/
|
||||
@Component("siliconflow")
|
||||
public class SiliconFlowEmbeddingProvider extends OpenAiEmbeddingProvider {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//package org.ruoyi.service.embed.impl;
|
||||
//
|
||||
//import dev.langchain4j.community.model.zhipu.ZhipuAiEmbeddingModel;
|
||||
//import dev.langchain4j.data.embedding.Embedding;
|
||||
//import dev.langchain4j.data.segment.TextSegment;
|
||||
//import dev.langchain4j.model.output.Response;
|
||||
//import org.ruoyi.domain.vo.chat.ChatModelVo;
|
||||
//import org.ruoyi.enums.ModalityType;
|
||||
//import org.ruoyi.service.embed.BaseEmbedModelService;
|
||||
//import org.springframework.stereotype.Component;
|
||||
//
|
||||
//import java.util.List;
|
||||
//import java.util.Set;
|
||||
//
|
||||
///**
|
||||
// * @Author: Robust_H
|
||||
// * @Date: 2025-09-30-下午4:02
|
||||
// * @Description: 智谱AI
|
||||
// */
|
||||
//@Component("zhipu")
|
||||
//public class ZhiPuAiEmbeddingProvider implements BaseEmbedModelService {
|
||||
// private ChatModelVo chatModelVo;
|
||||
//
|
||||
// @Override
|
||||
// public void configure(ChatModelVo config) {
|
||||
// this.chatModelVo = config;
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public Set<ModalityType> getSupportedModalities() {
|
||||
// return Set.of();
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public Response<List<Embedding>> embedAll(List<TextSegment> textSegments) {
|
||||
// return ZhipuAiEmbeddingModel.builder()
|
||||
// .baseUrl(chatModelVo.getApiHost())
|
||||
// .apiKey(chatModelVo.getApiKey())
|
||||
// .model(chatModelVo.getModelName())
|
||||
// .dimensions(chatModelVo.getDimension())
|
||||
// .build()
|
||||
// .embedAll(textSegments);
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,136 @@
|
||||
package org.ruoyi.service.graph;
|
||||
|
||||
|
||||
import org.ruoyi.domain.bo.graph.GraphBuildTask;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 图谱构建任务服务接口
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-09-30
|
||||
*/
|
||||
public interface IGraphBuildTaskService {
|
||||
|
||||
/**
|
||||
* 创建构建任务
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param docId 文档ID(可选)
|
||||
* @param taskType 任务类型
|
||||
* @return 任务信息
|
||||
*/
|
||||
GraphBuildTask createTask(String graphUuid, String knowledgeId, String docId, Integer taskType);
|
||||
|
||||
/**
|
||||
* 启动构建任务(异步)
|
||||
*
|
||||
* @param taskUuid 任务UUID
|
||||
* @return 异步结果
|
||||
*/
|
||||
void startTask(String taskUuid);
|
||||
|
||||
/**
|
||||
* 根据UUID获取任务
|
||||
*
|
||||
* @param taskUuid 任务UUID
|
||||
* @return 任务信息
|
||||
*/
|
||||
GraphBuildTask getByUuid(String taskUuid);
|
||||
|
||||
/**
|
||||
* 根据图谱UUID获取任务列表
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @return 任务列表
|
||||
*/
|
||||
List<GraphBuildTask> listByGraphUuid(String graphUuid);
|
||||
|
||||
/**
|
||||
* 获取图谱的最新构建任务
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @return 最新任务
|
||||
*/
|
||||
GraphBuildTask getLatestTask(String graphUuid);
|
||||
|
||||
/**
|
||||
* 根据知识库ID获取任务列表
|
||||
*
|
||||
* @param knowledgeId 知识库ID
|
||||
* @return 任务列表
|
||||
*/
|
||||
List<GraphBuildTask> listByKnowledgeId(String knowledgeId);
|
||||
|
||||
/**
|
||||
* 获取待执行和执行中的任务
|
||||
*
|
||||
* @return 任务列表
|
||||
*/
|
||||
List<GraphBuildTask> getPendingAndRunningTasks();
|
||||
|
||||
/**
|
||||
* 更新任务进度
|
||||
*
|
||||
* @param taskUuid 任务UUID
|
||||
* @param progress 进度百分比
|
||||
* @param processedDocs 已处理文档数
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateProgress(String taskUuid, Integer progress, Integer processedDocs);
|
||||
|
||||
/**
|
||||
* 更新任务状态
|
||||
*
|
||||
* @param taskUuid 任务UUID
|
||||
* @param status 状态
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateStatus(String taskUuid, Integer status);
|
||||
|
||||
/**
|
||||
* 更新提取统计信息
|
||||
*
|
||||
* @param taskUuid 任务UUID
|
||||
* @param extractedEntities 提取的实体数
|
||||
* @param extractedRelations 提取的关系数
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateExtractionStats(String taskUuid, Integer extractedEntities, Integer extractedRelations);
|
||||
|
||||
/**
|
||||
* 标记任务为成功
|
||||
*
|
||||
* @param taskUuid 任务UUID
|
||||
* @param resultSummary 结果摘要
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean markSuccess(String taskUuid, String resultSummary);
|
||||
|
||||
/**
|
||||
* 标记任务为失败
|
||||
*
|
||||
* @param taskUuid 任务UUID
|
||||
* @param errorMessage 错误信息
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean markFailed(String taskUuid, String errorMessage);
|
||||
|
||||
/**
|
||||
* 取消任务
|
||||
*
|
||||
* @param taskUuid 任务UUID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean cancelTask(String taskUuid);
|
||||
|
||||
/**
|
||||
* 重试失败的任务
|
||||
*
|
||||
* @param taskUuid 任务UUID
|
||||
* @return 新任务UUID
|
||||
*/
|
||||
String retryTask(String taskUuid);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.ruoyi.service.graph;
|
||||
|
||||
|
||||
import org.ruoyi.domain.dto.GraphExtractionResult;
|
||||
|
||||
/**
|
||||
* 图谱实体关系抽取服务接口
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-09-30
|
||||
*/
|
||||
public interface IGraphExtractionService {
|
||||
|
||||
/**
|
||||
* 从文本中抽取实体和关系
|
||||
*
|
||||
* @param text 输入文本
|
||||
* @return 抽取结果
|
||||
*/
|
||||
GraphExtractionResult extractFromText(String text);
|
||||
|
||||
/**
|
||||
* 从文本中抽取实体和关系(自定义实体类型)
|
||||
*
|
||||
* @param text 输入文本
|
||||
* @param entityTypes 实体类型列表
|
||||
* @return 抽取结果
|
||||
*/
|
||||
GraphExtractionResult extractFromText(String text, String[] entityTypes);
|
||||
|
||||
/**
|
||||
* 从文本中抽取实体和关系(使用指定的LLM模型)
|
||||
*
|
||||
* @param text 输入文本
|
||||
* @param modelName LLM模型名称
|
||||
* @return 抽取结果
|
||||
*/
|
||||
GraphExtractionResult extractFromTextWithModel(String text, String modelName);
|
||||
|
||||
/**
|
||||
* 解析LLM响应为实体和关系
|
||||
*
|
||||
* @param response LLM响应文本
|
||||
* @return 抽取结果
|
||||
*/
|
||||
GraphExtractionResult parseGraphResponse(String response);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package org.ruoyi.service.graph;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import org.ruoyi.domain.bo.graph.GraphInstance;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 图谱实例服务接口
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-09-30
|
||||
*/
|
||||
public interface IGraphInstanceService {
|
||||
|
||||
/**
|
||||
* 创建图谱实例
|
||||
*
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param graphName 图谱名称
|
||||
* @param config 配置信息
|
||||
* @return 图谱实例
|
||||
*/
|
||||
GraphInstance createInstance(String knowledgeId, String graphName, String config);
|
||||
|
||||
/**
|
||||
* 根据主键ID获取图谱实例
|
||||
*
|
||||
* @param id 主键ID
|
||||
* @return 图谱实例
|
||||
*/
|
||||
GraphInstance getById(Long id);
|
||||
|
||||
/**
|
||||
* 根据UUID获取图谱实例
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @return 图谱实例
|
||||
*/
|
||||
GraphInstance getByUuid(String graphUuid);
|
||||
|
||||
/**
|
||||
* 更新图谱实例
|
||||
*
|
||||
* @param instance 图谱实例
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateInstance(GraphInstance instance);
|
||||
|
||||
/**
|
||||
* 根据知识库ID获取图谱列表
|
||||
*
|
||||
* @param knowledgeId 知识库ID
|
||||
* @return 图谱实例列表
|
||||
*/
|
||||
List<GraphInstance> listByKnowledgeId(String knowledgeId);
|
||||
|
||||
/**
|
||||
* 条件查询图谱实例列表(分页)
|
||||
*
|
||||
* @param page 分页对象
|
||||
* @param instanceName 图谱名称(模糊查询)
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param graphStatus 图谱状态码
|
||||
* @return 分页结果
|
||||
*/
|
||||
Page<GraphInstance> queryPage(Page<GraphInstance> page, String instanceName, String knowledgeId, Integer graphStatus);
|
||||
|
||||
/**
|
||||
* 更新图谱状态
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @param status 状态
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateStatus(String graphUuid, Integer status);
|
||||
|
||||
/**
|
||||
* 更新图谱统计信息
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @param nodeCount 节点数量
|
||||
* @param relationshipCount 关系数量
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateCounts(String graphUuid, Integer nodeCount, Integer relationshipCount);
|
||||
|
||||
/**
|
||||
* 更新图谱配置
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @param config 配置信息
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateConfig(String graphUuid, String config);
|
||||
|
||||
/**
|
||||
* 删除图谱实例(软删除)
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deleteInstance(String graphUuid);
|
||||
|
||||
/**
|
||||
* 物理删除图谱实例及其数据
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deleteInstanceAndData(String graphUuid);
|
||||
|
||||
/**
|
||||
* 获取图谱统计信息
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @return 统计信息
|
||||
*/
|
||||
java.util.Map<String, Object> getStatistics(String graphUuid);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.ruoyi.service.graph;
|
||||
|
||||
|
||||
import org.ruoyi.domain.vo.chat.ChatModelVo;
|
||||
|
||||
/**
|
||||
* 图谱LLM服务接口
|
||||
* 参考 ruoyi-chat 的 IChatService 设计
|
||||
* 支持多种LLM模型(OpenAI、Qwen、Zhipu等)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-10-11
|
||||
*/
|
||||
public interface IGraphLLMService {
|
||||
|
||||
/**
|
||||
* 调用LLM进行图谱实体关系抽取
|
||||
*
|
||||
* @param prompt 提示词(包含文本和抽取指令)
|
||||
* @param chatModel 模型配置
|
||||
* @return LLM响应文本
|
||||
*/
|
||||
String extractGraph(String prompt, ChatModelVo chatModel);
|
||||
|
||||
/**
|
||||
* 获取此服务支持的模型类别
|
||||
* 例如: "openai", "qwen", "zhipu", "ollama"
|
||||
*
|
||||
* @return 模型类别标识
|
||||
*/
|
||||
String getCategory();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package org.ruoyi.service.graph;
|
||||
|
||||
import org.ruoyi.domain.dto.GraphExtractionResult;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* GraphRAG服务接口
|
||||
* 负责文档的图谱化处理和基于图谱的检索
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-09-30
|
||||
*/
|
||||
public interface IGraphRAGService {
|
||||
|
||||
/**
|
||||
* 将文本入库到图谱
|
||||
*
|
||||
* @param text 文本内容
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param metadata 元数据
|
||||
* @return 抽取结果
|
||||
*/
|
||||
GraphExtractionResult ingestText(String text, String knowledgeId, Map<String, Object> metadata);
|
||||
|
||||
/**
|
||||
* 将文本入库到图谱(指定模型)
|
||||
*
|
||||
* @param text 文本内容
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param metadata 元数据
|
||||
* @param modelName LLM模型名称
|
||||
* @return 抽取结果
|
||||
*/
|
||||
GraphExtractionResult ingestTextWithModel(String text, String knowledgeId, Map<String, Object> metadata, String modelName);
|
||||
|
||||
/**
|
||||
* 将文档入库到图谱(自动分片)
|
||||
*
|
||||
* @param documentText 文档内容
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param metadata 元数据
|
||||
* @return 总抽取结果(合并所有分片)
|
||||
*/
|
||||
GraphExtractionResult ingestDocument(String documentText, String knowledgeId, Map<String, Object> metadata);
|
||||
|
||||
/**
|
||||
* 将文档入库到图谱(自动分片,指定模型)
|
||||
*
|
||||
* @param documentText 文档内容
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param metadata 元数据
|
||||
* @param modelName LLM模型名称
|
||||
* @return 总抽取结果(合并所有分片)
|
||||
*/
|
||||
GraphExtractionResult ingestDocumentWithModel(String documentText, String knowledgeId, Map<String, Object> metadata, String modelName);
|
||||
|
||||
/**
|
||||
* 基于图谱检索相关内容
|
||||
*
|
||||
* @param query 查询文本
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param maxResults 最大结果数
|
||||
* @return 检索到的相关实体和关系
|
||||
*/
|
||||
String retrieveFromGraph(String query, String knowledgeId, int maxResults);
|
||||
|
||||
/**
|
||||
* 删除知识库的图谱数据
|
||||
*
|
||||
* @param knowledgeId 知识库ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deleteGraphData(String knowledgeId);
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package org.ruoyi.service.graph;
|
||||
|
||||
|
||||
import org.ruoyi.domain.bo.graph.GraphEdge;
|
||||
import org.ruoyi.domain.bo.graph.GraphVertex;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 图存储服务接口
|
||||
* 核心服务:负责与Neo4j图数据库交互
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-09-30
|
||||
*/
|
||||
public interface IGraphStoreService {
|
||||
|
||||
// ==================== 节点操作 ====================
|
||||
|
||||
/**
|
||||
* 添加单个节点
|
||||
*
|
||||
* @param vertex 节点信息
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean addVertex(GraphVertex vertex);
|
||||
|
||||
/**
|
||||
* 批量添加节点
|
||||
*
|
||||
* @param vertices 节点列表
|
||||
* @return 成功添加的节点数
|
||||
*/
|
||||
int addVertices(List<GraphVertex> vertices);
|
||||
|
||||
/**
|
||||
* 获取节点信息
|
||||
*
|
||||
* @param nodeId 节点ID
|
||||
* @param graphUuid 图谱UUID
|
||||
* @return 节点信息
|
||||
*/
|
||||
GraphVertex getVertex(String nodeId, String graphUuid);
|
||||
|
||||
/**
|
||||
* 根据条件搜索节点
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @param label 节点标签(可选)
|
||||
* @param limit 返回数量限制
|
||||
* @return 节点列表
|
||||
*/
|
||||
List<GraphVertex> searchVertices(String graphUuid, String label, Integer limit);
|
||||
|
||||
/**
|
||||
* 根据名称搜索节点
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @param name 节点名称
|
||||
* @return 节点列表
|
||||
*/
|
||||
List<GraphVertex> searchVerticesByName(String graphUuid, String name);
|
||||
|
||||
/**
|
||||
* 根据关键词和知识库ID搜索节点
|
||||
*
|
||||
* @param keyword 关键词
|
||||
* @param knowledgeId 知识库ID(可选)
|
||||
* @param limit 限制数量
|
||||
* @return 节点列表
|
||||
*/
|
||||
List<GraphVertex> searchVerticesByName(String keyword, String knowledgeId, Integer limit);
|
||||
|
||||
/**
|
||||
* 根据知识库ID查询节点
|
||||
*
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param limit 限制数量
|
||||
* @return 节点列表
|
||||
*/
|
||||
List<GraphVertex> queryVerticesByKnowledgeId(String knowledgeId, Integer limit);
|
||||
|
||||
/**
|
||||
* 更新节点信息
|
||||
*
|
||||
* @param vertex 节点信息
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateVertex(GraphVertex vertex);
|
||||
|
||||
/**
|
||||
* 删除节点
|
||||
*
|
||||
* @param nodeId 节点ID
|
||||
* @param graphUuid 图谱UUID
|
||||
* @param includeEdges 是否同时删除相关关系
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deleteVertex(String nodeId, String graphUuid, boolean includeEdges);
|
||||
|
||||
// ==================== 关系操作 ====================
|
||||
|
||||
/**
|
||||
* 添加关系
|
||||
*
|
||||
* @param edge 关系信息
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean addEdge(GraphEdge edge);
|
||||
|
||||
/**
|
||||
* 批量添加关系
|
||||
*
|
||||
* @param edges 关系列表
|
||||
* @return 成功添加的关系数
|
||||
*/
|
||||
int addEdges(List<GraphEdge> edges);
|
||||
|
||||
/**
|
||||
* 获取关系信息
|
||||
*
|
||||
* @param edgeId 关系ID
|
||||
* @param graphUuid 图谱UUID
|
||||
* @return 关系信息
|
||||
*/
|
||||
GraphEdge getEdge(String edgeId, String graphUuid);
|
||||
|
||||
/**
|
||||
* 搜索关系
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @param sourceNodeId 源节点ID(可选)
|
||||
* @param targetNodeId 目标节点ID(可选)
|
||||
* @param limit 返回数量限制
|
||||
* @return 关系列表
|
||||
*/
|
||||
List<GraphEdge> searchEdges(String graphUuid, String sourceNodeId, String targetNodeId, Integer limit);
|
||||
|
||||
/**
|
||||
* 根据知识库ID查询关系
|
||||
*
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param limit 限制数量
|
||||
* @return 关系列表
|
||||
*/
|
||||
List<GraphEdge> queryEdgesByKnowledgeId(String knowledgeId, Integer limit);
|
||||
|
||||
/**
|
||||
* 获取节点的所有关系
|
||||
*
|
||||
* @param nodeId 节点ID
|
||||
* @param graphUuid 图谱UUID
|
||||
* @param direction 方向: IN(入边), OUT(出边), BOTH(双向)
|
||||
* @return 关系列表
|
||||
*/
|
||||
List<GraphEdge> getNodeEdges(String nodeId, String graphUuid, String direction);
|
||||
|
||||
/**
|
||||
* 更新关系信息
|
||||
*
|
||||
* @param edge 关系信息
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateEdge(GraphEdge edge);
|
||||
|
||||
/**
|
||||
* 删除关系
|
||||
*
|
||||
* @param edgeId 关系ID
|
||||
* @param graphUuid 图谱UUID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deleteEdge(String edgeId, String graphUuid);
|
||||
|
||||
// ==================== 图谱管理 ====================
|
||||
|
||||
/**
|
||||
* 创建图谱Schema
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean createGraphSchema(String graphUuid);
|
||||
|
||||
/**
|
||||
* 删除整个图谱数据
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deleteGraph(String graphUuid);
|
||||
|
||||
/**
|
||||
* 根据知识库ID删除图谱数据
|
||||
*
|
||||
* @param knowledgeId 知识库ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deleteByKnowledgeId(String knowledgeId);
|
||||
|
||||
/**
|
||||
* 获取图谱统计信息
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @return 统计信息 {nodeCount, relationshipCount}
|
||||
*/
|
||||
java.util.Map<String, Object> getGraphStatistics(String graphUuid);
|
||||
|
||||
/**
|
||||
* 根据知识库ID获取统计信息
|
||||
*
|
||||
* @param knowledgeId 知识库ID
|
||||
* @return 统计信息
|
||||
*/
|
||||
java.util.Map<String, Object> getStatistics(String knowledgeId);
|
||||
|
||||
// ==================== 高级查询 ====================
|
||||
|
||||
/**
|
||||
* 查找两个节点之间的路径
|
||||
*
|
||||
* @param sourceNodeId 源节点ID
|
||||
* @param targetNodeId 目标节点ID
|
||||
* @param graphUuid 图谱UUID
|
||||
* @param maxDepth 最大深度
|
||||
* @return 路径列表
|
||||
*/
|
||||
List<List<GraphVertex>> findPaths(String sourceNodeId, String targetNodeId, String graphUuid, Integer maxDepth);
|
||||
|
||||
/**
|
||||
* 查找路径(简化版)
|
||||
*
|
||||
* @param startNodeId 起始节点ID
|
||||
* @param endNodeId 结束节点ID
|
||||
* @param maxDepth 最大深度
|
||||
* @return 路径列表
|
||||
*/
|
||||
List<List<GraphVertex>> findPaths(String startNodeId, String endNodeId, Integer maxDepth);
|
||||
|
||||
/**
|
||||
* 查找节点的邻居节点
|
||||
*
|
||||
* @param nodeId 节点ID
|
||||
* @param graphUuid 图谱UUID
|
||||
* @param depth 深度(几度关系)
|
||||
* @return 邻居节点列表
|
||||
*/
|
||||
List<GraphVertex> findNeighbors(String nodeId, String graphUuid, Integer depth);
|
||||
|
||||
/**
|
||||
* 获取节点的邻居(简化版)
|
||||
*
|
||||
* @param nodeId 节点ID
|
||||
* @param knowledgeId 知识库ID(可选)
|
||||
* @param limit 限制数量
|
||||
* @return 邻居节点列表
|
||||
*/
|
||||
List<GraphVertex> getNeighbors(String nodeId, String knowledgeId, Integer limit);
|
||||
|
||||
/**
|
||||
* 执行自定义Cypher查询
|
||||
*
|
||||
* @param cypher Cypher查询语句
|
||||
* @param params 参数
|
||||
* @return 查询结果
|
||||
*/
|
||||
List<java.util.Map<String, Object>> executeCypher(String cypher, java.util.Map<String, Object> params);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.ruoyi.service.graph.impl;
|
||||
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.chat.response.ChatResponse;
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.service.graph.IGraphLLMService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* DeepSeek 图谱LLM服务实现
|
||||
* 支持 DeepSeek 系列模型
|
||||
* <p>
|
||||
* 注意:使用 langchain4j 的 OpenAiStreamingChatModel,通过 CompletableFuture 转换为同步调用
|
||||
* 参考 DeepSeekChatImpl 的实现,但改为同步模式
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-10-13
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DeepSeekGraphLLMServiceImpl implements IGraphLLMService {
|
||||
|
||||
@Override
|
||||
public String extractGraph(String prompt, ChatModelVo chatModel) {
|
||||
log.info("DeepSeek模型调用: model={}, apiHost={}, 提示词长度={}",
|
||||
chatModel.getModelName(), chatModel.getApiHost(), prompt.length());
|
||||
|
||||
try {
|
||||
// 使用 langchain4j 的 OpenAiStreamingChatModel(参考 DeepSeekChatImpl)
|
||||
StreamingChatModel streamingModel = OpenAiStreamingChatModel.builder()
|
||||
.baseUrl(chatModel.getApiHost())
|
||||
.apiKey(chatModel.getApiKey())
|
||||
.modelName(chatModel.getModelName())
|
||||
.temperature(0.8)
|
||||
.logRequests(false)
|
||||
.logResponses(false)
|
||||
.build();
|
||||
|
||||
// 用于收集完整响应
|
||||
StringBuilder fullResponse = new StringBuilder();
|
||||
CompletableFuture<String> responseFuture = new CompletableFuture<>();
|
||||
|
||||
// 发送流式消息,但通过 CompletableFuture 转换为同步
|
||||
long startTime = System.currentTimeMillis();
|
||||
streamingModel.chat(prompt, new StreamingChatResponseHandler() {
|
||||
@Override
|
||||
public void onPartialResponse(String partialResponse) {
|
||||
fullResponse.append(partialResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleteResponse(ChatResponse completeResponse) {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
String responseText = fullResponse.toString();
|
||||
log.info("DeepSeek模型响应成功: 耗时={}ms, 响应长度={}", duration, responseText.length());
|
||||
responseFuture.complete(responseText);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
log.error("DeepSeek模型调用错误: {}", error.getMessage());
|
||||
responseFuture.completeExceptionally(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 同步等待结果(最多2分钟)
|
||||
return responseFuture.get(2, TimeUnit.MINUTES);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("DeepSeek模型调用失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("DeepSeek模型调用失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return "deepseek"; // 对应 ChatModel 表中的 category 字段
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package org.ruoyi.service.graph.impl;
|
||||
|
||||
import io.github.imfangs.dify.client.DifyClient;
|
||||
import io.github.imfangs.dify.client.DifyClientFactory;
|
||||
import io.github.imfangs.dify.client.callback.ChatStreamCallback;
|
||||
import io.github.imfangs.dify.client.enums.ResponseMode;
|
||||
import io.github.imfangs.dify.client.event.ErrorEvent;
|
||||
import io.github.imfangs.dify.client.event.MessageEndEvent;
|
||||
import io.github.imfangs.dify.client.event.MessageEvent;
|
||||
import io.github.imfangs.dify.client.model.DifyConfig;
|
||||
import io.github.imfangs.dify.client.model.chat.ChatMessage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.service.graph.IGraphLLMService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Dify 图谱LLM服务实现
|
||||
* 支持 Dify 平台的对话模型
|
||||
* <p>
|
||||
* 注意:Dify 使用流式调用,通过 CompletableFuture 实现同步等待
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-10-11
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DifyGraphLLMServiceImpl implements IGraphLLMService {
|
||||
|
||||
@Override
|
||||
public String extractGraph(String prompt, ChatModelVo chatModel) {
|
||||
log.info("Dify模型调用: model={}, apiHost={}, 提示词长度={}",
|
||||
chatModel.getModelName(), chatModel.getApiHost(), prompt.length());
|
||||
|
||||
try {
|
||||
// 创建 Dify 客户端配置
|
||||
DifyConfig config = DifyConfig.builder()
|
||||
.baseUrl(chatModel.getApiHost())
|
||||
.apiKey(chatModel.getApiKey())
|
||||
.connectTimeout(5000)
|
||||
.readTimeout(120000) // 2分钟超时
|
||||
.writeTimeout(30000)
|
||||
.build();
|
||||
|
||||
DifyClient chatClient = DifyClientFactory.createClient(config);
|
||||
|
||||
// 创建聊天消息(使用流式模式)
|
||||
ChatMessage message = ChatMessage.builder()
|
||||
.query(prompt)
|
||||
.user("graph-system") // 图谱系统用户
|
||||
.responseMode(ResponseMode.STREAMING) // 流式模式
|
||||
.build();
|
||||
|
||||
// 用于收集完整响应
|
||||
StringBuilder fullResponse = new StringBuilder();
|
||||
CompletableFuture<String> responseFuture = new CompletableFuture<>();
|
||||
|
||||
// 发送流式消息
|
||||
long startTime = System.currentTimeMillis();
|
||||
chatClient.sendChatMessageStream(message, new ChatStreamCallback() {
|
||||
@Override
|
||||
public void onMessage(MessageEvent event) {
|
||||
fullResponse.append(event.getAnswer());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageEnd(MessageEndEvent event) {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
String responseText = fullResponse.toString();
|
||||
log.info("Dify模型响应成功: 耗时={}ms, 响应长度={}, messageId={}",
|
||||
duration, responseText.length(), event.getMessageId());
|
||||
responseFuture.complete(responseText);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorEvent event) {
|
||||
log.error("Dify模型调用错误: {}", event.getMessage());
|
||||
responseFuture.completeExceptionally(
|
||||
new RuntimeException("Dify调用错误: " + event.getMessage())
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Throwable throwable) {
|
||||
log.error("Dify模型调用异常: {}", throwable.getMessage(), throwable);
|
||||
responseFuture.completeExceptionally(throwable);
|
||||
}
|
||||
});
|
||||
|
||||
// 同步等待结果(最多2分钟)
|
||||
return responseFuture.get(2, TimeUnit.MINUTES);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Dify模型调用失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("Dify模型调用失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return "dify"; // 对应 ChatModel 表中的 category 字段
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,367 @@
|
||||
package org.ruoyi.service.graph.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.config.GraphExtractPrompt;
|
||||
import org.ruoyi.constant.GraphConstants;
|
||||
import org.ruoyi.domain.bo.chat.ChatModelBo;
|
||||
import org.ruoyi.domain.dto.ExtractedEntity;
|
||||
import org.ruoyi.domain.dto.ExtractedRelation;
|
||||
import org.ruoyi.domain.dto.GraphExtractionResult;
|
||||
import org.ruoyi.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.factory.GraphLLMServiceFactory;
|
||||
import org.ruoyi.service.chat.IChatModelService;
|
||||
import org.ruoyi.service.graph.IGraphExtractionService;
|
||||
import org.ruoyi.service.graph.IGraphLLMService;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
/**
|
||||
* 图谱实体关系抽取服务实现
|
||||
* 使用工厂模式支持多种LLM模型(参考 ruoyi-chat 设计)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-09-30
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(prefix = "knowledge.graph", name = "enabled", havingValue = "true")
|
||||
public class GraphExtractionServiceImpl implements IGraphExtractionService {
|
||||
|
||||
private final IChatModelService chatModelService;
|
||||
private final GraphLLMServiceFactory llmServiceFactory;
|
||||
|
||||
/**
|
||||
* 实体匹配正则表达式
|
||||
* 格式: ("entity"<|>ENTITY_NAME<|>ENTITY_TYPE<|>ENTITY_DESCRIPTION)
|
||||
*/
|
||||
private static final Pattern ENTITY_PATTERN = Pattern.compile(
|
||||
"\\(\"entity\"" +
|
||||
Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) +
|
||||
"([^" + Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) + "]+)" +
|
||||
Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) +
|
||||
"([^" + Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) + "]+)" +
|
||||
Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) +
|
||||
"([^)]+)\\)"
|
||||
);
|
||||
/**
|
||||
* 关系匹配正则表达式
|
||||
* 格式: ("relationship"<|>SOURCE<|>TARGET<|>DESCRIPTION<|>STRENGTH)
|
||||
*/
|
||||
private static final Pattern RELATION_PATTERN = Pattern.compile(
|
||||
"\\(\"relationship\"" +
|
||||
Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) +
|
||||
"([^" + Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) + "]+)" +
|
||||
Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) +
|
||||
"([^" + Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) + "]+)" +
|
||||
Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) +
|
||||
"([^" + Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) + "]+)" +
|
||||
Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) +
|
||||
"([^)]+)\\)"
|
||||
);
|
||||
|
||||
|
||||
@Override
|
||||
public GraphExtractionResult extractFromText(String text) {
|
||||
return extractFromText(text, GraphConstants.DEFAULT_ENTITY_TYPES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphExtractionResult extractFromText(String text, String[] entityTypes) {
|
||||
log.info("开始从文本中抽取实体和关系,文本长度: {}", text.length());
|
||||
|
||||
try {
|
||||
// 1. 构建提示词
|
||||
String prompt = GraphExtractPrompt.buildExtractionPrompt(text, entityTypes);
|
||||
|
||||
// 2. 调用LLM(使用默认模型)
|
||||
String llmResponse = callLLM(prompt);
|
||||
|
||||
// 3. 解析响应
|
||||
GraphExtractionResult result = parseGraphResponse(llmResponse);
|
||||
result.setRawResponse(llmResponse);
|
||||
result.setSuccess(true);
|
||||
|
||||
log.info("抽取完成,实体数: {}, 关系数: {}",
|
||||
result.getEntities().size(), result.getRelations().size());
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("实体关系抽取失败", e);
|
||||
return GraphExtractionResult.builder()
|
||||
.entities(new ArrayList<>())
|
||||
.relations(new ArrayList<>())
|
||||
.success(false)
|
||||
.errorMessage(e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphExtractionResult extractFromTextWithModel(String text, String modelName) {
|
||||
log.info("开始从文本中抽取实体和关系,使用模型: {}, 文本长度: {}", modelName, text.length());
|
||||
|
||||
try {
|
||||
// 1. 获取模型配置
|
||||
ChatModelVo chatModel = chatModelService.selectModelByName(modelName);
|
||||
if (chatModel == null) {
|
||||
log.warn("未找到模型: {}, 使用默认模型", modelName);
|
||||
return extractFromText(text);
|
||||
}
|
||||
|
||||
// 2. 构建提示词
|
||||
String prompt = GraphExtractPrompt.buildExtractionPrompt(text, GraphConstants.DEFAULT_ENTITY_TYPES);
|
||||
|
||||
// 3. 调用LLM(使用指定模型)
|
||||
String llmResponse = callLLMWithModel(prompt, chatModel);
|
||||
|
||||
// 4. 解析响应
|
||||
GraphExtractionResult result = parseGraphResponse(llmResponse);
|
||||
result.setRawResponse(llmResponse);
|
||||
result.setSuccess(true);
|
||||
|
||||
log.info("抽取完成,实体数: {}, 关系数: {}, 使用模型: {}",
|
||||
result.getEntities().size(), result.getRelations().size(), modelName);
|
||||
|
||||
// ⭐ 调试:如果没有关系,记录原始响应(便于诊断)
|
||||
if (result.getRelations().isEmpty() && !result.getEntities().isEmpty()) {
|
||||
log.warn("⚠️ LLM 提取到 {} 个实体,但没有提取到任何关系!", result.getEntities().size());
|
||||
log.warn("LLM 原始响应预览(前500字符): {}",
|
||||
llmResponse.length() > 500 ? llmResponse.substring(0, 500) + "..." : llmResponse);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("实体关系抽取失败,模型: {}", modelName, e);
|
||||
return GraphExtractionResult.builder()
|
||||
.entities(new ArrayList<>())
|
||||
.relations(new ArrayList<>())
|
||||
.success(false)
|
||||
.errorMessage(e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphExtractionResult parseGraphResponse(String response) {
|
||||
log.debug("开始解析图谱响应,响应长度: {}", response != null ? response.length() : 0);
|
||||
|
||||
List<ExtractedEntity> entities = new ArrayList<>();
|
||||
List<ExtractedRelation> relations = new ArrayList<>();
|
||||
|
||||
if (StrUtil.isBlank(response)) {
|
||||
log.warn("响应为空,无法解析");
|
||||
return GraphExtractionResult.builder()
|
||||
.entities(entities)
|
||||
.relations(relations)
|
||||
.success(false)
|
||||
.errorMessage("LLM响应为空")
|
||||
.build();
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 解析实体
|
||||
Matcher entityMatcher = ENTITY_PATTERN.matcher(response);
|
||||
while (entityMatcher.find()) {
|
||||
String name = entityMatcher.group(1).trim();
|
||||
String type = entityMatcher.group(2).trim();
|
||||
String description = entityMatcher.group(3).trim();
|
||||
|
||||
// ⭐ 过滤无效实体(N/A 或包含特殊字符)
|
||||
if (isInvalidEntity(name, type)) {
|
||||
log.debug("跳过无效实体: name={}, type={}", name, type);
|
||||
continue;
|
||||
}
|
||||
|
||||
ExtractedEntity entity = ExtractedEntity.builder()
|
||||
.name(name)
|
||||
.type(type)
|
||||
.description(description)
|
||||
.build();
|
||||
|
||||
entities.add(entity);
|
||||
log.debug("解析到实体: name={}, type={}", name, type);
|
||||
}
|
||||
|
||||
// 2. 解析关系
|
||||
Matcher relationMatcher = RELATION_PATTERN.matcher(response);
|
||||
while (relationMatcher.find()) {
|
||||
String sourceEntity = relationMatcher.group(1).trim();
|
||||
String targetEntity = relationMatcher.group(2).trim();
|
||||
String description = relationMatcher.group(3).trim();
|
||||
String strengthStr = relationMatcher.group(4).trim();
|
||||
|
||||
Integer strength = parseStrength(strengthStr);
|
||||
Double confidence = calculateConfidence(strength);
|
||||
|
||||
ExtractedRelation relation = ExtractedRelation.builder()
|
||||
.sourceEntity(sourceEntity)
|
||||
.targetEntity(targetEntity)
|
||||
.description(description)
|
||||
.strength(strength)
|
||||
.confidence(confidence)
|
||||
.build();
|
||||
|
||||
relations.add(relation);
|
||||
log.debug("解析到关系: sourceEntity={}, targetEntity={}, strength={}",
|
||||
sourceEntity, targetEntity, strength);
|
||||
}
|
||||
|
||||
log.info("解析完成,实体数: {}, 关系数: {}", entities.size(), relations.size());
|
||||
|
||||
return GraphExtractionResult.builder()
|
||||
.entities(entities)
|
||||
.relations(relations)
|
||||
.success(true)
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("解析图谱响应失败", e);
|
||||
return GraphExtractionResult.builder()
|
||||
.entities(entities)
|
||||
.relations(relations)
|
||||
.success(false)
|
||||
.errorMessage("解析失败: " + e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用LLM获取响应(使用默认模型)
|
||||
*
|
||||
* @param prompt 提示词
|
||||
* @return LLM响应
|
||||
*/
|
||||
private String callLLM(String prompt) {
|
||||
// 获取聊天分类的最高优先级模型作为默认模型
|
||||
// 如果没有chat分类的模型,尝试查询任意可用模型
|
||||
ChatModelVo defaultModel = chatModelService.queryList(new ChatModelBo()).get(0);
|
||||
|
||||
if (defaultModel == null) {
|
||||
log.error("未找到可用的LLM模型");
|
||||
throw new RuntimeException("未找到可用的LLM模型,请先配置聊天模型");
|
||||
}
|
||||
|
||||
log.info("使用默认模型: {}", defaultModel.getModelName());
|
||||
return callLLMWithModel(prompt, defaultModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用指定模型调用LLM获取响应(使用工厂模式,支持多种LLM)
|
||||
*
|
||||
* @param prompt 提示词
|
||||
* @param chatModel 模型配置
|
||||
* @return LLM响应
|
||||
*/
|
||||
private String callLLMWithModel(String prompt, ChatModelVo chatModel) {
|
||||
log.info("调用LLM模型: model={}, category={}, 提示词长度={}",
|
||||
chatModel.getModelName(), chatModel.getCategory(), prompt.length());
|
||||
|
||||
try {
|
||||
// 根据模型类别获取对应的LLM服务实现
|
||||
IGraphLLMService llmService = llmServiceFactory.getLLMService(chatModel.getCategory());
|
||||
|
||||
// 调用LLM进行图谱抽取
|
||||
String responseText = llmService.extractGraph(prompt, chatModel);
|
||||
|
||||
log.info("LLM调用成功: model={}, category={}, 响应长度={}",
|
||||
chatModel.getModelName(), chatModel.getCategory(), responseText.length());
|
||||
|
||||
return responseText;
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
// 不支持的模型类别,降级到默认实现
|
||||
log.warn("不支持的模型类别: {}, 尝试使用OpenAI兼容模式", chatModel.getCategory());
|
||||
|
||||
try {
|
||||
IGraphLLMService openAiService = llmServiceFactory.getLLMService("openai");
|
||||
return openAiService.extractGraph(prompt, chatModel);
|
||||
} catch (Exception fallbackEx) {
|
||||
log.error("降级调用也失败: {}", fallbackEx.getMessage(), fallbackEx);
|
||||
throw new RuntimeException("LLM调用失败: " + fallbackEx.getMessage(), fallbackEx);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("LLM调用失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("LLM调用失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析关系强度
|
||||
*
|
||||
* @param strengthStr 强度字符串
|
||||
* @return 强度值(0-10)
|
||||
*/
|
||||
private Integer parseStrength(String strengthStr) {
|
||||
try {
|
||||
// 尝试解析为整数
|
||||
int strength = Integer.parseInt(strengthStr);
|
||||
// 限制在0-10范围内
|
||||
return Math.max(0, Math.min(10, strength));
|
||||
} catch (NumberFormatException e) {
|
||||
log.debug("无法解析关系强度: {}, 使用默认值5", strengthStr);
|
||||
return 5; // 默认中等强度
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证实体是否有效
|
||||
* 过滤 N/A 以及包含 Neo4j 不支持的特殊字符的实体
|
||||
*
|
||||
* @param name 实体名称
|
||||
* @param type 实体类型
|
||||
* @return true=无效,false=有效
|
||||
*/
|
||||
private boolean isInvalidEntity(String name, String type) {
|
||||
// 1. 检查是否为 N/A
|
||||
if ("N/A".equalsIgnoreCase(name) || "N/A".equalsIgnoreCase(type)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. 检查是否为空或纯空格
|
||||
if (StrUtil.isBlank(name) || StrUtil.isBlank(type)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. 检查类型是否包含 Neo4j Label 不支持的字符
|
||||
// Neo4j Label 规则:不能包含 / : & | 等特殊字符
|
||||
if (type.matches(".*[/:&|\\\\].*")) {
|
||||
log.warn("⚠️ 实体类型包含非法字符,将被过滤: type={}", type);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 4. 检查名称是否过长(Neo4j 建议 < 256)
|
||||
if (name.length() > 255 || type.length() > 64) {
|
||||
log.warn("⚠️ 实体名称或类型过长,将被过滤: name.length={}, type.length={}",
|
||||
name.length(), type.length());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据关系强度计算置信度
|
||||
*
|
||||
* @param strength 关系强度(0-10)
|
||||
* @return 置信度(0.0-1.0)
|
||||
*/
|
||||
private Double calculateConfidence(Integer strength) {
|
||||
if (strength == null) {
|
||||
return 0.5;
|
||||
}
|
||||
// 将0-10的强度映射到0.3-1.0的置信度
|
||||
return 0.3 + (strength / 10.0) * 0.7;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package org.ruoyi.service.graph.impl;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
import org.ruoyi.domain.bo.graph.GraphInstance;
|
||||
import org.ruoyi.mapper.graph.GraphInstanceMapper;
|
||||
import org.ruoyi.service.graph.IGraphInstanceService;
|
||||
import org.ruoyi.service.graph.IGraphStoreService;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 图谱实例服务实现
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-09-30
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(prefix = "knowledge.graph", name = "enabled", havingValue = "true")
|
||||
public class GraphInstanceServiceImpl implements IGraphInstanceService {
|
||||
|
||||
private final GraphInstanceMapper graphInstanceMapper;
|
||||
private final IGraphStoreService graphStoreService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public GraphInstance createInstance(String knowledgeId, String graphName, String config) {
|
||||
// 检查是否已存在
|
||||
LambdaQueryWrapper<GraphInstance> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(GraphInstance::getKnowledgeId, knowledgeId);
|
||||
GraphInstance existing = graphInstanceMapper.selectOne(wrapper);
|
||||
|
||||
if (existing != null) {
|
||||
log.warn("知识库 {} 已存在图谱实例", knowledgeId);
|
||||
return existing;
|
||||
}
|
||||
|
||||
// 创建新实例
|
||||
GraphInstance instance = new GraphInstance();
|
||||
instance.setGraphUuid(String.valueOf(IdUtil.getSnowflake().nextId())); // UUID
|
||||
instance.setKnowledgeId(knowledgeId);
|
||||
instance.setGraphName(StringUtils.isNotBlank(graphName) ? graphName : "知识图谱-" + knowledgeId);
|
||||
instance.setGraphStatus(0); // 0-未构建(新建时状态为未构建,需手动点击"构建"按钮)
|
||||
instance.setNodeCount(0);
|
||||
instance.setRelationshipCount(0);
|
||||
|
||||
// 解析配置
|
||||
if (StringUtils.isNotBlank(config)) {
|
||||
instance.setConfig(config);
|
||||
}
|
||||
|
||||
graphInstanceMapper.insert(instance);
|
||||
|
||||
// 创建 Neo4j Schema
|
||||
graphStoreService.createGraphSchema(knowledgeId);
|
||||
|
||||
log.info("创建图谱实例成功: knowledgeId={}, instanceId={}", knowledgeId, instance.getId());
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphInstance getById(Long id) {
|
||||
return graphInstanceMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphInstance getByUuid(String graphUuid) {
|
||||
LambdaQueryWrapper<GraphInstance> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(GraphInstance::getGraphUuid, graphUuid);
|
||||
return graphInstanceMapper.selectOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateInstance(GraphInstance instance) {
|
||||
try {
|
||||
int rows = graphInstanceMapper.updateById(instance);
|
||||
log.info("更新图谱实例: id={}, rows={}", instance.getId(), rows);
|
||||
return rows > 0;
|
||||
} catch (Exception e) {
|
||||
log.error("更新图谱实例失败: id={}", instance.getId(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GraphInstance> listByKnowledgeId(String knowledgeId) {
|
||||
LambdaQueryWrapper<GraphInstance> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(GraphInstance::getKnowledgeId, knowledgeId);
|
||||
wrapper.orderByDesc(GraphInstance::getCreateTime);
|
||||
return graphInstanceMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<GraphInstance> queryPage(Page<GraphInstance> page, String instanceName, String knowledgeId, Integer graphStatus) {
|
||||
LambdaQueryWrapper<GraphInstance> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 图谱名称模糊查询
|
||||
if (StringUtils.isNotBlank(instanceName)) {
|
||||
wrapper.like(GraphInstance::getGraphName, instanceName.trim());
|
||||
}
|
||||
|
||||
// 知识库ID精确查询
|
||||
if (StringUtils.isNotBlank(knowledgeId)) {
|
||||
wrapper.eq(GraphInstance::getKnowledgeId, knowledgeId.trim());
|
||||
}
|
||||
|
||||
// 状态精确查询
|
||||
if (graphStatus != null) {
|
||||
wrapper.eq(GraphInstance::getGraphStatus, graphStatus);
|
||||
}
|
||||
|
||||
// 只查询未删除的记录
|
||||
wrapper.eq(GraphInstance::getDelFlag, "0");
|
||||
|
||||
// 按创建时间倒序
|
||||
wrapper.orderByDesc(GraphInstance::getCreateTime);
|
||||
|
||||
return graphInstanceMapper.selectPage(page, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateStatus(String graphUuid, Integer status) {
|
||||
try {
|
||||
LambdaUpdateWrapper<GraphInstance> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(GraphInstance::getGraphUuid, graphUuid);
|
||||
wrapper.set(GraphInstance::getGraphStatus, status);
|
||||
|
||||
int rows = graphInstanceMapper.update(null, wrapper);
|
||||
|
||||
log.info("更新图谱状态: graphUuid={}, status={}, rows={}", graphUuid, status, rows);
|
||||
return rows > 0;
|
||||
} catch (Exception e) {
|
||||
log.error("更新图谱状态失败: graphUuid={}, status={}", graphUuid, status, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateCounts(String graphUuid, Integer nodeCount, Integer relationshipCount) {
|
||||
try {
|
||||
LambdaUpdateWrapper<GraphInstance> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(GraphInstance::getGraphUuid, graphUuid);
|
||||
|
||||
if (nodeCount != null) {
|
||||
wrapper.set(GraphInstance::getNodeCount, nodeCount);
|
||||
}
|
||||
if (relationshipCount != null) {
|
||||
wrapper.set(GraphInstance::getRelationshipCount, relationshipCount);
|
||||
}
|
||||
|
||||
int rows = graphInstanceMapper.update(null, wrapper);
|
||||
|
||||
log.info("更新图谱统计: graphUuid={}, nodeCount={}, relationshipCount={}, rows={}",
|
||||
graphUuid, nodeCount, relationshipCount, rows);
|
||||
return rows > 0;
|
||||
} catch (Exception e) {
|
||||
log.error("更新图谱统计失败: graphUuid={}", graphUuid, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateConfig(String graphUuid, String config) {
|
||||
try {
|
||||
LambdaUpdateWrapper<GraphInstance> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(GraphInstance::getGraphUuid, graphUuid);
|
||||
wrapper.set(GraphInstance::getConfig, config);
|
||||
|
||||
int rows = graphInstanceMapper.update(null, wrapper);
|
||||
|
||||
log.info("更新图谱配置: graphUuid={}, rows={}", graphUuid, rows);
|
||||
return rows > 0;
|
||||
} catch (Exception e) {
|
||||
log.error("更新图谱配置失败: graphUuid={}", graphUuid, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean deleteInstance(String graphUuid) {
|
||||
try {
|
||||
log.info("🗑️ 开始删除图谱实例及数据,graphUuid: {}", graphUuid);
|
||||
|
||||
// ⭐ 1. 先获取实例信息(获取knowledgeId)
|
||||
GraphInstance instance = getByUuid(graphUuid);
|
||||
if (instance == null) {
|
||||
log.warn("⚠️ 图谱实例不存在: graphUuid={}", graphUuid);
|
||||
return false;
|
||||
}
|
||||
|
||||
String knowledgeId = instance.getKnowledgeId();
|
||||
|
||||
// ⭐ 2. 删除Neo4j中的图数据(通过knowledgeId)
|
||||
if (StrUtil.isNotBlank(knowledgeId)) {
|
||||
log.info("删除Neo4j图数据,knowledgeId: {}", knowledgeId);
|
||||
boolean neo4jDeleted = graphStoreService.deleteByKnowledgeId(knowledgeId);
|
||||
if (neo4jDeleted) {
|
||||
log.info("✅ Neo4j图数据删除成功");
|
||||
} else {
|
||||
log.warn("⚠️ Neo4j图数据删除失败(可能是没有数据)");
|
||||
}
|
||||
} else {
|
||||
log.warn("⚠️ 实例没有关联知识库ID,跳过Neo4j数据删除");
|
||||
}
|
||||
|
||||
// 3. 删除MySQL中的实例记录
|
||||
LambdaQueryWrapper<GraphInstance> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(GraphInstance::getGraphUuid, graphUuid);
|
||||
int rows = graphInstanceMapper.delete(wrapper);
|
||||
|
||||
log.info("✅ 删除图谱实例成功: graphUuid={}, knowledgeId={}, rows={}",
|
||||
graphUuid, knowledgeId, rows);
|
||||
return rows > 0;
|
||||
} catch (Exception e) {
|
||||
log.error("❌ 删除图谱实例失败: graphUuid={}", graphUuid, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean deleteInstanceAndData(String graphUuid) {
|
||||
try {
|
||||
// 1. 删除 Neo4j 中的图谱数据
|
||||
boolean graphDeleted = graphStoreService.deleteGraph(graphUuid);
|
||||
|
||||
// 2. 删除 MySQL 中的实例记录
|
||||
boolean instanceDeleted = deleteInstance(graphUuid);
|
||||
|
||||
log.info("删除图谱实例及数据: graphUuid={}, graphDeleted={}, instanceDeleted={}",
|
||||
graphUuid, graphDeleted, instanceDeleted);
|
||||
|
||||
return graphDeleted && instanceDeleted;
|
||||
} catch (Exception e) {
|
||||
log.error("删除图谱实例及数据失败: graphUuid={}", graphUuid, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getStatistics(String graphUuid) {
|
||||
try {
|
||||
// 从 Neo4j 获取实时统计
|
||||
Map<String, Object> stats = graphStoreService.getGraphStatistics(graphUuid);
|
||||
|
||||
// 更新到 MySQL(异步)
|
||||
if (stats.containsKey("nodeCount") && stats.containsKey("relationshipCount")) {
|
||||
updateCounts(
|
||||
graphUuid,
|
||||
(Integer) stats.get("nodeCount"),
|
||||
(Integer) stats.get("relationshipCount")
|
||||
);
|
||||
}
|
||||
|
||||
// 添加实例信息
|
||||
GraphInstance instance = getByUuid(graphUuid);
|
||||
if (instance != null) {
|
||||
stats.put("graphName", instance.getGraphName());
|
||||
stats.put("status", instance.getGraphStatus());
|
||||
stats.put("createTime", instance.getCreateTime());
|
||||
stats.put("updateTime", instance.getUpdateTime());
|
||||
}
|
||||
|
||||
return stats;
|
||||
} catch (Exception e) {
|
||||
log.error("获取图谱统计信息失败: graphUuid={}", graphUuid, e);
|
||||
return new HashMap<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
package org.ruoyi.service.graph.impl;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.constant.GraphConstants;
|
||||
import org.ruoyi.domain.bo.graph.GraphEdge;
|
||||
import org.ruoyi.domain.bo.graph.GraphVertex;
|
||||
import org.ruoyi.domain.dto.ExtractedEntity;
|
||||
import org.ruoyi.domain.dto.ExtractedRelation;
|
||||
import org.ruoyi.domain.dto.GraphExtractionResult;
|
||||
import org.ruoyi.service.graph.IGraphExtractionService;
|
||||
import org.ruoyi.service.graph.IGraphRAGService;
|
||||
import org.ruoyi.service.graph.IGraphStoreService;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* GraphRAG服务实现
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-09-30
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(prefix = "knowledge.graph", name = "enabled", havingValue = "true")
|
||||
public class GraphRAGServiceImpl implements IGraphRAGService {
|
||||
|
||||
private final IGraphExtractionService graphExtractionService;
|
||||
private final IGraphStoreService graphStoreService;
|
||||
|
||||
@Override
|
||||
public GraphExtractionResult ingestText(String text, String knowledgeId, Map<String, Object> metadata) {
|
||||
return ingestTextWithModel(text, knowledgeId, metadata, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphExtractionResult ingestTextWithModel(String text, String knowledgeId, Map<String, Object> metadata, String modelName) {
|
||||
log.info("开始将文本入库到图谱,知识库ID: {}, 模型: {}, 文本长度: {}",
|
||||
knowledgeId, modelName != null ? modelName : "默认", text.length());
|
||||
|
||||
try {
|
||||
// 1. 从文本中抽取实体和关系
|
||||
GraphExtractionResult extractionResult;
|
||||
if (StrUtil.isNotBlank(modelName)) {
|
||||
extractionResult = graphExtractionService.extractFromTextWithModel(text, modelName);
|
||||
} else {
|
||||
extractionResult = graphExtractionService.extractFromText(text);
|
||||
}
|
||||
|
||||
if (!extractionResult.getSuccess()) {
|
||||
log.error("实体抽取失败: {}", extractionResult.getErrorMessage());
|
||||
return extractionResult;
|
||||
}
|
||||
|
||||
// 2. 将抽取的实体转换为图节点
|
||||
List<GraphVertex> vertices = convertEntitiesToVertices(
|
||||
extractionResult.getEntities(),
|
||||
knowledgeId,
|
||||
metadata
|
||||
);
|
||||
|
||||
// 3. 批量添加节点到Neo4j,并建立实体名称→nodeId的映射
|
||||
Map<String, String> entityNameToNodeIdMap = new HashMap<>();
|
||||
if (!vertices.isEmpty()) {
|
||||
int addedCount = graphStoreService.addVertices(vertices);
|
||||
log.info("成功添加 {} 个节点到图谱", addedCount);
|
||||
|
||||
// ⭐ 建立映射:实体名称 → nodeId
|
||||
for (GraphVertex vertex : vertices) {
|
||||
entityNameToNodeIdMap.put(vertex.getName(), vertex.getNodeId());
|
||||
}
|
||||
log.debug("建立实体名称映射: {} 个实体", entityNameToNodeIdMap.size());
|
||||
}
|
||||
|
||||
// 4. 将抽取的关系转换为图边,使用映射填充nodeId
|
||||
List<GraphEdge> edges = convertRelationsToEdges(
|
||||
extractionResult.getRelations(),
|
||||
knowledgeId,
|
||||
metadata,
|
||||
entityNameToNodeIdMap // ⭐ 传入映射
|
||||
);
|
||||
|
||||
// 5. 批量添加关系到Neo4j
|
||||
if (!edges.isEmpty()) {
|
||||
int addedCount = graphStoreService.addEdges(edges);
|
||||
log.info("成功添加 {} 个关系到图谱", addedCount);
|
||||
}
|
||||
|
||||
return extractionResult;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("文本入库失败", e);
|
||||
return GraphExtractionResult.builder()
|
||||
.entities(new ArrayList<>())
|
||||
.relations(new ArrayList<>())
|
||||
.success(false)
|
||||
.errorMessage(e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphExtractionResult ingestDocument(String documentText, String knowledgeId, Map<String, Object> metadata) {
|
||||
return ingestDocumentWithModel(documentText, knowledgeId, metadata, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphExtractionResult ingestDocumentWithModel(String documentText, String knowledgeId, Map<String, Object> metadata, String modelName) {
|
||||
log.info("开始将文档入库到图谱,知识库ID: {}, 模型: {}, 文档长度: {}",
|
||||
knowledgeId, modelName != null ? modelName : "默认", documentText.length());
|
||||
|
||||
// 如果文档较短,直接处理
|
||||
if (documentText.length() < GraphConstants.RAG_MAX_SEGMENT_SIZE_IN_TOKENS * 4) {
|
||||
return ingestTextWithModel(documentText, knowledgeId, metadata, modelName);
|
||||
}
|
||||
|
||||
// 文档较长,需要分片处理
|
||||
List<String> chunks = splitDocument(documentText);
|
||||
log.info("文档已分割为 {} 个片段", chunks.size());
|
||||
|
||||
// 合并结果
|
||||
List<ExtractedEntity> allEntities = new ArrayList<>();
|
||||
List<ExtractedRelation> allRelations = new ArrayList<>();
|
||||
int totalTokenUsed = 0;
|
||||
|
||||
for (int i = 0; i < chunks.size(); i++) {
|
||||
String chunk = chunks.get(i);
|
||||
log.debug("处理第 {}/{} 个片段", i + 1, chunks.size());
|
||||
|
||||
// 为每个片段添加序号元数据
|
||||
Map<String, Object> chunkMetadata = new HashMap<>(metadata);
|
||||
chunkMetadata.put("chunk_index", i);
|
||||
chunkMetadata.put("total_chunks", chunks.size());
|
||||
|
||||
GraphExtractionResult result = ingestTextWithModel(chunk, knowledgeId, chunkMetadata, modelName);
|
||||
|
||||
if (result.getSuccess()) {
|
||||
allEntities.addAll(result.getEntities());
|
||||
allRelations.addAll(result.getRelations());
|
||||
if (result.getTokenUsed() != null) {
|
||||
totalTokenUsed += result.getTokenUsed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 去重实体(基于名称和类型)
|
||||
List<ExtractedEntity> uniqueEntities = deduplicateEntities(allEntities);
|
||||
log.info("去重后实体数: {} -> {}", allEntities.size(), uniqueEntities.size());
|
||||
|
||||
return GraphExtractionResult.builder()
|
||||
.entities(uniqueEntities)
|
||||
.relations(allRelations)
|
||||
.tokenUsed(totalTokenUsed)
|
||||
.success(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String retrieveFromGraph(String query, String knowledgeId, int maxResults) {
|
||||
log.info("从图谱检索相关内容,查询: {}, 知识库ID: {}", query, knowledgeId);
|
||||
|
||||
try {
|
||||
// 1. 从查询中抽取关键词(简单分词)
|
||||
List<String> keywords = extractKeywords(query);
|
||||
log.debug("提取的关键词: {}", keywords);
|
||||
|
||||
if (keywords.isEmpty()) {
|
||||
return "未能从查询中提取关键信息";
|
||||
}
|
||||
|
||||
// 2. 在图谱中搜索相关实体节点
|
||||
List<GraphVertex> matchedNodes = new ArrayList<>();
|
||||
for (String keyword : keywords) {
|
||||
List<GraphVertex> nodes = graphStoreService.searchVerticesByName(
|
||||
keyword, knowledgeId, Math.min(5, maxResults)
|
||||
);
|
||||
matchedNodes.addAll(nodes);
|
||||
}
|
||||
|
||||
if (matchedNodes.isEmpty()) {
|
||||
return "图谱中未找到相关实体";
|
||||
}
|
||||
|
||||
log.info("找到 {} 个匹配的实体节点", matchedNodes.size());
|
||||
|
||||
// 3. 去重(按nodeId)
|
||||
Map<String, GraphVertex> uniqueNodes = new HashMap<>();
|
||||
for (GraphVertex node : matchedNodes) {
|
||||
uniqueNodes.putIfAbsent(node.getNodeId(), node);
|
||||
}
|
||||
matchedNodes = new ArrayList<>(uniqueNodes.values());
|
||||
|
||||
// 限制结果数量
|
||||
if (matchedNodes.size() > maxResults) {
|
||||
matchedNodes = matchedNodes.subList(0, maxResults);
|
||||
}
|
||||
|
||||
// 4. 为每个匹配节点获取邻居,构建子图上下文
|
||||
StringBuilder result = new StringBuilder();
|
||||
result.append("### 图谱检索结果\n\n");
|
||||
result.append(String.format("查询: %s\n", query));
|
||||
result.append(String.format("找到 %d 个相关实体:\n\n", matchedNodes.size()));
|
||||
|
||||
for (int i = 0; i < matchedNodes.size(); i++) {
|
||||
GraphVertex node = matchedNodes.get(i);
|
||||
result.append(String.format("**%d. %s** (%s)\n", i + 1, node.getName(), node.getLabel()));
|
||||
|
||||
if (StrUtil.isNotBlank(node.getDescription())) {
|
||||
result.append(String.format(" 描述: %s\n", node.getDescription()));
|
||||
}
|
||||
|
||||
// 获取邻居节点(1跳)
|
||||
List<GraphVertex> neighbors = graphStoreService.getNeighbors(
|
||||
node.getNodeId(), knowledgeId, 5
|
||||
);
|
||||
|
||||
if (!neighbors.isEmpty()) {
|
||||
result.append(" 关联实体: ");
|
||||
List<String> neighborNames = neighbors.stream()
|
||||
.map(GraphVertex::getName)
|
||||
.limit(5)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
result.append(String.join(", ", neighborNames));
|
||||
result.append("\n");
|
||||
}
|
||||
|
||||
result.append("\n");
|
||||
}
|
||||
|
||||
// 5. 添加统计信息
|
||||
result.append("---\n");
|
||||
result.append(String.format("总计: %d 个实体节点\n", matchedNodes.size()));
|
||||
|
||||
return result.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("图谱检索失败", e);
|
||||
return "检索失败: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从查询中提取关键词
|
||||
*
|
||||
* @param query 查询文本
|
||||
* @return 关键词列表
|
||||
*/
|
||||
private List<String> extractKeywords(String query) {
|
||||
List<String> keywords = new ArrayList<>();
|
||||
|
||||
// 简单的中文分词策略
|
||||
// 1. 去除标点符号
|
||||
String cleaned = query.replaceAll("[\\p{Punct}\\s]+", " ");
|
||||
|
||||
// 2. 按空格分割
|
||||
String[] words = cleaned.split("\\s+");
|
||||
|
||||
// 3. 过滤停用词和短词
|
||||
Set<String> stopWords = new HashSet<>(Arrays.asList(
|
||||
"的", "了", "和", "是", "在", "我", "有", "个", "这", "那", "为",
|
||||
"与", "或", "但", "等", "及", "而", "中", "如", "一", "二", "三"
|
||||
));
|
||||
|
||||
for (String word : words) {
|
||||
word = word.trim();
|
||||
if (word.length() >= 2 && !stopWords.contains(word)) {
|
||||
keywords.add(word);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 如果没有提取到关键词,尝试按2-3字切分
|
||||
if (keywords.isEmpty() && query.length() >= 2) {
|
||||
for (int i = 0; i <= query.length() - 2; i++) {
|
||||
String chunk = query.substring(i, Math.min(i + 3, query.length()));
|
||||
if (chunk.length() >= 2 && !stopWords.contains(chunk)) {
|
||||
keywords.add(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 去重并限制数量
|
||||
return keywords.stream()
|
||||
.distinct()
|
||||
.limit(5)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteGraphData(String knowledgeId) {
|
||||
log.info("删除知识库图谱数据,知识库ID: {}", knowledgeId);
|
||||
|
||||
try {
|
||||
// 删除该知识库的所有节点和关系
|
||||
return graphStoreService.deleteByKnowledgeId(knowledgeId);
|
||||
} catch (Exception e) {
|
||||
log.error("删除图谱数据失败", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将抽取的实体转换为图节点
|
||||
*/
|
||||
private List<GraphVertex> convertEntitiesToVertices(
|
||||
List<ExtractedEntity> entities,
|
||||
String knowledgeId,
|
||||
Map<String, Object> metadata) {
|
||||
|
||||
List<GraphVertex> vertices = new ArrayList<>();
|
||||
|
||||
for (ExtractedEntity entity : entities) {
|
||||
GraphVertex vertex = new GraphVertex();
|
||||
vertex.setNodeId(IdUtil.simpleUUID()); // 生成唯一ID
|
||||
vertex.setName(entity.getName());
|
||||
vertex.setLabel(entity.getType());
|
||||
vertex.setDescription(entity.getDescription());
|
||||
vertex.setKnowledgeId(knowledgeId);
|
||||
vertex.setConfidence(entity.getConfidence() != null ? entity.getConfidence() : 1.0);
|
||||
|
||||
// 添加元数据
|
||||
if (metadata != null && !metadata.isEmpty()) {
|
||||
vertex.setMetadata(metadata);
|
||||
}
|
||||
|
||||
vertices.add(vertex);
|
||||
}
|
||||
|
||||
return vertices;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将抽取的关系转换为图边
|
||||
*
|
||||
* @param relations 抽取的关系列表
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param metadata 元数据
|
||||
* @param entityNameToNodeIdMap 实体名称到节点ID的映射
|
||||
* @return 图边列表
|
||||
*/
|
||||
private List<GraphEdge> convertRelationsToEdges(
|
||||
List<ExtractedRelation> relations,
|
||||
String knowledgeId,
|
||||
Map<String, Object> metadata,
|
||||
Map<String, String> entityNameToNodeIdMap) {
|
||||
|
||||
List<GraphEdge> edges = new ArrayList<>();
|
||||
int skippedCount = 0;
|
||||
|
||||
for (ExtractedRelation relation : relations) {
|
||||
// ⭐ 通过实体名称查找对应的nodeId
|
||||
String sourceNodeId = entityNameToNodeIdMap.get(relation.getSourceEntity());
|
||||
String targetNodeId = entityNameToNodeIdMap.get(relation.getTargetEntity());
|
||||
|
||||
// 如果找不到对应的节点ID,跳过这个关系
|
||||
if (sourceNodeId == null || targetNodeId == null) {
|
||||
log.warn("⚠️ 跳过关系(节点未找到): {} -> {}",
|
||||
relation.getSourceEntity(), relation.getTargetEntity());
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
GraphEdge edge = new GraphEdge();
|
||||
edge.setEdgeId(IdUtil.simpleUUID());
|
||||
edge.setSourceNodeId(sourceNodeId); // ⭐ 设置源节点ID
|
||||
edge.setTargetNodeId(targetNodeId); // ⭐ 设置目标节点ID
|
||||
edge.setSourceName(relation.getSourceEntity());
|
||||
edge.setTargetName(relation.getTargetEntity());
|
||||
edge.setLabel("RELATED_TO"); // 默认关系类型
|
||||
edge.setDescription(relation.getDescription());
|
||||
edge.setWeight(relation.getStrength() / 10.0); // 转换为0-1的权重
|
||||
edge.setKnowledgeId(knowledgeId);
|
||||
edge.setConfidence(relation.getConfidence() != null ? relation.getConfidence() : 1.0);
|
||||
|
||||
// 添加元数据
|
||||
if (metadata != null && !metadata.isEmpty()) {
|
||||
edge.setMetadata(metadata);
|
||||
}
|
||||
|
||||
edges.add(edge);
|
||||
}
|
||||
|
||||
if (skippedCount > 0) {
|
||||
log.warn("⚠️ 共跳过 {} 个关系(对应的实体节点未找到)", skippedCount);
|
||||
}
|
||||
|
||||
return edges;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分割文档为多个片段
|
||||
*/
|
||||
private List<String> splitDocument(String text) {
|
||||
List<String> chunks = new ArrayList<>();
|
||||
int chunkSize = GraphConstants.RAG_MAX_SEGMENT_SIZE_IN_TOKENS * 4; // 简单估算字符数
|
||||
int overlap = GraphConstants.RAG_SEGMENT_OVERLAP_IN_TOKENS * 4;
|
||||
|
||||
int start = 0;
|
||||
while (start < text.length()) {
|
||||
int end = Math.min(start + chunkSize, text.length());
|
||||
|
||||
// 尝试在句子边界分割
|
||||
if (end < text.length()) {
|
||||
int lastPeriod = text.lastIndexOf('。', end);
|
||||
int lastNewline = text.lastIndexOf('\n', end);
|
||||
int boundary = Math.max(lastPeriod, lastNewline);
|
||||
|
||||
if (boundary > start) {
|
||||
end = boundary + 1;
|
||||
}
|
||||
}
|
||||
|
||||
chunks.add(text.substring(start, end));
|
||||
|
||||
// ⭐ 修复死循环:确保 start 一定会增加
|
||||
// 如果已经到达文本末尾,直接退出
|
||||
if (end >= text.length()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 计算下一个起始位置,确保至少前进1个字符
|
||||
int nextStart = end - overlap;
|
||||
if (nextStart <= start) {
|
||||
// 如果 overlap 太大导致无法前进,强制前进到 end
|
||||
start = end;
|
||||
} else {
|
||||
start = nextStart;
|
||||
}
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 去重实体
|
||||
*/
|
||||
private List<ExtractedEntity> deduplicateEntities(List<ExtractedEntity> entities) {
|
||||
Map<String, ExtractedEntity> entityMap = new HashMap<>();
|
||||
|
||||
for (ExtractedEntity entity : entities) {
|
||||
String key = entity.getName() + "|" + entity.getType();
|
||||
|
||||
if (!entityMap.containsKey(key)) {
|
||||
entityMap.put(key, entity);
|
||||
} else {
|
||||
// 如果已存在,保留置信度更高的
|
||||
ExtractedEntity existing = entityMap.get(key);
|
||||
double entityConf = entity.getConfidence() != null ? entity.getConfidence() : 1.0;
|
||||
double existingConf = existing.getConfidence() != null ? existing.getConfidence() : 1.0;
|
||||
if (entityConf > existingConf) {
|
||||
entityMap.put(key, entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ArrayList<>(entityMap.values());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
package org.ruoyi.service.graph.impl;
|
||||
|
||||
import dev.langchain4j.model.openai.OpenAiChatModel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.service.graph.IGraphLLMService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
|
||||
/**
|
||||
* OpenAI 图谱LLM服务实现
|
||||
* 支持 OpenAI 兼容的模型(GPT-3.5, GPT-4, 自定义OpenAI兼容接口等)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-10-11
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class OpenAIGraphLLMServiceImpl implements IGraphLLMService {
|
||||
|
||||
@Override
|
||||
public String extractGraph(String prompt, ChatModelVo chatModel) {
|
||||
log.info("OpenAI模型调用: model={}, apiHost={}, 提示词长度={}",
|
||||
chatModel.getModelName(), chatModel.getApiHost(), prompt.length());
|
||||
try {
|
||||
OpenAiChatModel model = OpenAiChatModel.builder()
|
||||
.baseUrl(chatModel.getApiHost())
|
||||
.apiKey(chatModel.getApiKey())
|
||||
.modelName(chatModel.getModelName())
|
||||
.build();
|
||||
String content = model.chat(prompt);
|
||||
String responseText = content != null ? content : "";
|
||||
log.info("OpenAI模型响应成功:, 响应长度={}", responseText.length());
|
||||
return responseText;
|
||||
} catch (Exception e) {
|
||||
log.error("OpenAI模型调用失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("OpenAI模型调用失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return "openai"; // 对应 ChatModel 表中的 category 字段
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package org.ruoyi.service.knowledge;
|
||||
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import org.ruoyi.domain.bo.knowledge.KnowledgeAttachBo;
|
||||
import org.ruoyi.domain.bo.knowledge.KnowledgeInfoUploadBo;
|
||||
import org.ruoyi.domain.vo.knowledge.KnowledgeAttachVo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 知识库附件Service接口
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-17
|
||||
*/
|
||||
public interface IKnowledgeAttachService {
|
||||
|
||||
/**
|
||||
* 查询知识库附件
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 知识库附件
|
||||
*/
|
||||
KnowledgeAttachVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 分页查询知识库附件列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @return 知识库附件分页列表
|
||||
*/
|
||||
TableDataInfo<KnowledgeAttachVo> queryPageList(KnowledgeAttachBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询符合条件的知识库附件列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 知识库附件列表
|
||||
*/
|
||||
List<KnowledgeAttachVo> queryList(KnowledgeAttachBo bo);
|
||||
|
||||
/**
|
||||
* 新增知识库附件
|
||||
*
|
||||
* @param bo 知识库附件
|
||||
* @return 是否新增成功
|
||||
*/
|
||||
Boolean insertByBo(KnowledgeAttachBo bo);
|
||||
|
||||
/**
|
||||
* 修改知识库附件
|
||||
*
|
||||
* @param bo 知识库附件
|
||||
* @return 是否修改成功
|
||||
*/
|
||||
Boolean updateByBo(KnowledgeAttachBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除知识库附件信息
|
||||
*
|
||||
* @param ids 待删除的主键集合
|
||||
* @param isValid 是否进行有效性校验
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
|
||||
|
||||
/**
|
||||
* 上传附件
|
||||
*/
|
||||
void upload(KnowledgeInfoUploadBo bo);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.ruoyi.service.knowledge;
|
||||
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import org.ruoyi.domain.bo.knowledge.KnowledgeFragmentBo;
|
||||
import org.ruoyi.domain.vo.knowledge.KnowledgeFragmentVo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 知识片段Service接口
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-17
|
||||
*/
|
||||
public interface IKnowledgeFragmentService {
|
||||
|
||||
/**
|
||||
* 查询知识片段
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 知识片段
|
||||
*/
|
||||
KnowledgeFragmentVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 分页查询知识片段列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @return 知识片段分页列表
|
||||
*/
|
||||
TableDataInfo<KnowledgeFragmentVo> queryPageList(KnowledgeFragmentBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询符合条件的知识片段列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 知识片段列表
|
||||
*/
|
||||
List<KnowledgeFragmentVo> queryList(KnowledgeFragmentBo bo);
|
||||
|
||||
/**
|
||||
* 新增知识片段
|
||||
*
|
||||
* @param bo 知识片段
|
||||
* @return 是否新增成功
|
||||
*/
|
||||
Boolean insertByBo(KnowledgeFragmentBo bo);
|
||||
|
||||
/**
|
||||
* 修改知识片段
|
||||
*
|
||||
* @param bo 知识片段
|
||||
* @return 是否修改成功
|
||||
*/
|
||||
Boolean updateByBo(KnowledgeFragmentBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除知识片段信息
|
||||
*
|
||||
* @param ids 待删除的主键集合
|
||||
* @param isValid 是否进行有效性校验
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.ruoyi.service.knowledge;
|
||||
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import org.ruoyi.domain.bo.knowledge.KnowledgeGraphInstanceBo;
|
||||
import org.ruoyi.domain.vo.knowledge.KnowledgeGraphInstanceVo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 知识图谱实例Service接口
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-17
|
||||
*/
|
||||
public interface IKnowledgeGraphInstanceService {
|
||||
|
||||
/**
|
||||
* 查询知识图谱实例
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 知识图谱实例
|
||||
*/
|
||||
KnowledgeGraphInstanceVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 分页查询知识图谱实例列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @return 知识图谱实例分页列表
|
||||
*/
|
||||
TableDataInfo<KnowledgeGraphInstanceVo> queryPageList(KnowledgeGraphInstanceBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询符合条件的知识图谱实例列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 知识图谱实例列表
|
||||
*/
|
||||
List<KnowledgeGraphInstanceVo> queryList(KnowledgeGraphInstanceBo bo);
|
||||
|
||||
/**
|
||||
* 新增知识图谱实例
|
||||
*
|
||||
* @param bo 知识图谱实例
|
||||
* @return 是否新增成功
|
||||
*/
|
||||
Boolean insertByBo(KnowledgeGraphInstanceBo bo);
|
||||
|
||||
/**
|
||||
* 修改知识图谱实例
|
||||
*
|
||||
* @param bo 知识图谱实例
|
||||
* @return 是否修改成功
|
||||
*/
|
||||
Boolean updateByBo(KnowledgeGraphInstanceBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除知识图谱实例信息
|
||||
*
|
||||
* @param ids 待删除的主键集合
|
||||
* @param isValid 是否进行有效性校验
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.ruoyi.service.knowledge;
|
||||
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import org.ruoyi.domain.bo.knowledge.KnowledgeGraphSegmentBo;
|
||||
import org.ruoyi.domain.vo.knowledge.KnowledgeGraphSegmentVo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 知识图谱片段Service接口
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-17
|
||||
*/
|
||||
public interface IKnowledgeGraphSegmentService {
|
||||
|
||||
/**
|
||||
* 查询知识图谱片段
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 知识图谱片段
|
||||
*/
|
||||
KnowledgeGraphSegmentVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 分页查询知识图谱片段列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @return 知识图谱片段分页列表
|
||||
*/
|
||||
TableDataInfo<KnowledgeGraphSegmentVo> queryPageList(KnowledgeGraphSegmentBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询符合条件的知识图谱片段列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 知识图谱片段列表
|
||||
*/
|
||||
List<KnowledgeGraphSegmentVo> queryList(KnowledgeGraphSegmentBo bo);
|
||||
|
||||
/**
|
||||
* 新增知识图谱片段
|
||||
*
|
||||
* @param bo 知识图谱片段
|
||||
* @return 是否新增成功
|
||||
*/
|
||||
Boolean insertByBo(KnowledgeGraphSegmentBo bo);
|
||||
|
||||
/**
|
||||
* 修改知识图谱片段
|
||||
*
|
||||
* @param bo 知识图谱片段
|
||||
* @return 是否修改成功
|
||||
*/
|
||||
Boolean updateByBo(KnowledgeGraphSegmentBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除知识图谱片段信息
|
||||
*
|
||||
* @param ids 待删除的主键集合
|
||||
* @param isValid 是否进行有效性校验
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package org.ruoyi.service.knowledge;
|
||||
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import org.ruoyi.domain.bo.knowledge.KnowledgeInfoBo;
|
||||
import org.ruoyi.domain.vo.knowledge.KnowledgeInfoVo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 知识库Service接口
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-17
|
||||
*/
|
||||
public interface IKnowledgeInfoService {
|
||||
|
||||
/**
|
||||
* 查询知识库
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 知识库
|
||||
*/
|
||||
KnowledgeInfoVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 分页查询知识库列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @return 知识库分页列表
|
||||
*/
|
||||
TableDataInfo<KnowledgeInfoVo> queryPageList(KnowledgeInfoBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询符合条件的知识库列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 知识库列表
|
||||
*/
|
||||
List<KnowledgeInfoVo> queryList(KnowledgeInfoBo bo);
|
||||
|
||||
/**
|
||||
* 新增知识库
|
||||
*
|
||||
* @param bo 知识库
|
||||
* @return 是否新增成功
|
||||
*/
|
||||
Boolean insertByBo(KnowledgeInfoBo bo);
|
||||
|
||||
/**
|
||||
* 修改知识库
|
||||
*
|
||||
* @param bo 知识库
|
||||
* @return 是否修改成功
|
||||
*/
|
||||
Boolean updateByBo(KnowledgeInfoBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除知识库信息
|
||||
*
|
||||
* @param ids 待删除的主键集合
|
||||
* @param isValid 是否进行有效性校验
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.ruoyi.service.knowledge;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 资源载入
|
||||
*/
|
||||
public interface ResourceLoader {
|
||||
|
||||
String getContent(InputStream inputStream);
|
||||
|
||||
List<String> getChunkList(String content, String kid);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.ruoyi.service.knowledge;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文本切分
|
||||
*/
|
||||
public interface TextSplitter {
|
||||
|
||||
/**
|
||||
* 文本切分
|
||||
*
|
||||
* @param content 文本内容
|
||||
* @param kid 知识库id
|
||||
* @return 切分后的文本列表
|
||||
*/
|
||||
List<String> split(String content, String kid);
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package org.ruoyi.service.knowledge.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import org.ruoyi.common.core.domain.dto.OssDTO;
|
||||
import org.ruoyi.common.core.service.OssService;
|
||||
import org.ruoyi.common.core.utils.MapstructUtils;
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.domain.bo.knowledge.KnowledgeAttachBo;
|
||||
import org.ruoyi.domain.bo.knowledge.KnowledgeInfoUploadBo;
|
||||
import org.ruoyi.domain.bo.vector.StoreEmbeddingBo;
|
||||
import org.ruoyi.domain.entity.knowledge.KnowledgeAttach;
|
||||
import org.ruoyi.domain.entity.knowledge.KnowledgeFragment;
|
||||
import org.ruoyi.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.domain.vo.knowledge.KnowledgeAttachVo;
|
||||
import org.ruoyi.domain.vo.knowledge.KnowledgeInfoVo;
|
||||
import org.ruoyi.factory.ResourceLoaderFactory;
|
||||
import org.ruoyi.mapper.knowledge.KnowledgeAttachMapper;
|
||||
import org.ruoyi.mapper.knowledge.KnowledgeFragmentMapper;
|
||||
import org.ruoyi.service.chat.IChatModelService;
|
||||
import org.ruoyi.service.knowledge.IKnowledgeAttachService;
|
||||
import org.ruoyi.service.knowledge.IKnowledgeInfoService;
|
||||
import org.ruoyi.service.knowledge.ResourceLoader;
|
||||
import org.ruoyi.service.vector.VectorStoreService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 知识库附件Service业务层处理
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-17
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class KnowledgeAttachServiceImpl implements IKnowledgeAttachService {
|
||||
|
||||
private final KnowledgeAttachMapper baseMapper;
|
||||
|
||||
private final IKnowledgeInfoService knowledgeInfoService;
|
||||
|
||||
private final KnowledgeFragmentMapper knowledgeFragmentMapper;
|
||||
|
||||
private final IChatModelService chatModelService;
|
||||
|
||||
private final ResourceLoaderFactory resourceLoaderFactory;
|
||||
|
||||
private final VectorStoreService vectorStoreService;
|
||||
|
||||
private final OssService ossService;
|
||||
/**
|
||||
* 查询知识库附件
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 知识库附件
|
||||
*/
|
||||
@Override
|
||||
public KnowledgeAttachVo queryById(Long id){
|
||||
return baseMapper.selectVoById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询知识库附件列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @return 知识库附件分页列表
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<KnowledgeAttachVo> queryPageList(KnowledgeAttachBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<KnowledgeAttach> lqw = buildQueryWrapper(bo);
|
||||
Page<KnowledgeAttachVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询符合条件的知识库附件列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 知识库附件列表
|
||||
*/
|
||||
@Override
|
||||
public List<KnowledgeAttachVo> queryList(KnowledgeAttachBo bo) {
|
||||
LambdaQueryWrapper<KnowledgeAttach> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<KnowledgeAttach> buildQueryWrapper(KnowledgeAttachBo bo) {
|
||||
Map<String, Object> params = bo.getParams();
|
||||
LambdaQueryWrapper<KnowledgeAttach> lqw = Wrappers.lambdaQuery();
|
||||
lqw.orderByAsc(KnowledgeAttach::getId);
|
||||
lqw.eq(bo.getKnowledgeId() != null, KnowledgeAttach::getKnowledgeId, bo.getKnowledgeId());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getName()), KnowledgeAttach::getName, bo.getName());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getType()), KnowledgeAttach::getType, bo.getType());
|
||||
lqw.eq(bo.getOssId() != null, KnowledgeAttach::getOssId, bo.getOssId());
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增知识库附件
|
||||
*
|
||||
* @param bo 知识库附件
|
||||
* @return 是否新增成功
|
||||
*/
|
||||
@Override
|
||||
public Boolean insertByBo(KnowledgeAttachBo bo) {
|
||||
KnowledgeAttach add = MapstructUtils.convert(bo, KnowledgeAttach.class);
|
||||
validEntityBeforeSave(add);
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setId(add.getId());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改知识库附件
|
||||
*
|
||||
* @param bo 知识库附件
|
||||
* @return 是否修改成功
|
||||
*/
|
||||
@Override
|
||||
public Boolean updateByBo(KnowledgeAttachBo bo) {
|
||||
KnowledgeAttach update = MapstructUtils.convert(bo, KnowledgeAttach.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前的数据校验
|
||||
*/
|
||||
private void validEntityBeforeSave(KnowledgeAttach entity){
|
||||
//TODO 做一些数据校验,如唯一约束
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并批量删除知识库附件信息
|
||||
*
|
||||
* @param ids 待删除的主键集合
|
||||
* @param isValid 是否进行有效性校验
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
@Override
|
||||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||||
if(isValid){
|
||||
//TODO 做一些业务上的校验,判断是否需要校验
|
||||
}
|
||||
return baseMapper.deleteByIds(ids) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(KnowledgeInfoUploadBo bo) {
|
||||
MultipartFile file = bo.getFile();
|
||||
// 保存文件信息
|
||||
OssDTO ossDTO = ossService.uploadFile(file);
|
||||
Long knowledgeId = bo.getKnowledgeId();
|
||||
List<String> chunkList = new ArrayList<>();
|
||||
KnowledgeAttach knowledgeAttach = new KnowledgeAttach();
|
||||
knowledgeAttach.setKnowledgeId(bo.getKnowledgeId());
|
||||
String docId = RandomUtil.randomString(10);
|
||||
knowledgeAttach.setOssId(ossDTO.getOssId());
|
||||
knowledgeAttach.setDocId(docId);
|
||||
knowledgeAttach.setName(ossDTO.getOriginalName());
|
||||
knowledgeAttach.setType(ossDTO.getFileSuffix());
|
||||
String content = "";
|
||||
ResourceLoader resourceLoader = resourceLoaderFactory.getLoaderByFileType(knowledgeAttach.getType());
|
||||
// 文档分段入库
|
||||
List<String> fids = new ArrayList<>();
|
||||
try {
|
||||
content = resourceLoader.getContent(file.getInputStream());
|
||||
chunkList = resourceLoader.getChunkList(content, String.valueOf(knowledgeId));
|
||||
List<KnowledgeFragment> knowledgeFragmentList = new ArrayList<>();
|
||||
if (CollUtil.isNotEmpty(chunkList)) {
|
||||
for (int i = 0; i < chunkList.size(); i++) {
|
||||
// 生成知识片段ID
|
||||
String fid = RandomUtil.randomString(10);
|
||||
fids.add(fid);
|
||||
KnowledgeFragment knowledgeFragment = new KnowledgeFragment();
|
||||
knowledgeFragment.setDocId(docId);
|
||||
knowledgeFragment.setIdx(i);
|
||||
knowledgeFragment.setContent(chunkList.get(i));
|
||||
knowledgeFragment.setCreateTime(new Date());
|
||||
knowledgeFragmentList.add(knowledgeFragment);
|
||||
}
|
||||
}
|
||||
knowledgeFragmentMapper.insertBatch(knowledgeFragmentList);
|
||||
} catch (IOException e) {
|
||||
log.error("保存知识库信息失败!{}", e.getMessage());
|
||||
}
|
||||
baseMapper.insert(knowledgeAttach);
|
||||
|
||||
// 查询知识库信息
|
||||
KnowledgeInfoVo knowledgeInfoVo = knowledgeInfoService.queryById(knowledgeId);
|
||||
|
||||
// 查询向量模信息
|
||||
ChatModelVo chatModelVo = chatModelService.selectModelByName(knowledgeInfoVo.getEmbeddingModel());
|
||||
|
||||
StoreEmbeddingBo storeEmbeddingBo = new StoreEmbeddingBo();
|
||||
storeEmbeddingBo.setKid(String.valueOf(knowledgeId));
|
||||
storeEmbeddingBo.setDocId(docId);
|
||||
storeEmbeddingBo.setFids(fids);
|
||||
storeEmbeddingBo.setChunkList(chunkList);
|
||||
storeEmbeddingBo.setVectorStoreName(knowledgeInfoVo.getVectorModel());
|
||||
storeEmbeddingBo.setEmbeddingModelName(knowledgeInfoVo.getEmbeddingModel());
|
||||
storeEmbeddingBo.setApiKey(chatModelVo.getApiKey());
|
||||
storeEmbeddingBo.setBaseUrl(chatModelVo.getApiHost());
|
||||
vectorStoreService.storeEmbeddings(storeEmbeddingBo);
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user