From 885d46a4aae5e8a0959d2b233da26c1b59b3d8a5 Mon Sep 17 00:00:00 2001 From: zhang <823772544@qq.com> Date: Fri, 13 Feb 2026 18:14:39 +0800 Subject: [PATCH] =?UTF-8?q?AI=E5=B7=A5=E4=BD=9C=E6=B5=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/script/sql/workFlow-v3.0.sql | 6 + ruoyi-common/pom.xml | 1 + ruoyi-common/ruoyi-common-bom/pom.xml | 7 + ruoyi-common/ruoyi-common-chat/pom.xml | 67 +++++++++ .../chat/Service}/IChatModelService.java | 6 +- .../common/chat/Service/IChatService.java | 27 ++++ .../chat}/domain/bo/chat/ChatModelBo.java | 10 +- .../chat}/domain/dto/ChatMessageDTO.java | 12 +- .../chat}/domain/dto/request/ChatRequest.java | 12 +- .../chat}/domain/entity/chat/ChatModel.java | 8 +- .../chat}/domain/vo/chat/ChatModelVo.java | 8 +- .../chat}/factory/ChatServiceFactory.java | 4 +- ruoyi-common/ruoyi-common-oss/pom.xml | 14 ++ .../common/oss/constant/OssConstant.java | 12 ++ ruoyi-modules/ruoyi-aiflow/pom.xml | 9 +- .../workflow/workflow/WfNodeFactory.java | 2 + .../org/ruoyi/workflow/workflow/WfState.java | 9 +- .../workflow/workflow/WorkflowEngine.java | 4 +- .../workflow/workflow/WorkflowStarter.java | 19 ++- .../ruoyi/workflow/workflow/WorkflowUtil.java | 140 ++++++++++-------- .../workflow/node/AbstractWfNode.java | 8 - .../workflow/node/HumanFeedbackNode.java | 54 +++++++ .../workflow/node/answer/LLMAnswerNode.java | 8 +- .../KeywordExtractorNode.java | 8 +- .../KnowledgeRetrievalNode.java | 6 +- .../workflow/node/switcher/SwitcherNode.java | 60 +++++++- ruoyi-modules/ruoyi-chat/pom.xml | 35 +---- .../main/java/org/ruoyi/agent/McpAgent.java | 46 ++++++ .../java/org/ruoyi/config/McpSseConfig.java | 21 +++ .../ruoyi/controller/chat/ChatController.java | 2 +- .../controller/chat/ChatModelController.java | 6 +- .../ruoyi/factory/EmbeddingModelFactory.java | 4 +- .../ruoyi/mapper/chat/ChatModelMapper.java | 4 +- .../service/chat/IChatMessageService.java | 2 +- .../org/ruoyi/service/chat/IChatService.java | 26 ---- .../impl/AbstractStreamingChatService.java | 84 +++++++---- .../chat/impl/ChatMessageServiceImpl.java | 4 +- .../chat/impl/ChatModelServiceImpl.java | 8 +- .../service/chat/impl/ChatServiceFacade.java | 13 +- .../impl/memory/ChatMemoryUsageExample.java | 14 +- .../memory/PersistentChatMemoryStore.java | 2 +- .../chat/impl/provider/OllamaServiceImpl.java | 4 +- .../chat/impl/provider/OpenAIServiceImpl.java | 23 +-- .../impl/provider/QianWenChatServiceImpl.java | 126 +++++++++++++++- .../service/embed/BaseEmbedModelService.java | 2 +- .../impl/AliBaiLianBaseEmbedProvider.java | 2 +- .../AliBaiLianMultiEmbeddingProvider.java | 2 +- .../embed/impl/OllamaEmbeddingProvider.java | 2 +- .../embed/impl/OpenAiEmbeddingProvider.java | 2 +- .../ruoyi/service/graph/IGraphLLMService.java | 2 +- .../impl/DeepSeekGraphLLMServiceImpl.java | 2 +- .../graph/impl/DifyGraphLLMServiceImpl.java | 2 +- .../impl/GraphExtractionServiceImpl.java | 6 +- .../graph/impl/OpenAIGraphLLMServiceImpl.java | 2 +- .../impl/KnowledgeAttachServiceImpl.java | 4 +- .../controller/system/SysOssController.java | 14 ++ .../ruoyi/system/service/ISysOssService.java | 9 ++ .../service/impl/SysOssServiceImpl.java | 104 +++++++++++++ .../system/utils/QwenFileUploadUtils.java | 50 +++++++ 59 files changed, 863 insertions(+), 287 deletions(-) create mode 100644 docs/script/sql/workFlow-v3.0.sql create mode 100644 ruoyi-common/ruoyi-common-chat/pom.xml rename {ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat => ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/Service}/IChatModelService.java (91%) create mode 100644 ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/Service/IChatService.java rename {ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi => ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat}/domain/bo/chat/ChatModelBo.java (90%) rename {ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi => ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat}/domain/dto/ChatMessageDTO.java (94%) rename {ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi => ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat}/domain/dto/request/ChatRequest.java (81%) rename {ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi => ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat}/domain/entity/chat/ChatModel.java (89%) rename {ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi => ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat}/domain/vo/chat/ChatModelVo.java (95%) rename {ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi => ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat}/factory/ChatServiceFactory.java (93%) create mode 100644 ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/HumanFeedbackNode.java create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/McpAgent.java create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/config/McpSseConfig.java delete mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/IChatService.java create mode 100644 ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/utils/QwenFileUploadUtils.java diff --git a/docs/script/sql/workFlow-v3.0.sql b/docs/script/sql/workFlow-v3.0.sql new file mode 100644 index 00000000..b97366f2 --- /dev/null +++ b/docs/script/sql/workFlow-v3.0.sql @@ -0,0 +1,6 @@ +INSERT INTO `mysql`.`t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (17, '5cd68dccbbb411f0bb7840c2ba9a7fbc', 'Start', '开始', '流程由此开始', 0, 1, '2025-11-07 16:32:49', '2025-11-07 16:32:49', 0, '000000'); +INSERT INTO `mysql`.`t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (18, '5cd6ac69bbb411f0bb7840c2ba9a7fbc', 'End', '结束', '流程由此结束', 0, 1, '2025-11-07 16:32:49', '2025-11-07 16:32:49', 0, '000000'); +INSERT INTO `mysql`.`t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (19, '5cd6c8eabbb411f0bb7840c2ba9a7fbc', 'Answer', '生成回答', '调用大语言模型回答问题', 0, 1, '2025-11-07 16:32:49', '2025-11-07 16:32:49', 0, '000000'); +INSERT INTO `mysql`.`t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (25, '0b4369bb60dc46d6bd84ceb4e36184dc', 'KeywordExtractor', '关键词提取', '从文本中提取关键词', 0, 1, '2025-12-26 16:30:05', '2025-12-26 16:30:05', 0, '000000'); +INSERT INTO `mysql`.`t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (26, 'bb00fc2f52c74fec82ee3f99725b56bb', 'Switcher', '条件分支', '根据条件执行不同分支', 0, 1, '2025-12-26 16:30:46', '2025-12-26 16:30:46', 0, '000000'); +INSERT INTO `mysql`.`t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (36, 'f37dbcb8f0d5464d90fbb22774490a56', 'HumanFeedback', '人类', '人机沟通', 0, 1, '2025-12-30 17:37:14', '2025-12-30 17:37:14', 0, '000000'); diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml index 0ef1c44a..a91e84b0 100644 --- a/ruoyi-common/pom.xml +++ b/ruoyi-common/pom.xml @@ -11,6 +11,7 @@ ruoyi-common-bom + ruoyi-common-chat ruoyi-common-social ruoyi-common-core ruoyi-common-doc diff --git a/ruoyi-common/ruoyi-common-bom/pom.xml b/ruoyi-common/ruoyi-common-bom/pom.xml index 17f930bc..ec18b08a 100644 --- a/ruoyi-common/ruoyi-common-bom/pom.xml +++ b/ruoyi-common/ruoyi-common-bom/pom.xml @@ -26,6 +26,13 @@ ${revision} + + + org.ruoyi + ruoyi-common-chat + ${revision} + + org.ruoyi diff --git a/ruoyi-common/ruoyi-common-chat/pom.xml b/ruoyi-common/ruoyi-common-chat/pom.xml new file mode 100644 index 00000000..0db3c41c --- /dev/null +++ b/ruoyi-common/ruoyi-common-chat/pom.xml @@ -0,0 +1,67 @@ + + + + org.ruoyi + ruoyi-common + ${revision} + + 4.0.0 + + ruoyi-common-chat + + + ruoyi-common-chat chat服务 + + + + + + org.springframework + spring-web + + + + org.springframework + spring-webmvc + + + + org.ruoyi + ruoyi-common-core + + + + org.ruoyi + ruoyi-common-sse + + + + com.alibaba + fastjson + + + + org.ruoyi + ruoyi-common-excel + + + + dev.langchain4j + langchain4j + ${langchain4j.version} + + + + org.ruoyi + ruoyi-common-mybatis + + + + org.ruoyi + ruoyi-common-tenant + + + + diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/IChatModelService.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/Service/IChatModelService.java similarity index 91% rename from ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/IChatModelService.java rename to ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/Service/IChatModelService.java index 62850223..edb7ed52 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/IChatModelService.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/Service/IChatModelService.java @@ -1,9 +1,9 @@ -package org.ruoyi.service.chat; +package org.ruoyi.common.chat.Service; +import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; 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; diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/Service/IChatService.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/Service/IChatService.java new file mode 100644 index 00000000..1aeb2456 --- /dev/null +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/Service/IChatService.java @@ -0,0 +1,27 @@ +package org.ruoyi.common.chat.Service; + +import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; +import org.ruoyi.common.chat.domain.dto.request.ChatRequest; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +/** + * 公共大模型对话接口 + */ +public interface IChatService { + + /** + * 客户端发送对话消息到服务端 + */ + SseEmitter chat(ChatModelVo chatModelVo, ChatRequest chatRequest, SseEmitter emitter, Long userId, String tokenValue); + + /** + * 工作流专用对话 + */ + SseEmitter chat(ChatModelVo chatModelVo, ChatRequest chatRequest,SseEmitter emitter,Long userId,String tokenValue, StreamingChatResponseHandler handler); + + /** + * 获取服务提供商名称 + */ + String getProviderName(); +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/bo/chat/ChatModelBo.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/bo/chat/ChatModelBo.java similarity index 90% rename from ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/bo/chat/ChatModelBo.java rename to ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/bo/chat/ChatModelBo.java index f6ebdeed..8807503c 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/bo/chat/ChatModelBo.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/bo/chat/ChatModelBo.java @@ -1,12 +1,12 @@ -package org.ruoyi.domain.bo.chat; +package org.ruoyi.common.chat.domain.bo.chat; -import org.ruoyi.common.core.validate.EditGroup; -import org.ruoyi.domain.entity.chat.ChatModel; -import org.ruoyi.common.mybatis.core.domain.BaseEntity; import io.github.linpeilie.annotations.AutoMapper; +import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.EqualsAndHashCode; -import jakarta.validation.constraints.*; +import org.ruoyi.common.chat.domain.entity.chat.ChatModel; +import org.ruoyi.common.core.validate.EditGroup; +import org.ruoyi.common.mybatis.core.domain.BaseEntity; /** * 模型管理业务对象 chat_model diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/ChatMessageDTO.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/dto/ChatMessageDTO.java similarity index 94% rename from ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/ChatMessageDTO.java rename to ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/dto/ChatMessageDTO.java index 60529e09..633c90c8 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/ChatMessageDTO.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/dto/ChatMessageDTO.java @@ -1,4 +1,4 @@ -package org.ruoyi.domain.dto; +package org.ruoyi.common.chat.domain.dto; import lombok.Data; @@ -10,31 +10,31 @@ import lombok.Data; */ @Data public class ChatMessageDTO { - + /** * 消息角色: system/user/assistant */ private String role; - + /** * 消息内容 */ private String content; - + public static ChatMessageDTO system(String content) { ChatMessageDTO msg = new ChatMessageDTO(); msg.role = "system"; msg.content = content; return msg; } - + public static ChatMessageDTO user(String content) { ChatMessageDTO msg = new ChatMessageDTO(); msg.role = "user"; msg.content = content; return msg; } - + public static ChatMessageDTO assistant(String content) { ChatMessageDTO msg = new ChatMessageDTO(); msg.role = "assistant"; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/request/ChatRequest.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/dto/request/ChatRequest.java similarity index 81% rename from ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/request/ChatRequest.java rename to ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/dto/request/ChatRequest.java index 370260d0..1b36ae42 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/dto/request/ChatRequest.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/dto/request/ChatRequest.java @@ -1,8 +1,10 @@ -package org.ruoyi.domain.dto.request; +package org.ruoyi.common.chat.domain.dto.request; +import dev.langchain4j.data.message.ChatMessage; import jakarta.validation.constraints.NotEmpty; import lombok.Data; -import org.ruoyi.domain.dto.ChatMessageDTO; +import org.ruoyi.common.chat.domain.dto.ChatMessageDTO; + import java.util.List; /** @@ -16,7 +18,6 @@ public class ChatRequest { @NotEmpty(message = "对话消息不能为空") private List messages; - @NotEmpty(message = "传入的模型不能为空") private String model; @@ -60,4 +61,9 @@ public class ChatRequest { */ private String token; + /** + * 原生对话对象 + */ + private List chatMessages; + } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/chat/ChatModel.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/entity/chat/ChatModel.java similarity index 89% rename from ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/chat/ChatModel.java rename to ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/entity/chat/ChatModel.java index fc8e3f7b..9edee32f 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/chat/ChatModel.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/entity/chat/ChatModel.java @@ -1,10 +1,10 @@ -package org.ruoyi.domain.entity.chat; +package org.ruoyi.common.chat.domain.entity.chat; -import cn.idev.excel.annotation.ExcelProperty; -import org.ruoyi.common.tenant.core.TenantEntity; -import com.baomidou.mybatisplus.annotation.*; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.EqualsAndHashCode; +import org.ruoyi.common.tenant.core.TenantEntity; import java.io.Serial; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/chat/ChatModelVo.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/vo/chat/ChatModelVo.java similarity index 95% rename from ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/chat/ChatModelVo.java rename to ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/vo/chat/ChatModelVo.java index fa6b99e9..77ba3328 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/chat/ChatModelVo.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/vo/chat/ChatModelVo.java @@ -1,16 +1,15 @@ -package org.ruoyi.domain.vo.chat; +package org.ruoyi.common.chat.domain.vo.chat; + -import org.ruoyi.domain.entity.chat.ChatModel; import cn.idev.excel.annotation.ExcelIgnoreUnannotated; import cn.idev.excel.annotation.ExcelProperty; import io.github.linpeilie.annotations.AutoMapper; import lombok.Data; +import org.ruoyi.common.chat.domain.entity.chat.ChatModel; import java.io.Serial; import java.io.Serializable; - - /** * 模型管理视图对象 chat_model * @@ -110,3 +109,4 @@ public class ChatModelVo implements Serializable { } + diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/factory/ChatServiceFactory.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/factory/ChatServiceFactory.java similarity index 93% rename from ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/factory/ChatServiceFactory.java rename to ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/factory/ChatServiceFactory.java index 5e0f81eb..ee884357 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/factory/ChatServiceFactory.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/factory/ChatServiceFactory.java @@ -1,6 +1,6 @@ -package org.ruoyi.factory; +package org.ruoyi.common.chat.factory; -import org.ruoyi.service.chat.IChatService; +import org.ruoyi.common.chat.Service.IChatService; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; diff --git a/ruoyi-common/ruoyi-common-oss/pom.xml b/ruoyi-common/ruoyi-common-oss/pom.xml index 0ddb6cce..cd42515b 100644 --- a/ruoyi-common/ruoyi-common-oss/pom.xml +++ b/ruoyi-common/ruoyi-common-oss/pom.xml @@ -61,6 +61,20 @@ s3-transfer-manager + + + com.openai + openai-java + 4.8.0 + + + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/ruoyi/common/oss/constant/OssConstant.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/ruoyi/common/oss/constant/OssConstant.java index ce73e598..6e68e03e 100644 --- a/ruoyi-common/ruoyi-common-oss/src/main/java/org/ruoyi/common/oss/constant/OssConstant.java +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/ruoyi/common/oss/constant/OssConstant.java @@ -37,4 +37,16 @@ public interface OssConstant { */ String IS_HTTPS = "Y"; + // 文档解析前缀 + String FILE_ID_PREFIX = "fileid://"; + + // 服务名称 + String DASH_SCOPE = "Qwen"; + + // apiKey 配置名称 + String CONFIG_NAME_KEY = "file.api.key"; + + // apiHost 配置名称 + String CONFIG_NAME_URL = "file.api.host"; + } diff --git a/ruoyi-modules/ruoyi-aiflow/pom.xml b/ruoyi-modules/ruoyi-aiflow/pom.xml index 7d6507f5..34d46d62 100644 --- a/ruoyi-modules/ruoyi-aiflow/pom.xml +++ b/ruoyi-modules/ruoyi-aiflow/pom.xml @@ -26,7 +26,7 @@ org.ruoyi - ruoyi-common-core + ruoyi-common-chat @@ -34,13 +34,6 @@ ruoyi-common-web - - - org.ruoyi - ruoyi-common-mybatis - - - org.ruoyi ruoyi-common-satoken diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WfNodeFactory.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WfNodeFactory.java index 59b135e7..94aafe8b 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WfNodeFactory.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WfNodeFactory.java @@ -4,6 +4,7 @@ import org.ruoyi.workflow.entity.WorkflowComponent; import org.ruoyi.workflow.entity.WorkflowNode; import org.ruoyi.workflow.workflow.node.AbstractWfNode; import org.ruoyi.workflow.workflow.node.EndNode; +import org.ruoyi.workflow.workflow.node.HumanFeedbackNode; import org.ruoyi.workflow.workflow.node.answer.LLMAnswerNode; import org.ruoyi.workflow.workflow.node.httpRequest.HttpRequestNode; import org.ruoyi.workflow.workflow.node.keywordExtractor.KeywordExtractorNode; @@ -25,6 +26,7 @@ public class WfNodeFactory { case MAIL_SEND -> wfNode = new MailSendNode(wfComponent, nodeDefinition, wfState, nodeState); case HTTP_REQUEST -> wfNode = new HttpRequestNode(wfComponent, nodeDefinition, wfState, nodeState); case SWITCHER -> wfNode = new SwitcherNode(wfComponent, nodeDefinition, wfState, nodeState); + case HUMAN_FEEDBACK -> wfNode = new HumanFeedbackNode(wfComponent, nodeDefinition, wfState, nodeState); default -> { } } diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WfState.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WfState.java index 0988d36c..97962a71 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WfState.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WfState.java @@ -9,6 +9,7 @@ import org.ruoyi.workflow.entity.User; import org.ruoyi.workflow.entity.WorkflowNode; import org.ruoyi.workflow.workflow.data.NodeIOData; import org.ruoyi.workflow.workflow.node.AbstractWfNode; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.util.*; @@ -24,6 +25,9 @@ public class WfState { private String uuid; private User user; private String processingNodeUuid; + private Long userId; + private String tokenValue; + private SseEmitter sseEmitter; //Source node uuid => target node uuid list private Map> edges = new HashMap<>(); @@ -55,10 +59,13 @@ public class WfState { */ private Set interruptNodes = new HashSet<>(); - public WfState(User user, List input, String uuid) { + public WfState(User user, List input, String uuid, Long userId, String tokenValue, SseEmitter sseEmitter) { this.input = input; this.user = user; this.uuid = uuid; + this.userId = userId; + this.tokenValue = tokenValue; + this.sseEmitter = sseEmitter; } /** diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowEngine.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowEngine.java index 55d10a17..f84b2164 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowEngine.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowEngine.java @@ -68,7 +68,7 @@ public class WorkflowEngine { this.workflowRuntimeNodeService = workflowRuntimeNodeService; } - public void run(User user, List userInputs, SseEmitter sseEmitter) { + public void run(User user, List userInputs, SseEmitter sseEmitter, Long userId, String tokenValue) { this.user = user; this.sseEmitter = sseEmitter; log.info("WorkflowEngine run,userId:{},workflowUuid:{},userInputs:{}", user.getId(), workflow.getUuid(), userInputs); @@ -86,7 +86,7 @@ public class WorkflowEngine { Pair> startAndEnds = findStartAndEndNode(); WorkflowNode startNode = startAndEnds.getLeft(); List wfInputs = getAndCheckUserInput(userInputs, startNode); - this.wfState = new WfState(user, wfInputs, runtimeUuid); + this.wfState = new WfState(user, wfInputs, runtimeUuid,userId, tokenValue, sseEmitter); workflowRuntimeService.updateInput(this.wfRuntimeResp.getId(), wfState); diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowStarter.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowStarter.java index 69e036fb..7865af51 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowStarter.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowStarter.java @@ -1,9 +1,12 @@ package org.ruoyi.workflow.workflow; +import cn.dev33.satoken.stp.StpUtil; import com.fasterxml.jackson.databind.node.ObjectNode; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.ruoyi.common.core.exception.base.BaseException; +import org.ruoyi.common.satoken.utils.LoginHelper; +import org.ruoyi.common.sse.core.SseEmitterManager; import org.ruoyi.workflow.entity.*; import org.ruoyi.workflow.helper.SSEEmitterHelper; import org.ruoyi.workflow.service.*; @@ -46,9 +49,17 @@ public class WorkflowStarter { @Resource private SSEEmitterHelper sseEmitterHelper; + @Resource + private SseEmitterManager sseEmitterManager; + public SseEmitter streaming(User user, String workflowUuid, List userInputs) { - SseEmitter sseEmitter = new SseEmitter(SSE_TIMEOUT); + // 获取用户ID + Long userId = LoginHelper.getUserId(); + // 获取登录Token + String tokenValue = StpUtil.getTokenValue(); + // 根据用户ID和Token连接SSE对象 + SseEmitter sseEmitter = sseEmitterManager.connect(userId, tokenValue); if (!sseEmitterHelper.checkOrComplete(user, sseEmitter)) { return sseEmitter; } @@ -60,12 +71,12 @@ public class WorkflowStarter { sseEmitterHelper.sendErrorAndComplete(user.getId(), sseEmitter, A_WF_DISABLED.getInfo()); return sseEmitter; } - self.asyncRun(user, workflow, userInputs, sseEmitter); + self.asyncRun(user, workflow, userInputs, sseEmitter, userId, tokenValue); return sseEmitter; } @Async - public void asyncRun(User user, Workflow workflow, List userInputs, SseEmitter sseEmitter) { + public void asyncRun(User user, Workflow workflow, List userInputs, SseEmitter sseEmitter, Long userId, String tokenValue) { log.info("WorkflowEngine run,userId:{},workflowUuid:{},userInputs:{}", user.getId(), workflow.getUuid(), userInputs); List components = workflowComponentService.getAllEnable(); List nodes = workflowNodeService.lambdaQuery() @@ -79,7 +90,7 @@ public class WorkflowStarter { WorkflowEngine workflowEngine = new WorkflowEngine(workflow, sseEmitterHelper, components, nodes, edges, workflowRuntimeService, workflowRuntimeNodeService); - workflowEngine.run(user, userInputs, sseEmitter); + workflowEngine.run(user, userInputs, sseEmitter, userId, tokenValue); } @Async diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowUtil.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowUtil.java index 2da7a52c..e57b324c 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowUtil.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowUtil.java @@ -3,10 +3,19 @@ package org.ruoyi.workflow.workflow; import cn.hutool.core.collection.CollStreamUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.data.message.SystemMessage; import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; +import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.bsc.langgraph4j.langchain4j.generators.StreamingChatGenerator; import org.bsc.langgraph4j.state.AgentState; +import org.ruoyi.common.chat.Service.IChatModelService; +import org.ruoyi.common.chat.Service.IChatService; +import org.ruoyi.common.chat.domain.dto.request.ChatRequest; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; +import org.ruoyi.common.chat.factory.ChatServiceFactory; import org.ruoyi.workflow.base.NodeInputConfigTypeHandler; import org.ruoyi.workflow.entity.WorkflowNode; import org.ruoyi.workflow.enums.WfIODataTypeEnum; @@ -15,11 +24,9 @@ import org.ruoyi.workflow.workflow.data.NodeIOData; import org.ruoyi.workflow.workflow.data.NodeIODataContent; import org.ruoyi.workflow.workflow.def.WfNodeParamRef; import org.springframework.stereotype.Component; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.DEFAULT_OUTPUT_PARAM_NAME; @@ -27,6 +34,12 @@ import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.DEFAULT_O @Component public class WorkflowUtil { + @Resource + private ChatServiceFactory chatServiceFactory; + + @Resource + private IChatModelService chatModelService; + public static String renderTemplate(String template, List values) { // 🔒 关键修复:如果 template 为 null,直接返回 null 或空字符串 if (template == null) { @@ -74,8 +87,8 @@ public class WorkflowUtil { public static String getHumanFeedbackTip(String nodeUuid, List wfNodes) { WorkflowNode wfNode = wfNodes.stream() - .filter(item -> item.getUuid().equals(nodeUuid)) - .findFirst().orElse(null); + .filter(item -> item.getUuid().equals(nodeUuid)) + .findFirst().orElse(null); if (null == wfNode) { return ""; } @@ -88,73 +101,82 @@ public class WorkflowUtil { return String.valueOf(tip); } - public void streamingInvokeLLM(WfState wfState, WfNodeState state, WorkflowNode node, String category, - String modelName, List systemMessage) { - log.info("stream invoke, category: {}, modelName: {}", category, modelName); + public void streamingInvokeLLM(WfState wfState, WfNodeState state, WorkflowNode node, String modelName, + List systemMessage) { + log.info("stream invoke, modelName: {}", modelName); + // 根据模型名称查询模型信息 + ChatModelVo chatModelVo = chatModelService.selectModelByName(modelName); + if (chatModelVo == null) { + throw new IllegalArgumentException("模型不存在: " + modelName); + } + + // 根据模型名称找到模型实体 + String modelVoCategory = chatModelVo.getCategory(); // 根据 category 获取对应的 ChatService(不使用计费代理,工作流场景单独计费) - //IChatService chatService = chatServiceFactory.getOriginalService(category); + IChatService chatService = chatServiceFactory.getOriginalService(modelVoCategory); StreamingChatGenerator streamingGenerator = StreamingChatGenerator.builder() - .mapResult(response -> { - String responseTxt = response.aiMessage().text(); - log.info("llm response:{}", responseTxt); + .mapResult(response -> { + String responseTxt = response.aiMessage().text(); + log.info("llm response:{}", responseTxt); - // 传递所有输入数据 + 添加 LLM 输出 - wfState.getNodeStateByNodeUuid(node.getUuid()).ifPresent(item -> { - List outputs = new ArrayList<>(item.getInputs()); - NodeIOData output = NodeIOData.createByText(DEFAULT_OUTPUT_PARAM_NAME, "", responseTxt); - outputs.add(output); - item.setOutputs(outputs); - }); + // 传递所有输入数据 + 添加 LLM 输出 + wfState.getNodeStateByNodeUuid(node.getUuid()).ifPresent(item -> { + List outputs = new ArrayList<>(item.getInputs()); + NodeIOData output = NodeIOData.createByText(DEFAULT_OUTPUT_PARAM_NAME, "", responseTxt); + outputs.add(output); + item.setOutputs(outputs); + }); - return Map.of("completeResult", response.aiMessage().text()); - }) - .startingNode(node.getUuid()) - .startingState(state) - .build(); + return Map.of("completeResult", response.aiMessage().text()); + }) + .startingNode(node.getUuid()) + .startingState(state) + .build(); + + Long userId = wfState.getUserId(); + String tokenValue = wfState.getTokenValue(); + SseEmitter sseEmitter = wfState.getSseEmitter(); // 构建 ruoyi-ai 的 ChatRequest -// List messages = new ArrayList<>(); -// -// addUserMessage(node, state.getInputs(), messages); -// -// addSystemMessage(systemMessage, messages); -// -// ChatRequest chatRequest = new ChatRequest(); -// chatRequest.setModel(modelName); -// chatRequest.setMessages(messages); + List chatMessages = new ArrayList<>(); + addUserMessage(node, state.getInputs(), chatMessages); + chatMessages.addAll(systemMessage); + + // 定义模型调用对象 + ChatRequest chatRequest = new ChatRequest(); + // 目前工作流深度思考成员变量只能写死 + chatRequest.setEnableThinking(false); + chatRequest.setModel(modelName); + chatRequest.setChatMessages(chatMessages); // 使用工作流专用方法 + StreamingChatResponseHandler handler = streamingGenerator.handler(); + chatService.chat(chatModelVo, chatRequest, sseEmitter, userId, tokenValue, handler); wfState.getNodeToStreamingGenerator().put(node.getUuid(), streamingGenerator); } /** * 添加用户信息 * - * @param node - * @param messages + * @param node 节点 + * @param userMessage 用户信息 */ - private void addUserMessage(WorkflowNode node, List userMessage, List messages) { + private void addUserMessage(WorkflowNode node, List userMessage, List messages) { if (CollUtil.isEmpty(userMessage)) { return; } - WfNodeInputConfig nodeInputConfig = NodeInputConfigTypeHandler.fillNodeInputConfig(node.getInputConfig()); - List refInputs = nodeInputConfig.getRefInputs(); - Set nameSet = CollStreamUtil.toSet(refInputs, WfNodeParamRef::getName); - - userMessage.stream().filter(item -> nameSet.contains(item.getName())) - .map(item -> getMessage("user", item.getContent().getValue().toString())).forEach(messages::add); - - if (CollUtil.isNotEmpty(messages)) { - return; + // 构建消息列表 + List messageList = buildMessageList(userMessage, nameSet); + // 如果没有找到匹配的消息,尝试使用input字段 + if (CollUtil.isEmpty(messageList)) { + messageList = buildMessageList(userMessage, Set.of("input")); } - - userMessage.stream().filter(item -> "input".equals(item.getName())) - .map(item -> getMessage("user", item.getContent().getValue().toString())).forEach(messages::add); + messages.addAll(messageList); } /** @@ -170,19 +192,13 @@ public class WorkflowUtil { } /** - * 添加系统信息 - * - * @param systemMessage - * @param messages + * 构建消息列表 */ - private void addSystemMessage(List systemMessage, List messages) { - log.info("addSystemMessage received: {}", systemMessage); // 🔥 加这一行 - - if (CollUtil.isEmpty(systemMessage)) { - return; - } - systemMessage.stream() - .map(userMsg -> getMessage("system", userMsg.singleText())) - .forEach(messages::add); + private List buildMessageList(List userMessage, Set nameSet) { + return userMessage.stream() + .filter(item -> item != null && item.getName() != null) + // 兼容默认输出参数的人机交互 + .filter(item -> nameSet.contains(item.getName())) + .map(item -> getMessage("user", item.getContent().getValue().toString())).toList(); } } diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/AbstractWfNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/AbstractWfNode.java index 143637bc..d3262971 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/AbstractWfNode.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/AbstractWfNode.java @@ -124,14 +124,6 @@ public abstract class AbstractWfNode { log.info("↓↓↓↓↓ node process start,name:{},uuid:{}", node.getTitle(), node.getUuid()); state.setProcessStatus(NODE_PROCESS_STATUS_DOING); initInput(); - //HumanFeedback的情况 - Object humanFeedbackState = state.data().get(HUMAN_FEEDBACK_KEY); - if (null != humanFeedbackState) { - String userInput = humanFeedbackState.toString(); - if (StringUtils.isNotBlank(userInput)) { - state.getInputs().add(NodeIOData.createByText(HUMAN_FEEDBACK_KEY, "default", userInput)); - } - } if (null != inputConsumer) { inputConsumer.accept(state); } diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/HumanFeedbackNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/HumanFeedbackNode.java new file mode 100644 index 00000000..f41e1938 --- /dev/null +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/HumanFeedbackNode.java @@ -0,0 +1,54 @@ +package org.ruoyi.workflow.workflow.node; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.ruoyi.workflow.entity.WorkflowComponent; +import org.ruoyi.workflow.entity.WorkflowNode; +import org.ruoyi.workflow.workflow.NodeProcessResult; +import org.ruoyi.workflow.workflow.WfNodeState; +import org.ruoyi.workflow.workflow.WfState; +import org.ruoyi.workflow.workflow.data.NodeIOData; + +import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.*; + +/** + * 人机交互节点实现类 + */ +@Slf4j +public class HumanFeedbackNode extends AbstractWfNode { + + public HumanFeedbackNode(WorkflowComponent component, WorkflowNode nodeDefinition, WfState wfState, WfNodeState nodeState) { + super(component, nodeDefinition, wfState, nodeState); + } + + // 人机交互节点的处理逻辑 + @Override + public NodeProcessResult onProcess() { + log.info("Processing HumanFeedback node: {}", node.getTitle()); + // 从状态中获取用户输入数据 + Object humanFeedbackState = state.data().get(HUMAN_FEEDBACK_KEY); + if (null != humanFeedbackState) { + String userInput = humanFeedbackState.toString(); + if (StringUtils.isNotBlank(userInput)) { + // 用户已提供输入,将用户输入添加到节点输入和输出中 + NodeIOData feedbackData = NodeIOData.createByText("output", "default", userInput); + // 添加到输出列表,这样后续节点可以使用 + state.getOutputs().add(feedbackData); + // 设置为成功状态 + state.setProcessStatus(NODE_PROCESS_STATUS_SUCCESS); + log.info("Human feedback processed for node: {}, content: {}", node.getTitle(), userInput); + } else { + // 用户输入为空,设置等待状态 + state.setProcessStatus(NODE_PROCESS_STATUS_DOING); + log.info("Human feedback is empty for node: {}", node.getTitle()); + } + } else { + // 没有用户输入,这可能是正常情况(等待用户输入) + // 但为了确保流程可以继续,我们仍然标记为成功 + state.setProcessStatus(NODE_PROCESS_STATUS_SUCCESS); + log.info("No human feedback found for node: {}, continuing workflow", node.getTitle()); + } + return new NodeProcessResult(); + } +} + diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/answer/LLMAnswerNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/answer/LLMAnswerNode.java index a9826112..e0c2f31c 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/answer/LLMAnswerNode.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/answer/LLMAnswerNode.java @@ -1,6 +1,6 @@ package org.ruoyi.workflow.workflow.node.answer; -import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.data.message.SystemMessage; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.ruoyi.workflow.entity.WorkflowComponent; @@ -44,9 +44,9 @@ public class LLMAnswerNode extends AbstractWfNode { // 调用LLM WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class); String modelName = nodeConfigObj.getModelName(); - String category = nodeConfigObj.getCategory(); - List systemMessage = List.of(UserMessage.from(prompt)); - workflowUtil.streamingInvokeLLM(wfState, state, node, category, modelName, systemMessage); + // 转换系统信息结构 + List systemMessage = List.of(new SystemMessage(prompt)); + workflowUtil.streamingInvokeLLM(wfState, state, node, modelName, systemMessage); return new NodeProcessResult(); } } diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/keywordExtractor/KeywordExtractorNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/keywordExtractor/KeywordExtractorNode.java index a3d98d85..5ad7147a 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/keywordExtractor/KeywordExtractorNode.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/keywordExtractor/KeywordExtractorNode.java @@ -1,5 +1,6 @@ package org.ruoyi.workflow.workflow.node.keywordExtractor; +import dev.langchain4j.data.message.SystemMessage; import dev.langchain4j.data.message.UserMessage; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -65,12 +66,9 @@ public class KeywordExtractorNode extends AbstractWfNode { // 调用 LLM 进行关键词提取 WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class); String modelName = config.getModelName(); - String category = config.getCategory(); - List systemMessage = List.of(UserMessage.from(prompt)); - + List systemMessage = List.of(new SystemMessage(prompt)); // 使用流式调用 - workflowUtil.streamingInvokeLLM(wfState, state, node, category, modelName, systemMessage); - + workflowUtil.streamingInvokeLLM(wfState, state, node, modelName, systemMessage); return new NodeProcessResult(); } diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/knowledgeRetrieval/KnowledgeRetrievalNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/knowledgeRetrieval/KnowledgeRetrievalNode.java index fc1e4837..f17dc744 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/knowledgeRetrieval/KnowledgeRetrievalNode.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/knowledgeRetrieval/KnowledgeRetrievalNode.java @@ -1,5 +1,6 @@ package org.ruoyi.workflow.workflow.node.knowledgeRetrieval; +import dev.langchain4j.data.message.SystemMessage; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.ruoyi.workflow.entity.WorkflowComponent; @@ -150,18 +151,15 @@ public class KnowledgeRetrievalNode extends AbstractWfNode { // 使用WorkflowUtil调用LLM(流式) WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class); - List systemMessage = - List.of(dev.langchain4j.data.message.UserMessage.from(prompt)); + List systemMessage = List.of(new SystemMessage(prompt)); // 调用流式LLM - String category = StringUtils.isNotBlank(config.getCategory()) ? config.getCategory() : "llm"; String modelName = StringUtils.isNotBlank(config.getModelName()) ? config.getModelName() : "deepseek-chat"; workflowUtil.streamingInvokeLLM( wfState, tempState, tempNode, - category, modelName, systemMessage ); diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/switcher/SwitcherNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/switcher/SwitcherNode.java index 646e79eb..6c27509d 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/switcher/SwitcherNode.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/switcher/SwitcherNode.java @@ -1,9 +1,15 @@ package org.ruoyi.workflow.workflow.node.switcher; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.ruoyi.common.core.utils.SpringUtils; import org.ruoyi.workflow.entity.WorkflowComponent; import org.ruoyi.workflow.entity.WorkflowNode; +import org.ruoyi.workflow.service.WorkflowNodeService; import org.ruoyi.workflow.workflow.NodeProcessResult; import org.ruoyi.workflow.workflow.WfNodeState; import org.ruoyi.workflow.workflow.WfState; @@ -12,6 +18,8 @@ import org.ruoyi.workflow.workflow.node.AbstractWfNode; import java.math.BigDecimal; import java.util.List; +import java.util.Objects; +import java.util.Optional; /** * 条件分支节点 @@ -24,6 +32,8 @@ public class SwitcherNode extends AbstractWfNode { super(wfComponent, nodeDef, wfState, nodeState); } + private static final WorkflowNodeService workflowNodeService = SpringUtils.getBean(WorkflowNodeService.class); + @Override public NodeProcessResult onProcess() { try { @@ -233,7 +243,7 @@ public class SwitcherNode extends AbstractWfNode { */ private String getValueFromInputs(String nodeUuid, String paramName, List inputs) { log.debug("从节点UUID '{}' 搜索参数 '{}'", nodeUuid, paramName); - + String result = null; // 首先尝试从当前输入中查找 @@ -244,7 +254,7 @@ public class SwitcherNode extends AbstractWfNode { result = input.valueToString(); } } - + if (result != null) { log.info("在当前输入中找到参数 '{}': '{}'", paramName, result); return result; @@ -260,7 +270,10 @@ public class SwitcherNode extends AbstractWfNode { result = output.valueToString(); } } - + + // 根据UUID查询对应节点是否存在Param(替换成Input) + result = findParamValueInNode(nodeUuid, paramName, inputs, result); + if (result != null) { log.info("在节点 '{}' 的输出中找到参数 '{}': '{}'", nodeUuid, paramName, result); return result; @@ -282,6 +295,47 @@ public class SwitcherNode extends AbstractWfNode { return null; } + /** + * 根据节点UUID和参数名查找对应的输入值 + * 意义:修复开始节点参数名错误的问题 + * + * @param nodeUuid 节点的唯一标识符(UUID) + * @param paramName 需要查找的参数名称 + * @param inputs 输入数据列表,用于匹配参数值 + * @param result 默认返回结果,若未找到匹配项则返回该值 + * @return 返回查找到的参数值,若未找到则返回默认结果 + */ + private String findParamValueInNode(String nodeUuid, String paramName, List inputs, String result) { + // 查询工作流节点信息 + WorkflowNode workflowNode = workflowNodeService.lambdaQuery().eq(WorkflowNode::getUuid, nodeUuid).one(); + if (ObjectUtils.isNotEmpty(workflowNode)){ + // 获取节点的输入配置 + String inputConfig = workflowNode.getInputConfig(); + log.info("节点 '{}' 的输入配置: {}", nodeUuid, inputConfig); + if (StringUtils.isNotBlank(inputConfig)){ + // 解析输入配置为JSON对象 + JSONObject configJson = JSON.parseObject(inputConfig); + // 获取 user_inputs 数组 + JSONArray userInputs = configJson.getJSONArray("user_inputs"); + if (userInputs != null && !userInputs.isEmpty()) { + // 在 user_inputs 中查找匹配的参数名,并获取对应值 + Optional valueOpt = userInputs.stream() + .filter(JSONObject.class::isInstance) + .map(JSONObject.class::cast) + .filter(obj -> paramName.equals(obj.getString("name"))) + .map(matchedObj -> getValueFromInputs(nodeUuid, "input", inputs)) + .filter(Objects::nonNull) + .findFirst(); + // 若找到匹配值,则更新结果 + if (valueOpt.isPresent()) { + result = valueOpt.get(); + } + } + } + } + return result; + } + /** * 根据运算符评估条件 */ diff --git a/ruoyi-modules/ruoyi-chat/pom.xml b/ruoyi-modules/ruoyi-chat/pom.xml index 447c40f6..ae85abd9 100644 --- a/ruoyi-modules/ruoyi-chat/pom.xml +++ b/ruoyi-modules/ruoyi-chat/pom.xml @@ -16,17 +16,7 @@ org.ruoyi - ruoyi-common-core - - - - org.ruoyi - ruoyi-common-mybatis - - - - org.ruoyi - ruoyi-common-excel + ruoyi-common-chat @@ -34,17 +24,6 @@ ruoyi-common-sensitive - - org.ruoyi - ruoyi-common-tenant - - - - dev.langchain4j - langchain4j - ${langchain4j.version} - - dev.langchain4j langchain4j-open-ai @@ -139,23 +118,11 @@ ruoyi-common-doc - - - org.ruoyi - ruoyi-common-sse - - org.neo4j.driver neo4j-java-driver - - - com.alibaba - fastjson - - io.github.imfangs dify-java-client diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/McpAgent.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/McpAgent.java new file mode 100644 index 00000000..1f5d0c6a --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/McpAgent.java @@ -0,0 +1,46 @@ +package org.ruoyi.agent; + +import dev.langchain4j.agentic.Agent; +import dev.langchain4j.service.SystemMessage; +import dev.langchain4j.service.UserMessage; +import dev.langchain4j.service.V; + +public interface McpAgent { + /** + * 系统提示词:定义智能体身份、核心职责、强制遵守的规则 + * 适配SSE流式特性,明确工具全来自远端MCP服务,仅做代理调用和结果整理 + */ + @SystemMessage(""" + 你是专业的MCP服务工具代理智能体,核心能力是通过HTTP SSE流式传输协议,调用本地http://localhost:8085/sse地址上MCP服务端注册的所有工具。 + 你的核心工作职责: + 1. 准确理解用户的自然语言请求,判断需要调用MCP服务端的哪一个/哪些工具; + 2. 通过绑定的工具提供者,向MCP服务端发起工具调用请求,传递完整的工具执行参数; + 3. 实时接收MCP服务端通过SSE流式返回的工具执行结果,保证结果片段的完整性; + 4. 将流式结果按原始顺序整理为清晰、易懂的自然语言答案,返回给用户。 + + 【强制遵守的核心规则 - 无例外】 + 1. 所有工具调用必须通过远端MCP服务执行,严禁尝试本地执行任何业务逻辑; + 2. 处理SSE流式结果时,严格保留结果片段的返回顺序,不得打乱或遗漏; + 3. 若MCP服务返回错误(如工具未找到、参数错误、执行失败),直接将错误信息友好反馈给用户,无需额外推理; + 4. 工具执行结果若为结构化数据(如JSON、表格),需格式化后返回,提升可读性。 + """) + + /** + * 用户消息模板:{{query}}为参数占位符,与方法入参的@V("query")绑定 + */ + @UserMessage(""" + 请通过调用MCP服务端的工具,处理用户的以下请求: + {{query}} + """) + /** + * 智能体标识:用于日志打印、监控追踪、多智能体协作时的身份识别 + */ + @Agent("MCP服务SSE流式代理智能体-连接本地8085端口") + /** + * 智能体对外调用入口方法 + * @param query 用户的自然语言请求(如:生成订单数据柱状图、查询今日天气) + * @V("query") 将方法入参值绑定到@UserMessage的{{query}}占位符中 + * @return 整理后的MCP工具执行结果(流式结果会自动拼接为完整字符串) + */ + String callMcpTool(@V("query") String query); +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/config/McpSseConfig.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/config/McpSseConfig.java new file mode 100644 index 00000000..12451034 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/config/McpSseConfig.java @@ -0,0 +1,21 @@ +package org.ruoyi.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "mcp.sse") +public class McpSseConfig { + + /** + * mcp对外暴露的端点地址 + */ + private String url; + + /** + * 是否开启 + */ + private boolean enabled; +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatController.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatController.java index b6a434e1..89566dee 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatController.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatController.java @@ -4,7 +4,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.ruoyi.domain.dto.request.ChatRequest; +import org.ruoyi.common.chat.domain.dto.request.ChatRequest; import org.ruoyi.service.chat.impl.ChatServiceFacade; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatModelController.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatModelController.java index dc3c7c79..06212c1d 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatModelController.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatModelController.java @@ -6,8 +6,10 @@ import lombok.RequiredArgsConstructor; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.*; import cn.dev33.satoken.annotation.SaCheckPermission; +import org.ruoyi.common.chat.Service.IChatModelService; +import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.enums.ModelType; -import org.ruoyi.service.chat.IChatModelService; import org.springframework.web.bind.annotation.*; import org.springframework.validation.annotation.Validated; import org.ruoyi.common.idempotent.annotation.RepeatSubmit; @@ -19,8 +21,6 @@ import org.ruoyi.common.core.validate.AddGroup; import org.ruoyi.common.core.validate.EditGroup; import org.ruoyi.common.log.enums.BusinessType; import org.ruoyi.common.excel.utils.ExcelUtil; -import org.ruoyi.domain.vo.chat.ChatModelVo; -import org.ruoyi.domain.bo.chat.ChatModelBo; import org.ruoyi.common.mybatis.core.page.TableDataInfo; /** diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/factory/EmbeddingModelFactory.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/factory/EmbeddingModelFactory.java index 657b2d18..0aecf556 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/factory/EmbeddingModelFactory.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/factory/EmbeddingModelFactory.java @@ -2,8 +2,8 @@ package org.ruoyi.factory; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.ruoyi.domain.vo.chat.ChatModelVo; -import org.ruoyi.service.chat.IChatModelService; +import org.ruoyi.common.chat.Service.IChatModelService; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.service.embed.BaseEmbedModelService; import org.ruoyi.service.embed.MultiModalEmbedModelService; import org.springframework.beans.factory.NoSuchBeanDefinitionException; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/chat/ChatModelMapper.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/chat/ChatModelMapper.java index 0cc5b97a..50c296a4 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/chat/ChatModelMapper.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/chat/ChatModelMapper.java @@ -1,7 +1,7 @@ package org.ruoyi.mapper.chat; -import org.ruoyi.domain.entity.chat.ChatModel; -import org.ruoyi.domain.vo.chat.ChatModelVo; +import org.ruoyi.common.chat.domain.entity.chat.ChatModel; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.common.mybatis.core.mapper.BaseMapperPlus; /** diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/IChatMessageService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/IChatMessageService.java index 6f610eea..3289b5f2 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/IChatMessageService.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/IChatMessageService.java @@ -1,9 +1,9 @@ package org.ruoyi.service.chat; +import org.ruoyi.common.chat.domain.dto.ChatMessageDTO; 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; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/IChatService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/IChatService.java deleted file mode 100644 index 18fe4056..00000000 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/IChatService.java +++ /dev/null @@ -1,26 +0,0 @@ -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(); - -} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/AbstractStreamingChatService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/AbstractStreamingChatService.java index c8389004..c671e679 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/AbstractStreamingChatService.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/AbstractStreamingChatService.java @@ -4,6 +4,7 @@ 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.data.message.UserMessage; import dev.langchain4j.mcp.McpToolProvider; import dev.langchain4j.mcp.client.DefaultMcpClient; import dev.langchain4j.mcp.client.McpClient; @@ -23,20 +24,21 @@ 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.chat.Service.IChatService; +import org.ruoyi.common.chat.domain.dto.request.ChatRequest; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; +import org.ruoyi.common.core.utils.ObjectUtils; import org.ruoyi.common.core.utils.SpringUtils; +import org.ruoyi.common.core.utils.StringUtils; 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.util.CollectionUtils; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** @@ -76,23 +78,50 @@ public abstract class AbstractStreamingChatService implements IChatService { */ @Override public SseEmitter chat(ChatModelVo chatModelVo, ChatRequest chatRequest, SseEmitter emitter, Long userId, String tokenValue) { + return executeChat(chatModelVo, chatRequest, emitter, userId, tokenValue, null); + } + + /** + * 定义聊天流程骨架(包含流式回调结构) + */ + @Override + public SseEmitter chat(ChatModelVo chatModelVo, ChatRequest chatRequest, SseEmitter emitter, Long userId, String tokenValue, StreamingChatResponseHandler handler) { + return executeChat(chatModelVo, chatRequest, emitter, userId, tokenValue, handler); + } + + /** + * 定义聊天流程骨架 + */ + public SseEmitter executeChat(ChatModelVo chatModelVo, ChatRequest chatRequest, SseEmitter emitter, Long userId, String tokenValue, StreamingChatResponseHandler handler) { try { - String content = chatRequest.getMessages().get(0).getContent(); + String content = Optional.ofNullable(chatRequest.getMessages()).filter(messages -> !messages.isEmpty()) + // 对话逻辑:从 messages 筛选第一个元素 + .map(messages -> messages.get(0).getContent()) + .filter(StringUtils::isNotBlank) + // 工作流逻辑:从 chatMessages 筛选 UserMessage 的文本 + .orElseGet(() -> Optional.ofNullable(chatRequest.getChatMessages()).orElse(List.of()).stream() + .filter(message -> message instanceof UserMessage um) + .map(message -> ((UserMessage) message).singleText()) + .filter(StringUtils::isNotBlank) + .findFirst() + .orElse("")); + // 保存用户消息 saveChatMessage(chatRequest, userId, content, RoleType.USER.getName(), chatModelVo); - // 使用长期记忆增强的消息列表 List messagesWithMemory = buildMessagesWithMemory(chatRequest); - if(chatRequest.getEnableThinking()){ - String msg = doAgent(content,chatModelVo); + 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 { + } else { // 创建包含内存管理的响应处理器 - StreamingChatResponseHandler handler = createResponseHandler(chatRequest, userId, tokenValue, chatModelVo); + if (ObjectUtils.isEmpty(handler)) { + handler = createResponseHandler(chatRequest, userId, tokenValue, chatModelVo); + } // 调用具体实现的聊天方法 doChat(chatModelVo, chatRequest, messagesWithMemory, handler); } @@ -115,8 +144,6 @@ public abstract class AbstractStreamingChatService implements IChatService { */ protected List buildMessagesWithMemory(ChatRequest chatRequest) { List messages = new ArrayList<>(); - - // 加载历史消息 if (enablePersistentMemory && chatRequest.getSessionId() != null) { MessageWindowChatMemory memory = createChatMemory(chatRequest.getSessionId()); if (memory != null) { @@ -126,8 +153,13 @@ public abstract class AbstractStreamingChatService implements IChatService { log.debug("已加载 {} 条历史消息用于会话 {}", historicalMessages.size(), chatRequest.getSessionId()); } } + return messages; + } + // 工作流方式 + List chatMessages = chatRequest.getChatMessages(); + if (!CollectionUtils.isEmpty(chatMessages)){ + messages.addAll(chatMessages); } - return messages; } @@ -172,16 +204,16 @@ public abstract class AbstractStreamingChatService implements IChatService { * * @param chatModelVo 模型配置 * @param chatRequest 聊天请求 - * @param handler 响应处理器 + * @param handler 响应处理器 */ - protected abstract void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List messagesWithMemory,StreamingChatResponseHandler handler); + protected abstract void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List messagesWithMemory, StreamingChatResponseHandler handler); /** * 创建标准的响应处理器 * * @param chatRequest 聊天请求,包含sessionId等上下文信息 - * @param userId 用户ID - * @param tokenValue 会话令牌 + * @param userId 用户ID + * @param tokenValue 会话令牌 * @param chatModelVo 模型配置 * @return 标准的流式响应处理器 */ @@ -225,7 +257,7 @@ public abstract class AbstractStreamingChatService implements IChatService { @Override public void onError(Throwable error) { log.error("{}流式响应错误: {}", getProviderName(), error.getMessage(), error); - + // 发送错误消息到前端 try { String errorMessage = String.format("模型调用失败: %s", error.getMessage()); @@ -233,7 +265,7 @@ public abstract class AbstractStreamingChatService implements IChatService { } catch (Exception e) { log.error("发送错误消息失败: {}", e.getMessage(), e); } - + // 关闭SSE连接,避免前端一直等待 try { SseMessageUtils.completeConnection(userId, tokenValue); @@ -248,9 +280,9 @@ public abstract class AbstractStreamingChatService implements IChatService { * 保存聊天消息到数据库 * * @param chatRequest 聊天请求 - * @param userId 用户ID - * @param content 消息内容 - * @param role 消息角色 + * @param userId 用户ID + * @param content 消息内容 + * @param role 消息角色 * @param chatModelVo 模型配置 */ private void saveChatMessage(ChatRequest chatRequest, Long userId, String content, String role, ChatModelVo chatModelVo) { @@ -290,7 +322,7 @@ public abstract class AbstractStreamingChatService implements IChatService { */ public abstract String getProviderName(); - protected String doAgent(String userMessage,ChatModelVo chatModelVo) { + protected String doAgent(String userMessage, ChatModelVo chatModelVo) { // 步骤1: 配置MCP传输层 - 连接到bing-cn-mcp服务器 // 该服务提供两个工具: bing_search (必应搜索) 和 crawl_webpage (网页抓取) McpTransport transport = new StdioMcpTransport.Builder() @@ -357,7 +389,7 @@ public abstract class AbstractStreamingChatService implements IChatService { SupervisorAgent supervisor = AgenticServices .supervisorBuilder() .chatModel(PLANNER_MODEL) - .subAgents(sqlAgent,chartGenerationAgent) + .subAgents(sqlAgent, chartGenerationAgent) .responseStrategy(SupervisorResponseStrategy.LAST) .build(); diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatMessageServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatMessageServiceImpl.java index ac9afff1..0804f010 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatMessageServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatMessageServiceImpl.java @@ -1,5 +1,6 @@ package org.ruoyi.service.chat.impl; +import org.ruoyi.common.chat.domain.dto.ChatMessageDTO; import org.ruoyi.common.core.utils.MapstructUtils; import org.ruoyi.common.core.utils.StringUtils; import org.ruoyi.common.mybatis.core.page.TableDataInfo; @@ -9,7 +10,6 @@ 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; @@ -146,7 +146,7 @@ public class ChatMessageServiceImpl implements IChatMessageService { * @return 消息DTO列表 */ @Override - public List getMessagesBySessionId(Long sessionId) { + public List getMessagesBySessionId(Long sessionId) { if (sessionId == null) { return new java.util.ArrayList<>(); } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatModelServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatModelServiceImpl.java index 5735cfb1..32c8f85a 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatModelServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatModelServiceImpl.java @@ -1,5 +1,9 @@ package org.ruoyi.service.chat.impl; +import org.ruoyi.common.chat.Service.IChatModelService; +import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo; +import org.ruoyi.common.chat.domain.entity.chat.ChatModel; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.common.core.utils.MapstructUtils; import org.ruoyi.common.core.utils.StringUtils; import org.ruoyi.common.mybatis.core.page.TableDataInfo; @@ -9,11 +13,7 @@ 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; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatServiceFacade.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatServiceFacade.java index 0a81547a..d35f9d9c 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatServiceFacade.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatServiceFacade.java @@ -4,16 +4,17 @@ import cn.dev33.satoken.stp.StpUtil; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.ruoyi.common.chat.Service.IChatModelService; +import org.ruoyi.common.chat.Service.IChatService; +import org.ruoyi.common.chat.domain.dto.ChatMessageDTO; +import org.ruoyi.common.chat.domain.dto.request.ChatRequest; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; +import org.ruoyi.common.chat.factory.ChatServiceFactory; 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; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/memory/ChatMemoryUsageExample.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/memory/ChatMemoryUsageExample.java index a8d9e1c8..2b9a7da4 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/memory/ChatMemoryUsageExample.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/memory/ChatMemoryUsageExample.java @@ -1,17 +1,7 @@ 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; +import org.ruoyi.common.chat.domain.dto.request.ChatRequest; /** * 长期记忆使用示例 @@ -322,4 +312,4 @@ public class ChatMemoryUsageExample { private boolean isTemporaryChatSession(ChatRequest request) { return false; } -} \ No newline at end of file +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/memory/PersistentChatMemoryStore.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/memory/PersistentChatMemoryStore.java index 4cd38aa4..63d9e151 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/memory/PersistentChatMemoryStore.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/memory/PersistentChatMemoryStore.java @@ -3,8 +3,8 @@ 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.chat.domain.dto.ChatMessageDTO; import org.ruoyi.common.core.utils.SpringUtils; -import org.ruoyi.domain.dto.ChatMessageDTO; import org.ruoyi.service.chat.IChatMessageService; import java.util.ArrayList; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/OllamaServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/OllamaServiceImpl.java index 8c534c3f..f09479af 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/OllamaServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/OllamaServiceImpl.java @@ -5,11 +5,11 @@ 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 org.ruoyi.common.chat.domain.dto.request.ChatRequest; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import java.util.List; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/OpenAIServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/OpenAIServiceImpl.java index bad40b0d..87147afe 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/OpenAIServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/OpenAIServiceImpl.java @@ -1,28 +1,13 @@ 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.common.chat.domain.dto.request.ChatRequest; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.enums.ChatModeType; import org.ruoyi.service.chat.impl.AbstractStreamingChatService; import org.springframework.stereotype.Service; @@ -57,8 +42,6 @@ public class OpenAIServiceImpl extends AbstractStreamingChatService { } - - @Override public String getProviderName() { return ChatModeType.OPEN_AI.getCode(); diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/QianWenChatServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/QianWenChatServiceImpl.java index 44bb2fe6..ab583520 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/QianWenChatServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/QianWenChatServiceImpl.java @@ -1,16 +1,34 @@ package org.ruoyi.service.chat.impl.provider; +import dev.langchain4j.agent.tool.ToolSpecification; +import dev.langchain4j.agentic.AgenticServices; +import dev.langchain4j.agentic.supervisor.SupervisorAgent; +import dev.langchain4j.agentic.supervisor.SupervisorResponseStrategy; +import dev.langchain4j.community.model.dashscope.QwenChatModel; import dev.langchain4j.community.model.dashscope.QwenStreamingChatModel; import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.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.http.StreamableHttpMcpTransport; import dev.langchain4j.model.chat.StreamingChatModel; import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; +import dev.langchain4j.service.tool.ToolProvider; import lombok.extern.slf4j.Slf4j; +import org.ruoyi.agent.McpAgent; +import org.ruoyi.config.McpSseConfig; 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.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import org.ruoyi.common.chat.domain.dto.request.ChatRequest; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; +import java.util.ArrayList; import java.util.List; /** @@ -23,6 +41,17 @@ import java.util.List; @Slf4j public class QianWenChatServiceImpl extends AbstractStreamingChatService { + @Autowired + private McpSseConfig mcpSseConfig; + + /** + * 千问开发者默认地址 + */ + private static final String QWEN_API_HOST = "https://dashscope.aliyuncs.com/api/v1"; + + // 添加文档解析的前缀字段 + private static final String UPLOAD_FILE_API_PREFIX = "fileid"; + @Override protected StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo,ChatRequest chatRequest) { return QwenStreamingChatModel.builder() @@ -35,7 +64,98 @@ public class QianWenChatServiceImpl extends AbstractStreamingChatService { protected void doChat(ChatModelVo chatModelVo,ChatRequest chatRequest,List messagesWithMemory, StreamingChatResponseHandler handler) { StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo,chatRequest); - streamingChatModel.chat(messagesWithMemory, handler); + // 判断是否存在需要使用阿里千问的文档解析功能 + List chatMessages = hasFileIdData(messagesWithMemory); + streamingChatModel.chat(chatMessages, handler); + } + + /** + * 检查是否包含fileId数据 + */ + private List hasFileIdData(List messagesWithMemory) { + if (CollectionUtils.isEmpty(messagesWithMemory)) { + return messagesWithMemory; + } + + // 找到包含阿里上传文件前缀的用户信息 + var foundUserMessage = messagesWithMemory.stream() + .filter(message -> message instanceof UserMessage) + .map(message -> (UserMessage) message) + .filter(userMessage -> + userMessage.singleText().toLowerCase().contains(UPLOAD_FILE_API_PREFIX.toLowerCase()) + ) + .findFirst(); + + // 找到原本SystemMessage + var systemMessage = messagesWithMemory.stream() + .filter(message -> message instanceof SystemMessage) + .map(message -> (SystemMessage) message) + .findFirst(); + + // 判断是否存在并重新构建信息体(符合千问文档解析格式) + return foundUserMessage.map(userMsg -> { + List messages = new ArrayList<>(); + messages.add(new SystemMessage(userMsg.singleText())); + systemMessage.ifPresent(sysMsg -> messages.add(new UserMessage(sysMsg.text()))); + return messages; + }).orElse(messagesWithMemory); + } + + /** + * 调用MCP服务(智能体) + * @param userMessage 用户信息 + * @param chatModelVo 模型信息 + * @return 返回LLM信息 + */ + protected String doAgent(String userMessage,ChatModelVo chatModelVo) { + // 判断是否开启MCP服务 + if (!mcpSseConfig.isEnabled()) { + return ""; + } + + // 步骤1:根据SSE对外暴露端点连接 + McpTransport httpMcpTransport = new StreamableHttpMcpTransport.Builder(). + url(mcpSseConfig.getUrl()). + logRequests(true). + build(); + + // 步骤2:开启客户端连接 + McpClient mcpClient = new DefaultMcpClient.Builder() + .transport(httpMcpTransport) + .build(); + + // 获取所有mcp工具 + List toolSpecifications = mcpClient.listTools(); + System.out.println(toolSpecifications); + + // 步骤3:将mcp对象包装 + ToolProvider toolProvider = McpToolProvider.builder() + .mcpClients(List.of(mcpClient)) + .build(); + + // 步骤4:加载LLM模型对话 + QwenChatModel qwenChatModel = QwenChatModel.builder() + .baseUrl(QWEN_API_HOST) + .apiKey(chatModelVo.getApiKey()) + .modelName(chatModelVo.getModelName()) + .build(); + + // 步骤5:将MCP对象由智能体Agent管控 + McpAgent mcpAgent = AgenticServices.agentBuilder(McpAgent.class) + .chatModel(qwenChatModel) + .toolProvider(toolProvider) + .build(); + + // 步骤6:将所有MCP对象由超级智能体管控 + SupervisorAgent supervisor = AgenticServices + .supervisorBuilder() + .chatModel(qwenChatModel) + .subAgents(mcpAgent) + .responseStrategy(SupervisorResponseStrategy.LAST) + .build(); + + // 步骤7:调用大模型LLM + return supervisor.invoke(userMessage); } @Override diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/embed/BaseEmbedModelService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/embed/BaseEmbedModelService.java index 4b3433cd..d7c9d4c9 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/embed/BaseEmbedModelService.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/embed/BaseEmbedModelService.java @@ -1,7 +1,7 @@ package org.ruoyi.service.embed; import dev.langchain4j.model.embedding.EmbeddingModel; -import org.ruoyi.domain.vo.chat.ChatModelVo; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.enums.ModalityType; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/embed/impl/AliBaiLianBaseEmbedProvider.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/embed/impl/AliBaiLianBaseEmbedProvider.java index 3b24b5f8..9a0f67f5 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/embed/impl/AliBaiLianBaseEmbedProvider.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/embed/impl/AliBaiLianBaseEmbedProvider.java @@ -5,7 +5,7 @@ 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.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.springframework.stereotype.Component; import org.ruoyi.enums.ModalityType; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/embed/impl/AliBaiLianMultiEmbeddingProvider.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/embed/impl/AliBaiLianMultiEmbeddingProvider.java index 7369bf46..38cc7323 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/embed/impl/AliBaiLianMultiEmbeddingProvider.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/embed/impl/AliBaiLianMultiEmbeddingProvider.java @@ -7,10 +7,10 @@ import dev.langchain4j.model.output.Response; import dev.langchain4j.model.output.TokenUsage; import lombok.extern.slf4j.Slf4j; import okhttp3.*; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; 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; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/embed/impl/OllamaEmbeddingProvider.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/embed/impl/OllamaEmbeddingProvider.java index 7dfb5e38..8d48a30e 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/embed/impl/OllamaEmbeddingProvider.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/embed/impl/OllamaEmbeddingProvider.java @@ -4,7 +4,7 @@ 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.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.enums.ModalityType; import org.ruoyi.service.embed.BaseEmbedModelService; import org.springframework.stereotype.Component; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/embed/impl/OpenAiEmbeddingProvider.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/embed/impl/OpenAiEmbeddingProvider.java index 21ad6910..c44b5ad8 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/embed/impl/OpenAiEmbeddingProvider.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/embed/impl/OpenAiEmbeddingProvider.java @@ -4,7 +4,7 @@ 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.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.enums.ModalityType; import org.ruoyi.service.embed.BaseEmbedModelService; import org.springframework.stereotype.Component; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/IGraphLLMService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/IGraphLLMService.java index 52964d51..4369d10d 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/IGraphLLMService.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/IGraphLLMService.java @@ -1,7 +1,7 @@ package org.ruoyi.service.graph; -import org.ruoyi.domain.vo.chat.ChatModelVo; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; /** * 图谱LLM服务接口 diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/DeepSeekGraphLLMServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/DeepSeekGraphLLMServiceImpl.java index ad2fe1e4..04d6f4be 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/DeepSeekGraphLLMServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/DeepSeekGraphLLMServiceImpl.java @@ -5,7 +5,7 @@ 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.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.service.graph.IGraphLLMService; import org.springframework.stereotype.Service; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/DifyGraphLLMServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/DifyGraphLLMServiceImpl.java index 570136ea..8204570c 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/DifyGraphLLMServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/DifyGraphLLMServiceImpl.java @@ -10,7 +10,7 @@ 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.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.service.graph.IGraphLLMService; import org.springframework.stereotype.Service; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/GraphExtractionServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/GraphExtractionServiceImpl.java index 7b617cf6..890bdc12 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/GraphExtractionServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/GraphExtractionServiceImpl.java @@ -3,15 +3,15 @@ package org.ruoyi.service.graph.impl; import cn.hutool.core.util.StrUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.ruoyi.common.chat.Service.IChatModelService; +import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; 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; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/OpenAIGraphLLMServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/OpenAIGraphLLMServiceImpl.java index f5d2f601..776c781c 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/OpenAIGraphLLMServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/OpenAIGraphLLMServiceImpl.java @@ -2,7 +2,7 @@ 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.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.service.graph.IGraphLLMService; import org.springframework.stereotype.Service; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/knowledge/impl/KnowledgeAttachServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/knowledge/impl/KnowledgeAttachServiceImpl.java index 90de896d..b7106120 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/knowledge/impl/KnowledgeAttachServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/knowledge/impl/KnowledgeAttachServiceImpl.java @@ -2,6 +2,8 @@ package org.ruoyi.service.knowledge.impl; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.RandomUtil; +import org.ruoyi.common.chat.Service.IChatModelService; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.common.core.domain.dto.OssDTO; import org.ruoyi.common.core.service.OssService; import org.ruoyi.common.core.utils.MapstructUtils; @@ -18,13 +20,11 @@ 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; diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/controller/system/SysOssController.java b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/controller/system/SysOssController.java index 78d192be..07ff778e 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/controller/system/SysOssController.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/controller/system/SysOssController.java @@ -81,6 +81,20 @@ public class SysOssController extends BaseController { return R.ok(uploadVo); } + /** + * 上传文件(千问百炼版) + * + * @param file 文件 + */ + @Log(title = "上传文件(千问百炼版)", businessType = BusinessType.INSERT) + @PostMapping(value = "/fileUpload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public R fileUpload(@RequestPart("file") MultipartFile file) { + if (ObjectUtil.isNull(file)) { + return R.fail("上传文件不能为空"); + } + return R.ok(ossService.fileUpload(file)); + } + /** * 下载OSS对象 * diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/ISysOssService.java b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/ISysOssService.java index aca2ff7f..e2cbb1ac 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/ISysOssService.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/ISysOssService.java @@ -3,6 +3,7 @@ package org.ruoyi.system.service; import org.ruoyi.common.mybatis.core.page.PageQuery; import org.ruoyi.common.mybatis.core.page.TableDataInfo; import org.ruoyi.system.domain.bo.SysOssBo; +import org.ruoyi.system.domain.vo.SysOssUploadVo; import org.ruoyi.system.domain.vo.SysOssVo; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.multipart.MultipartFile; @@ -60,6 +61,14 @@ public interface ISysOssService { */ SysOssVo upload(File file); + /** + * 上传文件到千问平台(千问百炼版本) + * + * @param file 要上传的 MultipartFile 对象 + * @return 上传成功后的 SysOssVo 对象,包含文件信息 + */ + SysOssUploadVo fileUpload(MultipartFile file); + /** * 文件下载方法,支持一次性下载完整文件 * diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/impl/SysOssServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/impl/SysOssServiceImpl.java index 24939920..8ca967ea 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/impl/SysOssServiceImpl.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/service/impl/SysOssServiceImpl.java @@ -11,6 +11,7 @@ import lombok.RequiredArgsConstructor; import org.ruoyi.common.core.constant.CacheNames; import org.ruoyi.common.core.domain.dto.OssDTO; import org.ruoyi.common.core.exception.ServiceException; +import org.ruoyi.common.core.service.ConfigService; import org.ruoyi.common.core.service.OssService; import org.ruoyi.common.core.utils.MapstructUtils; import org.ruoyi.common.core.utils.SpringUtils; @@ -20,6 +21,7 @@ import org.ruoyi.common.core.utils.file.FileUtils; import org.ruoyi.common.json.utils.JsonUtils; import org.ruoyi.common.mybatis.core.page.PageQuery; import org.ruoyi.common.mybatis.core.page.TableDataInfo; +import org.ruoyi.common.oss.constant.OssConstant; import org.ruoyi.common.oss.core.OssClient; import org.ruoyi.common.oss.entity.UploadResult; import org.ruoyi.common.oss.enums.AccessPolicyType; @@ -27,10 +29,14 @@ import org.ruoyi.common.oss.factory.OssFactory; import org.ruoyi.system.domain.SysOss; import org.ruoyi.system.domain.SysOssExt; import org.ruoyi.system.domain.bo.SysOssBo; +import org.ruoyi.system.domain.vo.SysOssUploadVo; import org.ruoyi.system.domain.vo.SysOssVo; import org.ruoyi.system.mapper.SysOssMapper; import org.ruoyi.system.service.ISysOssService; import org.jetbrains.annotations.NotNull; +import org.ruoyi.system.utils.QwenFileUploadUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.annotation.Cacheable; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; @@ -38,6 +44,9 @@ import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.time.Duration; import java.util.ArrayList; import java.util.Collection; @@ -55,6 +64,18 @@ public class SysOssServiceImpl implements ISysOssService, OssService { private final SysOssMapper baseMapper; + private final ConfigService configService; + + // 默认密钥 + private static String API_KEY; + + // 默认api路径地址 + private static String API_HOST; + + // 上传文件服务器地址 + @Value("${sys.upload.path}") + private String UPLOAD_PATH; + /** * 查询OSS对象存储列表 * @@ -220,6 +241,73 @@ public class SysOssServiceImpl implements ISysOssService, OssService { return BeanUtil.toBean(sysOssVo, OssDTO.class); } + /** + * 上传文件至千问百炼平台 + * @param file 要上传的 MultipartFile 对象 + * @return 上传成功后的 SysOssVo 对象,包含文件信息 + */ + @Override + public SysOssUploadVo fileUpload(MultipartFile file) { + String originalName = file.getOriginalFilename(); + if (StringUtils.isEmpty(originalName)){ + throw new ServiceException("文件名不能为空"); + } + int lastDotIndex = originalName != null ? originalName.lastIndexOf(".") : -1; + String prefix = lastDotIndex > 0 ? originalName.substring(0, lastDotIndex) : ""; + String suffix = lastDotIndex > 0 ? originalName.substring(lastDotIndex) : ""; + try { + // 确保上传目录存在 + Path uploadDir = Paths.get(UPLOAD_PATH); + if (!Files.exists(uploadDir)) { + Files.createDirectories(uploadDir); + } + // 生成上传文件名 + Path targetPath = uploadDir.resolve(originalName); + // 直接保存文件 + File pathFile = targetPath.toFile(); + file.transferTo(pathFile); + // 获取配置 + initConfig(); + // 使用工具类上传文件到阿里云 + String fileId = QwenFileUploadUtils.uploadFile(pathFile, API_HOST, API_KEY); + if (StringUtils.isEmpty(fileId)) { + throw new ServiceException("文件上传失败,未获取到fileId"); + } + // 拿到上传地址的路径地址 + String filePath = targetPath.toAbsolutePath().toString(); + // 保存文件信息到数据库 + return buildEntity(filePath, fileId, suffix, prefix, originalName); + } catch (IOException e) { + throw new ServiceException("文件上传失败: " + e.getMessage()); + } + } + + /** + * 保存数据到数据库中 + * @param filePath 上传文件地址 + * @param fileId 百炼平台返回fileID + * @param suffix 文件后缀 + * @param prefix 文件前缀 + * @param originalName 文件名称 + * @return SysOssUploadVo 上传返回VO对象 + */ + @NotNull + private SysOssUploadVo buildEntity(String filePath, String fileId, String suffix, String prefix, String originalName) { + String url = OssConstant.FILE_ID_PREFIX + fileId; + SysOss oss = new SysOss(); + oss.setUrl(url); + oss.setExt1(filePath); + oss.setFileSuffix(suffix); + oss.setFileName(prefix); + oss.setOriginalName(originalName); + oss.setService(OssConstant.DASH_SCOPE); + baseMapper.insert(oss); + SysOssUploadVo uploadVo = new SysOssUploadVo(); + BeanUtils.copyProperties(oss, uploadVo); + uploadVo.setFileName(originalName); + return uploadVo; + } + /** * 上传文件到对象存储服务,并保存文件信息到数据库 * @@ -286,4 +374,20 @@ public class SysOssServiceImpl implements ISysOssService, OssService { } return oss; } + + /** + * 初始化配置并返回API密钥和主机 + */ + private void initConfig() { + String apiKey = configService.getConfigValue(OssConstant.CONFIG_NAME_KEY); + if (StringUtils.isEmpty(apiKey)) { + throw new ServiceException("请先配置Qwen上传文件相关API_KEY"); + } + API_KEY = apiKey; + String apiHost = configService.getConfigValue(OssConstant.CONFIG_NAME_URL); + if (StringUtils.isEmpty(apiHost)) { + throw new ServiceException("请先配置Qwen上传文件相关API_HOST"); + } + API_HOST = apiHost; + } } diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/utils/QwenFileUploadUtils.java b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/utils/QwenFileUploadUtils.java new file mode 100644 index 00000000..e4ea9ab5 --- /dev/null +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/ruoyi/system/utils/QwenFileUploadUtils.java @@ -0,0 +1,50 @@ +package org.ruoyi.system.utils; + +import com.alibaba.fastjson.JSONObject; +import okhttp3.*; +import org.ruoyi.common.core.utils.StringUtils; + +import java.io.File; +import java.io.IOException; +import java.rmi.ServerException; + +/*** + * 千问上传文件工具类 + */ +public class QwenFileUploadUtils { + + // 上传本地文件 + public static String uploadFile(File file, String apiHost, String apiKey) throws IOException { + OkHttpClient client = new OkHttpClient(); + + // 构建 multipart/form-data 请求体(千问要求的格式) + RequestBody requestBody = new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("file", file.getName(), // 参数名必须为 file + RequestBody.create(MediaType.parse("application/octet-stream"), file)) + .addFormDataPart("purpose", "file-extract") // 必须为 file-extract,文档解析专用 + .build(); + + // 构建请求(必须为 POST 方法) + Request request = new Request.Builder() + .url(apiHost) + .post(requestBody) + .addHeader("Authorization", apiKey) // 认证头格式正确 + .build(); + + // 发送请求并解析 fileId + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new ServerException("上传失败:" + response.code() + " " + response.message()); + } + // 解析响应体,获取 fileId + String responseBody = response.body().string(); + if (StringUtils.isEmpty(responseBody)){ + throw new ServerException("上传失败:响应体为空"); + } + JSONObject jsonObject = JSONObject.parseObject(responseBody); + // 千问返回的 fileId + return jsonObject.getString("id"); + } + } +}