v3.0.0 init

This commit is contained in:
ageerle
2026-02-06 03:00:23 +08:00
parent eb2e8f3ff8
commit 7b8cfe02a1
1524 changed files with 53132 additions and 58866 deletions

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 +
'}';
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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 {
}

View File

@@ -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);
// }
//}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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 字段
}
}

View File

@@ -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 字段
}
}

View File

@@ -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;
}
}

View File

@@ -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<>();
}
}
}

View File

@@ -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());
}
}

View File

@@ -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 字段
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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