AI工作流优化

This commit is contained in:
zhang
2026-02-13 18:14:39 +08:00
parent afa3c78180
commit 885d46a4aa
59 changed files with 863 additions and 287 deletions

View File

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

View File

@@ -11,6 +11,7 @@
<modules> <modules>
<module>ruoyi-common-bom</module> <module>ruoyi-common-bom</module>
<module>ruoyi-common-chat</module>
<module>ruoyi-common-social</module> <module>ruoyi-common-social</module>
<module>ruoyi-common-core</module> <module>ruoyi-common-core</module>
<module>ruoyi-common-doc</module> <module>ruoyi-common-doc</module>

View File

@@ -26,6 +26,13 @@
<version>${revision}</version> <version>${revision}</version>
</dependency> </dependency>
<!-- 对话模块 -->
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-chat</artifactId>
<version>${revision}</version>
</dependency>
<!-- 接口模块 --> <!-- 接口模块 -->
<dependency> <dependency>
<groupId>org.ruoyi</groupId> <groupId>org.ruoyi</groupId>

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common-chat</artifactId>
<description>
ruoyi-common-chat chat服务
</description>
<dependencies>
<!-- SpringWeb模块 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-sse</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-excel</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-tenant</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -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.PageQuery;
import org.ruoyi.common.mybatis.core.page.TableDataInfo; 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.Collection;
import java.util.List; import java.util.List;

View File

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

View File

@@ -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 io.github.linpeilie.annotations.AutoMapper;
import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; 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 * 模型管理业务对象 chat_model

View File

@@ -1,4 +1,4 @@
package org.ruoyi.domain.dto; package org.ruoyi.common.chat.domain.dto;
import lombok.Data; import lombok.Data;

View File

@@ -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 jakarta.validation.constraints.NotEmpty;
import lombok.Data; import lombok.Data;
import org.ruoyi.domain.dto.ChatMessageDTO; import org.ruoyi.common.chat.domain.dto.ChatMessageDTO;
import java.util.List; import java.util.List;
/** /**
@@ -16,7 +18,6 @@ public class ChatRequest {
@NotEmpty(message = "对话消息不能为空") @NotEmpty(message = "对话消息不能为空")
private List<ChatMessageDTO> messages; private List<ChatMessageDTO> messages;
@NotEmpty(message = "传入的模型不能为空") @NotEmpty(message = "传入的模型不能为空")
private String model; private String model;
@@ -60,4 +61,9 @@ public class ChatRequest {
*/ */
private String token; private String token;
/**
* 原生对话对象
*/
private List<ChatMessage> chatMessages;
} }

View File

@@ -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 com.baomidou.mybatisplus.annotation.TableId;
import org.ruoyi.common.tenant.core.TenantEntity; import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.ruoyi.common.tenant.core.TenantEntity;
import java.io.Serial; import java.io.Serial;

View File

@@ -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.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import io.github.linpeilie.annotations.AutoMapper; import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data; import lombok.Data;
import org.ruoyi.common.chat.domain.entity.chat.ChatModel;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
/** /**
* 模型管理视图对象 chat_model * 模型管理视图对象 chat_model
* *
@@ -110,3 +109,4 @@ public class ChatModelVo implements Serializable {
} }

View File

@@ -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.beans.BeansException;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationContextAware;

View File

@@ -61,6 +61,20 @@
<artifactId>s3-transfer-manager</artifactId> <artifactId>s3-transfer-manager</artifactId>
</dependency> </dependency>
<!-- OpenAI Java SDK -->
<dependency>
<groupId>com.openai</groupId>
<artifactId>openai-java</artifactId>
<version>4.8.0</version>
</dependency>
<!-- OkHttp -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -37,4 +37,16 @@ public interface OssConstant {
*/ */
String IS_HTTPS = "Y"; 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";
} }

View File

@@ -26,7 +26,7 @@
<dependency> <dependency>
<groupId>org.ruoyi</groupId> <groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-core</artifactId> <artifactId>ruoyi-common-chat</artifactId>
</dependency> </dependency>
<dependency> <dependency>
@@ -34,13 +34,6 @@
<artifactId>ruoyi-common-web</artifactId> <artifactId>ruoyi-common-web</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-mybatis</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.ruoyi</groupId> <groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-satoken</artifactId> <artifactId>ruoyi-common-satoken</artifactId>

View File

@@ -4,6 +4,7 @@ import org.ruoyi.workflow.entity.WorkflowComponent;
import org.ruoyi.workflow.entity.WorkflowNode; import org.ruoyi.workflow.entity.WorkflowNode;
import org.ruoyi.workflow.workflow.node.AbstractWfNode; import org.ruoyi.workflow.workflow.node.AbstractWfNode;
import org.ruoyi.workflow.workflow.node.EndNode; 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.answer.LLMAnswerNode;
import org.ruoyi.workflow.workflow.node.httpRequest.HttpRequestNode; import org.ruoyi.workflow.workflow.node.httpRequest.HttpRequestNode;
import org.ruoyi.workflow.workflow.node.keywordExtractor.KeywordExtractorNode; 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 MAIL_SEND -> wfNode = new MailSendNode(wfComponent, nodeDefinition, wfState, nodeState);
case HTTP_REQUEST -> wfNode = new HttpRequestNode(wfComponent, nodeDefinition, wfState, nodeState); case HTTP_REQUEST -> wfNode = new HttpRequestNode(wfComponent, nodeDefinition, wfState, nodeState);
case SWITCHER -> wfNode = new SwitcherNode(wfComponent, nodeDefinition, wfState, nodeState); case SWITCHER -> wfNode = new SwitcherNode(wfComponent, nodeDefinition, wfState, nodeState);
case HUMAN_FEEDBACK -> wfNode = new HumanFeedbackNode(wfComponent, nodeDefinition, wfState, nodeState);
default -> { default -> {
} }
} }

View File

@@ -9,6 +9,7 @@ import org.ruoyi.workflow.entity.User;
import org.ruoyi.workflow.entity.WorkflowNode; import org.ruoyi.workflow.entity.WorkflowNode;
import org.ruoyi.workflow.workflow.data.NodeIOData; import org.ruoyi.workflow.workflow.data.NodeIOData;
import org.ruoyi.workflow.workflow.node.AbstractWfNode; import org.ruoyi.workflow.workflow.node.AbstractWfNode;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.*; import java.util.*;
@@ -24,6 +25,9 @@ public class WfState {
private String uuid; private String uuid;
private User user; private User user;
private String processingNodeUuid; private String processingNodeUuid;
private Long userId;
private String tokenValue;
private SseEmitter sseEmitter;
//Source node uuid => target node uuid list //Source node uuid => target node uuid list
private Map<String, List<String>> edges = new HashMap<>(); private Map<String, List<String>> edges = new HashMap<>();
@@ -55,10 +59,13 @@ public class WfState {
*/ */
private Set<String> interruptNodes = new HashSet<>(); private Set<String> interruptNodes = new HashSet<>();
public WfState(User user, List<NodeIOData> input, String uuid) { public WfState(User user, List<NodeIOData> input, String uuid, Long userId, String tokenValue, SseEmitter sseEmitter) {
this.input = input; this.input = input;
this.user = user; this.user = user;
this.uuid = uuid; this.uuid = uuid;
this.userId = userId;
this.tokenValue = tokenValue;
this.sseEmitter = sseEmitter;
} }
/** /**

View File

@@ -68,7 +68,7 @@ public class WorkflowEngine {
this.workflowRuntimeNodeService = workflowRuntimeNodeService; this.workflowRuntimeNodeService = workflowRuntimeNodeService;
} }
public void run(User user, List<ObjectNode> userInputs, SseEmitter sseEmitter) { public void run(User user, List<ObjectNode> userInputs, SseEmitter sseEmitter, Long userId, String tokenValue) {
this.user = user; this.user = user;
this.sseEmitter = sseEmitter; this.sseEmitter = sseEmitter;
log.info("WorkflowEngine run,userId:{},workflowUuid:{},userInputs:{}", user.getId(), workflow.getUuid(), userInputs); log.info("WorkflowEngine run,userId:{},workflowUuid:{},userInputs:{}", user.getId(), workflow.getUuid(), userInputs);
@@ -86,7 +86,7 @@ public class WorkflowEngine {
Pair<WorkflowNode, Set<WorkflowNode>> startAndEnds = findStartAndEndNode(); Pair<WorkflowNode, Set<WorkflowNode>> startAndEnds = findStartAndEndNode();
WorkflowNode startNode = startAndEnds.getLeft(); WorkflowNode startNode = startAndEnds.getLeft();
List<NodeIOData> wfInputs = getAndCheckUserInput(userInputs, startNode); List<NodeIOData> 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); workflowRuntimeService.updateInput(this.wfRuntimeResp.getId(), wfState);

View File

@@ -1,9 +1,12 @@
package org.ruoyi.workflow.workflow; package org.ruoyi.workflow.workflow;
import cn.dev33.satoken.stp.StpUtil;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.core.exception.base.BaseException; 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.entity.*;
import org.ruoyi.workflow.helper.SSEEmitterHelper; import org.ruoyi.workflow.helper.SSEEmitterHelper;
import org.ruoyi.workflow.service.*; import org.ruoyi.workflow.service.*;
@@ -46,9 +49,17 @@ public class WorkflowStarter {
@Resource @Resource
private SSEEmitterHelper sseEmitterHelper; private SSEEmitterHelper sseEmitterHelper;
@Resource
private SseEmitterManager sseEmitterManager;
public SseEmitter streaming(User user, String workflowUuid, List<ObjectNode> userInputs) { public SseEmitter streaming(User user, String workflowUuid, List<ObjectNode> 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)) { if (!sseEmitterHelper.checkOrComplete(user, sseEmitter)) {
return sseEmitter; return sseEmitter;
} }
@@ -60,12 +71,12 @@ public class WorkflowStarter {
sseEmitterHelper.sendErrorAndComplete(user.getId(), sseEmitter, A_WF_DISABLED.getInfo()); sseEmitterHelper.sendErrorAndComplete(user.getId(), sseEmitter, A_WF_DISABLED.getInfo());
return sseEmitter; return sseEmitter;
} }
self.asyncRun(user, workflow, userInputs, sseEmitter); self.asyncRun(user, workflow, userInputs, sseEmitter, userId, tokenValue);
return sseEmitter; return sseEmitter;
} }
@Async @Async
public void asyncRun(User user, Workflow workflow, List<ObjectNode> userInputs, SseEmitter sseEmitter) { public void asyncRun(User user, Workflow workflow, List<ObjectNode> userInputs, SseEmitter sseEmitter, Long userId, String tokenValue) {
log.info("WorkflowEngine run,userId:{},workflowUuid:{},userInputs:{}", user.getId(), workflow.getUuid(), userInputs); log.info("WorkflowEngine run,userId:{},workflowUuid:{},userInputs:{}", user.getId(), workflow.getUuid(), userInputs);
List<WorkflowComponent> components = workflowComponentService.getAllEnable(); List<WorkflowComponent> components = workflowComponentService.getAllEnable();
List<WorkflowNode> nodes = workflowNodeService.lambdaQuery() List<WorkflowNode> nodes = workflowNodeService.lambdaQuery()
@@ -79,7 +90,7 @@ public class WorkflowStarter {
WorkflowEngine workflowEngine = new WorkflowEngine(workflow, WorkflowEngine workflowEngine = new WorkflowEngine(workflow,
sseEmitterHelper, components, nodes, edges, sseEmitterHelper, components, nodes, edges,
workflowRuntimeService, workflowRuntimeNodeService); workflowRuntimeService, workflowRuntimeNodeService);
workflowEngine.run(user, userInputs, sseEmitter); workflowEngine.run(user, userInputs, sseEmitter, userId, tokenValue);
} }
@Async @Async

View File

@@ -3,10 +3,19 @@ package org.ruoyi.workflow.workflow;
import cn.hutool.core.collection.CollStreamUtil; import cn.hutool.core.collection.CollStreamUtil;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.bsc.langgraph4j.langchain4j.generators.StreamingChatGenerator; import org.bsc.langgraph4j.langchain4j.generators.StreamingChatGenerator;
import org.bsc.langgraph4j.state.AgentState; import org.bsc.langgraph4j.state.AgentState;
import org.ruoyi.common.chat.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.base.NodeInputConfigTypeHandler;
import org.ruoyi.workflow.entity.WorkflowNode; import org.ruoyi.workflow.entity.WorkflowNode;
import org.ruoyi.workflow.enums.WfIODataTypeEnum; 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.data.NodeIODataContent;
import org.ruoyi.workflow.workflow.def.WfNodeParamRef; import org.ruoyi.workflow.workflow.def.WfNodeParamRef;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.ArrayList; import java.util.*;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.DEFAULT_OUTPUT_PARAM_NAME; 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 @Component
public class WorkflowUtil { public class WorkflowUtil {
@Resource
private ChatServiceFactory chatServiceFactory;
@Resource
private IChatModelService chatModelService;
public static String renderTemplate(String template, List<NodeIOData> values) { public static String renderTemplate(String template, List<NodeIOData> values) {
// 🔒 关键修复:如果 template 为 null直接返回 null 或空字符串 // 🔒 关键修复:如果 template 为 null直接返回 null 或空字符串
if (template == null) { if (template == null) {
@@ -74,8 +87,8 @@ public class WorkflowUtil {
public static String getHumanFeedbackTip(String nodeUuid, List<WorkflowNode> wfNodes) { public static String getHumanFeedbackTip(String nodeUuid, List<WorkflowNode> wfNodes) {
WorkflowNode wfNode = wfNodes.stream() WorkflowNode wfNode = wfNodes.stream()
.filter(item -> item.getUuid().equals(nodeUuid)) .filter(item -> item.getUuid().equals(nodeUuid))
.findFirst().orElse(null); .findFirst().orElse(null);
if (null == wfNode) { if (null == wfNode) {
return ""; return "";
} }
@@ -88,73 +101,82 @@ public class WorkflowUtil {
return String.valueOf(tip); return String.valueOf(tip);
} }
public void streamingInvokeLLM(WfState wfState, WfNodeState state, WorkflowNode node, String category, public void streamingInvokeLLM(WfState wfState, WfNodeState state, WorkflowNode node, String modelName,
String modelName, List<UserMessage> systemMessage) { List<SystemMessage> systemMessage) {
log.info("stream invoke, category: {}, modelName: {}", category, modelName); log.info("stream invoke, modelName: {}", modelName);
// 根据模型名称查询模型信息
ChatModelVo chatModelVo = chatModelService.selectModelByName(modelName);
if (chatModelVo == null) {
throw new IllegalArgumentException("模型不存在: " + modelName);
}
// 根据模型名称找到模型实体
String modelVoCategory = chatModelVo.getCategory();
// 根据 category 获取对应的 ChatService不使用计费代理工作流场景单独计费 // 根据 category 获取对应的 ChatService不使用计费代理工作流场景单独计费
//IChatService chatService = chatServiceFactory.getOriginalService(category); IChatService chatService = chatServiceFactory.getOriginalService(modelVoCategory);
StreamingChatGenerator<AgentState> streamingGenerator = StreamingChatGenerator.builder() StreamingChatGenerator<AgentState> streamingGenerator = StreamingChatGenerator.builder()
.mapResult(response -> { .mapResult(response -> {
String responseTxt = response.aiMessage().text(); String responseTxt = response.aiMessage().text();
log.info("llm response:{}", responseTxt); log.info("llm response:{}", responseTxt);
// 传递所有输入数据 + 添加 LLM 输出 // 传递所有输入数据 + 添加 LLM 输出
wfState.getNodeStateByNodeUuid(node.getUuid()).ifPresent(item -> { wfState.getNodeStateByNodeUuid(node.getUuid()).ifPresent(item -> {
List<NodeIOData> outputs = new ArrayList<>(item.getInputs()); List<NodeIOData> outputs = new ArrayList<>(item.getInputs());
NodeIOData output = NodeIOData.createByText(DEFAULT_OUTPUT_PARAM_NAME, "", responseTxt); NodeIOData output = NodeIOData.createByText(DEFAULT_OUTPUT_PARAM_NAME, "", responseTxt);
outputs.add(output); outputs.add(output);
item.setOutputs(outputs); item.setOutputs(outputs);
}); });
return Map.of("completeResult", response.aiMessage().text()); return Map.of("completeResult", response.aiMessage().text());
}) })
.startingNode(node.getUuid()) .startingNode(node.getUuid())
.startingState(state) .startingState(state)
.build(); .build();
Long userId = wfState.getUserId();
String tokenValue = wfState.getTokenValue();
SseEmitter sseEmitter = wfState.getSseEmitter();
// 构建 ruoyi-ai 的 ChatRequest // 构建 ruoyi-ai 的 ChatRequest
// List<Message> messages = new ArrayList<>(); List<ChatMessage> chatMessages = new ArrayList<>();
// addUserMessage(node, state.getInputs(), chatMessages);
// addUserMessage(node, state.getInputs(), messages); chatMessages.addAll(systemMessage);
//
// addSystemMessage(systemMessage, messages); // 定义模型调用对象
// ChatRequest chatRequest = new ChatRequest();
// ChatRequest chatRequest = new ChatRequest(); // 目前工作流深度思考成员变量只能写死
// chatRequest.setModel(modelName); chatRequest.setEnableThinking(false);
// chatRequest.setMessages(messages); chatRequest.setModel(modelName);
chatRequest.setChatMessages(chatMessages);
// 使用工作流专用方法 // 使用工作流专用方法
StreamingChatResponseHandler handler = streamingGenerator.handler();
chatService.chat(chatModelVo, chatRequest, sseEmitter, userId, tokenValue, handler);
wfState.getNodeToStreamingGenerator().put(node.getUuid(), streamingGenerator); wfState.getNodeToStreamingGenerator().put(node.getUuid(), streamingGenerator);
} }
/** /**
* 添加用户信息 * 添加用户信息
* *
* @param node * @param node 节点
* @param messages * @param userMessage 用户信息
*/ */
private void addUserMessage(WorkflowNode node, List<NodeIOData> userMessage, List<UserMessage> messages) { private void addUserMessage(WorkflowNode node, List<NodeIOData> userMessage, List<ChatMessage> messages) {
if (CollUtil.isEmpty(userMessage)) { if (CollUtil.isEmpty(userMessage)) {
return; return;
} }
WfNodeInputConfig nodeInputConfig = NodeInputConfigTypeHandler.fillNodeInputConfig(node.getInputConfig()); WfNodeInputConfig nodeInputConfig = NodeInputConfigTypeHandler.fillNodeInputConfig(node.getInputConfig());
List<WfNodeParamRef> refInputs = nodeInputConfig.getRefInputs(); List<WfNodeParamRef> refInputs = nodeInputConfig.getRefInputs();
Set<String> nameSet = CollStreamUtil.toSet(refInputs, WfNodeParamRef::getName); Set<String> nameSet = CollStreamUtil.toSet(refInputs, WfNodeParamRef::getName);
// 构建消息列表
userMessage.stream().filter(item -> nameSet.contains(item.getName())) List<UserMessage> messageList = buildMessageList(userMessage, nameSet);
.map(item -> getMessage("user", item.getContent().getValue().toString())).forEach(messages::add); // 如果没有找到匹配的消息尝试使用input字段
if (CollUtil.isEmpty(messageList)) {
if (CollUtil.isNotEmpty(messages)) { messageList = buildMessageList(userMessage, Set.of("input"));
return;
} }
messages.addAll(messageList);
userMessage.stream().filter(item -> "input".equals(item.getName()))
.map(item -> getMessage("user", item.getContent().getValue().toString())).forEach(messages::add);
} }
/** /**
@@ -170,19 +192,13 @@ public class WorkflowUtil {
} }
/** /**
* 添加系统信息 * 构建消息列表
*
* @param systemMessage
* @param messages
*/ */
private void addSystemMessage(List<UserMessage> systemMessage, List<UserMessage> messages) { private List<UserMessage> buildMessageList(List<NodeIOData> userMessage, Set<String> nameSet) {
log.info("addSystemMessage received: {}", systemMessage); // 🔥 加这一行 return userMessage.stream()
.filter(item -> item != null && item.getName() != null)
if (CollUtil.isEmpty(systemMessage)) { // 兼容默认输出参数的人机交互
return; .filter(item -> nameSet.contains(item.getName()))
} .map(item -> getMessage("user", item.getContent().getValue().toString())).toList();
systemMessage.stream()
.map(userMsg -> getMessage("system", userMsg.singleText()))
.forEach(messages::add);
} }
} }

View File

@@ -124,14 +124,6 @@ public abstract class AbstractWfNode {
log.info("↓↓↓↓↓ node process start,name:{},uuid:{}", node.getTitle(), node.getUuid()); log.info("↓↓↓↓↓ node process start,name:{},uuid:{}", node.getTitle(), node.getUuid());
state.setProcessStatus(NODE_PROCESS_STATUS_DOING); state.setProcessStatus(NODE_PROCESS_STATUS_DOING);
initInput(); 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) { if (null != inputConsumer) {
inputConsumer.accept(state); inputConsumer.accept(state);
} }

View File

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

View File

@@ -1,6 +1,6 @@
package org.ruoyi.workflow.workflow.node.answer; 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 lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.ruoyi.workflow.entity.WorkflowComponent; import org.ruoyi.workflow.entity.WorkflowComponent;
@@ -44,9 +44,9 @@ public class LLMAnswerNode extends AbstractWfNode {
// 调用LLM // 调用LLM
WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class); WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class);
String modelName = nodeConfigObj.getModelName(); String modelName = nodeConfigObj.getModelName();
String category = nodeConfigObj.getCategory(); // 转换系统信息结构
List<UserMessage> systemMessage = List.of(UserMessage.from(prompt)); List<SystemMessage> systemMessage = List.of(new SystemMessage(prompt));
workflowUtil.streamingInvokeLLM(wfState, state, node, category, modelName, systemMessage); workflowUtil.streamingInvokeLLM(wfState, state, node, modelName, systemMessage);
return new NodeProcessResult(); return new NodeProcessResult();
} }
} }

View File

@@ -1,5 +1,6 @@
package org.ruoyi.workflow.workflow.node.keywordExtractor; package org.ruoyi.workflow.workflow.node.keywordExtractor;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.data.message.UserMessage;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -65,12 +66,9 @@ public class KeywordExtractorNode extends AbstractWfNode {
// 调用 LLM 进行关键词提取 // 调用 LLM 进行关键词提取
WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class); WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class);
String modelName = config.getModelName(); String modelName = config.getModelName();
String category = config.getCategory(); List<SystemMessage> systemMessage = List.of(new SystemMessage(prompt));
List<UserMessage> systemMessage = List.of(UserMessage.from(prompt));
// 使用流式调用 // 使用流式调用
workflowUtil.streamingInvokeLLM(wfState, state, node, category, modelName, systemMessage); workflowUtil.streamingInvokeLLM(wfState, state, node, modelName, systemMessage);
return new NodeProcessResult(); return new NodeProcessResult();
} }

View File

@@ -1,5 +1,6 @@
package org.ruoyi.workflow.workflow.node.knowledgeRetrieval; package org.ruoyi.workflow.workflow.node.knowledgeRetrieval;
import dev.langchain4j.data.message.SystemMessage;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.ruoyi.workflow.entity.WorkflowComponent; import org.ruoyi.workflow.entity.WorkflowComponent;
@@ -150,18 +151,15 @@ public class KnowledgeRetrievalNode extends AbstractWfNode {
// 使用WorkflowUtil调用LLM流式 // 使用WorkflowUtil调用LLM流式
WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class); WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class);
List<dev.langchain4j.data.message.UserMessage> systemMessage = List<SystemMessage> systemMessage = List.of(new SystemMessage(prompt));
List.of(dev.langchain4j.data.message.UserMessage.from(prompt));
// 调用流式LLM // 调用流式LLM
String category = StringUtils.isNotBlank(config.getCategory()) ? config.getCategory() : "llm";
String modelName = StringUtils.isNotBlank(config.getModelName()) ? config.getModelName() : "deepseek-chat"; String modelName = StringUtils.isNotBlank(config.getModelName()) ? config.getModelName() : "deepseek-chat";
workflowUtil.streamingInvokeLLM( workflowUtil.streamingInvokeLLM(
wfState, wfState,
tempState, tempState,
tempNode, tempNode,
category,
modelName, modelName,
systemMessage systemMessage
); );

View File

@@ -1,9 +1,15 @@
package org.ruoyi.workflow.workflow.node.switcher; 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 lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.ruoyi.common.core.utils.SpringUtils;
import org.ruoyi.workflow.entity.WorkflowComponent; import org.ruoyi.workflow.entity.WorkflowComponent;
import org.ruoyi.workflow.entity.WorkflowNode; import org.ruoyi.workflow.entity.WorkflowNode;
import org.ruoyi.workflow.service.WorkflowNodeService;
import org.ruoyi.workflow.workflow.NodeProcessResult; import org.ruoyi.workflow.workflow.NodeProcessResult;
import org.ruoyi.workflow.workflow.WfNodeState; import org.ruoyi.workflow.workflow.WfNodeState;
import org.ruoyi.workflow.workflow.WfState; import org.ruoyi.workflow.workflow.WfState;
@@ -12,6 +18,8 @@ import org.ruoyi.workflow.workflow.node.AbstractWfNode;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; 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); super(wfComponent, nodeDef, wfState, nodeState);
} }
private static final WorkflowNodeService workflowNodeService = SpringUtils.getBean(WorkflowNodeService.class);
@Override @Override
public NodeProcessResult onProcess() { public NodeProcessResult onProcess() {
try { try {
@@ -261,6 +271,9 @@ public class SwitcherNode extends AbstractWfNode {
} }
} }
// 根据UUID查询对应节点是否存在Param(替换成Input)
result = findParamValueInNode(nodeUuid, paramName, inputs, result);
if (result != null) { if (result != null) {
log.info("在节点 '{}' 的输出中找到参数 '{}': '{}'", nodeUuid, paramName, result); log.info("在节点 '{}' 的输出中找到参数 '{}': '{}'", nodeUuid, paramName, result);
return result; return result;
@@ -282,6 +295,47 @@ public class SwitcherNode extends AbstractWfNode {
return null; return null;
} }
/**
* 根据节点UUID和参数名查找对应的输入值
* 意义:修复开始节点参数名错误的问题
*
* @param nodeUuid 节点的唯一标识符UUID
* @param paramName 需要查找的参数名称
* @param inputs 输入数据列表,用于匹配参数值
* @param result 默认返回结果,若未找到匹配项则返回该值
* @return 返回查找到的参数值,若未找到则返回默认结果
*/
private String findParamValueInNode(String nodeUuid, String paramName, List<NodeIOData> 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<String> 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;
}
/** /**
* 根据运算符评估条件 * 根据运算符评估条件
*/ */

View File

@@ -16,17 +16,7 @@
<dependency> <dependency>
<groupId>org.ruoyi</groupId> <groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-core</artifactId> <artifactId>ruoyi-common-chat</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-excel</artifactId>
</dependency> </dependency>
<dependency> <dependency>
@@ -34,17 +24,6 @@
<artifactId>ruoyi-common-sensitive</artifactId> <artifactId>ruoyi-common-sensitive</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-tenant</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<dependency> <dependency>
<groupId>dev.langchain4j</groupId> <groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId> <artifactId>langchain4j-open-ai</artifactId>
@@ -139,23 +118,11 @@
<artifactId>ruoyi-common-doc</artifactId> <artifactId>ruoyi-common-doc</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-sse</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.neo4j.driver</groupId> <groupId>org.neo4j.driver</groupId>
<artifactId>neo4j-java-driver</artifactId> <artifactId>neo4j-java-driver</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency> <dependency>
<groupId>io.github.imfangs</groupId> <groupId>io.github.imfangs</groupId>
<artifactId>dify-java-client</artifactId> <artifactId>dify-java-client</artifactId>

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; 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.ruoyi.service.chat.impl.ChatServiceFacade;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;

View File

@@ -6,8 +6,10 @@ import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*; import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission; 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.enums.ModelType;
import org.ruoyi.service.chat.IChatModelService;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.ruoyi.common.idempotent.annotation.RepeatSubmit; import org.ruoyi.common.idempotent.annotation.RepeatSubmit;
@@ -19,8 +21,6 @@ import org.ruoyi.common.core.validate.AddGroup;
import org.ruoyi.common.core.validate.EditGroup; import org.ruoyi.common.core.validate.EditGroup;
import org.ruoyi.common.log.enums.BusinessType; import org.ruoyi.common.log.enums.BusinessType;
import org.ruoyi.common.excel.utils.ExcelUtil; 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; import org.ruoyi.common.mybatis.core.page.TableDataInfo;
/** /**

View File

@@ -2,8 +2,8 @@ package org.ruoyi.factory;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.ruoyi.domain.vo.chat.ChatModelVo; import org.ruoyi.common.chat.Service.IChatModelService;
import org.ruoyi.service.chat.IChatModelService; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.service.embed.BaseEmbedModelService; import org.ruoyi.service.embed.BaseEmbedModelService;
import org.ruoyi.service.embed.MultiModalEmbedModelService; import org.ruoyi.service.embed.MultiModalEmbedModelService;
import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoSuchBeanDefinitionException;

View File

@@ -1,7 +1,7 @@
package org.ruoyi.mapper.chat; package org.ruoyi.mapper.chat;
import org.ruoyi.domain.entity.chat.ChatModel; import org.ruoyi.common.chat.domain.entity.chat.ChatModel;
import org.ruoyi.domain.vo.chat.ChatModelVo; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.common.mybatis.core.mapper.BaseMapperPlus; import org.ruoyi.common.mybatis.core.mapper.BaseMapperPlus;
/** /**

View File

@@ -1,9 +1,9 @@
package org.ruoyi.service.chat; 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.PageQuery;
import org.ruoyi.common.mybatis.core.page.TableDataInfo; import org.ruoyi.common.mybatis.core.page.TableDataInfo;
import org.ruoyi.domain.bo.chat.ChatMessageBo; import org.ruoyi.domain.bo.chat.ChatMessageBo;
import org.ruoyi.domain.dto.ChatMessageDTO;
import org.ruoyi.domain.vo.chat.ChatMessageVo; import org.ruoyi.domain.vo.chat.ChatMessageVo;
import java.util.Collection; import java.util.Collection;

View File

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

View File

@@ -4,6 +4,7 @@ import dev.langchain4j.agentic.AgenticServices;
import dev.langchain4j.agentic.supervisor.SupervisorAgent; import dev.langchain4j.agentic.supervisor.SupervisorAgent;
import dev.langchain4j.agentic.supervisor.SupervisorResponseStrategy; import dev.langchain4j.agentic.supervisor.SupervisorResponseStrategy;
import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.mcp.McpToolProvider; import dev.langchain4j.mcp.McpToolProvider;
import dev.langchain4j.mcp.client.DefaultMcpClient; import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpClient; 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.ExecuteSqlQueryTool;
import org.ruoyi.agent.tool.QueryAllTablesTool; import org.ruoyi.agent.tool.QueryAllTablesTool;
import org.ruoyi.agent.tool.QueryTableSchemaTool; 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.SpringUtils;
import org.ruoyi.common.core.utils.StringUtils;
import org.ruoyi.common.sse.utils.SseMessageUtils; import org.ruoyi.common.sse.utils.SseMessageUtils;
import org.ruoyi.domain.bo.chat.ChatMessageBo; 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.enums.RoleType;
import org.ruoyi.service.chat.IChatMessageService; import org.ruoyi.service.chat.IChatMessageService;
import org.ruoyi.service.chat.IChatService;
import org.ruoyi.service.chat.impl.memory.PersistentChatMemoryStore; import org.ruoyi.service.chat.impl.memory.PersistentChatMemoryStore;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.ArrayList; import java.util.*;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/** /**
@@ -76,23 +78,50 @@ public abstract class AbstractStreamingChatService implements IChatService {
*/ */
@Override @Override
public SseEmitter chat(ChatModelVo chatModelVo, ChatRequest chatRequest, SseEmitter emitter, Long userId, String tokenValue) { 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 { 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); saveChatMessage(chatRequest, userId, content, RoleType.USER.getName(), chatModelVo);
// 使用长期记忆增强的消息列表 // 使用长期记忆增强的消息列表
List<ChatMessage> messagesWithMemory = buildMessagesWithMemory(chatRequest); List<ChatMessage> messagesWithMemory = buildMessagesWithMemory(chatRequest);
if(chatRequest.getEnableThinking()){ if (chatRequest.getEnableThinking()) {
String msg = doAgent(content,chatModelVo); String msg = doAgent(content, chatModelVo);
SseMessageUtils.sendMessage(userId, msg); SseMessageUtils.sendMessage(userId, msg);
SseMessageUtils.completeConnection(userId, tokenValue); SseMessageUtils.completeConnection(userId, tokenValue);
// 保存助手回复消息 // 保存助手回复消息
saveChatMessage(chatRequest, userId, msg, RoleType.ASSISTANT.getName(), chatModelVo); 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); doChat(chatModelVo, chatRequest, messagesWithMemory, handler);
} }
@@ -115,8 +144,6 @@ public abstract class AbstractStreamingChatService implements IChatService {
*/ */
protected List<ChatMessage> buildMessagesWithMemory(ChatRequest chatRequest) { protected List<ChatMessage> buildMessagesWithMemory(ChatRequest chatRequest) {
List<ChatMessage> messages = new ArrayList<>(); List<ChatMessage> messages = new ArrayList<>();
// 加载历史消息
if (enablePersistentMemory && chatRequest.getSessionId() != null) { if (enablePersistentMemory && chatRequest.getSessionId() != null) {
MessageWindowChatMemory memory = createChatMemory(chatRequest.getSessionId()); MessageWindowChatMemory memory = createChatMemory(chatRequest.getSessionId());
if (memory != null) { if (memory != null) {
@@ -126,8 +153,13 @@ public abstract class AbstractStreamingChatService implements IChatService {
log.debug("已加载 {} 条历史消息用于会话 {}", historicalMessages.size(), chatRequest.getSessionId()); log.debug("已加载 {} 条历史消息用于会话 {}", historicalMessages.size(), chatRequest.getSessionId());
} }
} }
return messages;
}
// 工作流方式
List<ChatMessage> chatMessages = chatRequest.getChatMessages();
if (!CollectionUtils.isEmpty(chatMessages)){
messages.addAll(chatMessages);
} }
return messages; return messages;
} }
@@ -172,16 +204,16 @@ public abstract class AbstractStreamingChatService implements IChatService {
* *
* @param chatModelVo 模型配置 * @param chatModelVo 模型配置
* @param chatRequest 聊天请求 * @param chatRequest 聊天请求
* @param handler 响应处理器 * @param handler 响应处理器
*/ */
protected abstract void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List<ChatMessage> messagesWithMemory,StreamingChatResponseHandler handler); protected abstract void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List<ChatMessage> messagesWithMemory, StreamingChatResponseHandler handler);
/** /**
* 创建标准的响应处理器 * 创建标准的响应处理器
* *
* @param chatRequest 聊天请求包含sessionId等上下文信息 * @param chatRequest 聊天请求包含sessionId等上下文信息
* @param userId 用户ID * @param userId 用户ID
* @param tokenValue 会话令牌 * @param tokenValue 会话令牌
* @param chatModelVo 模型配置 * @param chatModelVo 模型配置
* @return 标准的流式响应处理器 * @return 标准的流式响应处理器
*/ */
@@ -248,9 +280,9 @@ public abstract class AbstractStreamingChatService implements IChatService {
* 保存聊天消息到数据库 * 保存聊天消息到数据库
* *
* @param chatRequest 聊天请求 * @param chatRequest 聊天请求
* @param userId 用户ID * @param userId 用户ID
* @param content 消息内容 * @param content 消息内容
* @param role 消息角色 * @param role 消息角色
* @param chatModelVo 模型配置 * @param chatModelVo 模型配置
*/ */
private void saveChatMessage(ChatRequest chatRequest, Long userId, String content, String role, ChatModelVo 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(); public abstract String getProviderName();
protected String doAgent(String userMessage,ChatModelVo chatModelVo) { protected String doAgent(String userMessage, ChatModelVo chatModelVo) {
// 步骤1: 配置MCP传输层 - 连接到bing-cn-mcp服务器 // 步骤1: 配置MCP传输层 - 连接到bing-cn-mcp服务器
// 该服务提供两个工具: bing_search (必应搜索) 和 crawl_webpage (网页抓取) // 该服务提供两个工具: bing_search (必应搜索) 和 crawl_webpage (网页抓取)
McpTransport transport = new StdioMcpTransport.Builder() McpTransport transport = new StdioMcpTransport.Builder()
@@ -357,7 +389,7 @@ public abstract class AbstractStreamingChatService implements IChatService {
SupervisorAgent supervisor = AgenticServices SupervisorAgent supervisor = AgenticServices
.supervisorBuilder() .supervisorBuilder()
.chatModel(PLANNER_MODEL) .chatModel(PLANNER_MODEL)
.subAgents(sqlAgent,chartGenerationAgent) .subAgents(sqlAgent, chartGenerationAgent)
.responseStrategy(SupervisorResponseStrategy.LAST) .responseStrategy(SupervisorResponseStrategy.LAST)
.build(); .build();

View File

@@ -1,5 +1,6 @@
package org.ruoyi.service.chat.impl; 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.MapstructUtils;
import org.ruoyi.common.core.utils.StringUtils; import org.ruoyi.common.core.utils.StringUtils;
import org.ruoyi.common.mybatis.core.page.TableDataInfo; import org.ruoyi.common.mybatis.core.page.TableDataInfo;
@@ -9,7 +10,6 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.ruoyi.domain.dto.ChatMessageDTO;
import org.ruoyi.service.chat.IChatMessageService; import org.ruoyi.service.chat.IChatMessageService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.ruoyi.domain.bo.chat.ChatMessageBo; import org.ruoyi.domain.bo.chat.ChatMessageBo;
@@ -146,7 +146,7 @@ public class ChatMessageServiceImpl implements IChatMessageService {
* @return 消息DTO列表 * @return 消息DTO列表
*/ */
@Override @Override
public List<org.ruoyi.domain.dto.ChatMessageDTO> getMessagesBySessionId(Long sessionId) { public List<ChatMessageDTO> getMessagesBySessionId(Long sessionId) {
if (sessionId == null) { if (sessionId == null) {
return new java.util.ArrayList<>(); return new java.util.ArrayList<>();
} }

View File

@@ -1,5 +1,9 @@
package org.ruoyi.service.chat.impl; 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.MapstructUtils;
import org.ruoyi.common.core.utils.StringUtils; import org.ruoyi.common.core.utils.StringUtils;
import org.ruoyi.common.mybatis.core.page.TableDataInfo; import org.ruoyi.common.mybatis.core.page.TableDataInfo;
@@ -9,11 +13,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.ruoyi.service.chat.IChatModelService;
import org.springframework.stereotype.Service; 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 org.ruoyi.mapper.chat.ChatModelMapper;
import java.util.List; import java.util.List;

View File

@@ -4,16 +4,17 @@ import cn.dev33.satoken.stp.StpUtil;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; 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.satoken.utils.LoginHelper;
import org.ruoyi.common.sse.core.SseEmitterManager; import org.ruoyi.common.sse.core.SseEmitterManager;
import org.ruoyi.domain.bo.vector.QueryVectorBo; 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.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.knowledge.IKnowledgeInfoService;
import org.ruoyi.service.vector.VectorStoreService; import org.ruoyi.service.vector.VectorStoreService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;

View File

@@ -1,17 +1,7 @@
package org.ruoyi.service.chat.impl.memory; 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 lombok.extern.slf4j.Slf4j;
import org.ruoyi.domain.dto.ChatMessageDTO; import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.domain.dto.request.ChatRequest;
import org.ruoyi.domain.vo.chat.ChatModelVo;
import java.util.ArrayList;
import java.util.List;
/** /**
* 长期记忆使用示例 * 长期记忆使用示例

View File

@@ -3,8 +3,8 @@ package org.ruoyi.service.chat.impl.memory;
import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.store.memory.chat.ChatMemoryStore; import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.chat.domain.dto.ChatMessageDTO;
import org.ruoyi.common.core.utils.SpringUtils; import org.ruoyi.common.core.utils.SpringUtils;
import org.ruoyi.domain.dto.ChatMessageDTO;
import org.ruoyi.service.chat.IChatMessageService; import org.ruoyi.service.chat.IChatMessageService;
import java.util.ArrayList; import java.util.ArrayList;

View File

@@ -5,11 +5,11 @@ import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import dev.langchain4j.model.ollama.OllamaStreamingChatModel; import dev.langchain4j.model.ollama.OllamaStreamingChatModel;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.ruoyi.domain.dto.request.ChatRequest;
import org.ruoyi.domain.vo.chat.ChatModelVo;
import org.ruoyi.enums.ChatModeType; import org.ruoyi.enums.ChatModeType;
import org.ruoyi.service.chat.impl.AbstractStreamingChatService; import org.ruoyi.service.chat.impl.AbstractStreamingChatService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import java.util.List; import java.util.List;

View File

@@ -1,28 +1,13 @@
package org.ruoyi.service.chat.impl.provider; 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.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.StreamingChatModel;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.openai.OpenAiStreamingChatModel; import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
import dev.langchain4j.service.tool.ToolProvider;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.ruoyi.agent.ChartGenerationAgent; import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.agent.SqlAgent; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.agent.WebSearchAgent;
import org.ruoyi.agent.tool.ExecuteSqlQueryTool;
import org.ruoyi.agent.tool.QueryAllTablesTool;
import org.ruoyi.agent.tool.QueryTableSchemaTool;
import org.ruoyi.domain.dto.request.ChatRequest;
import org.ruoyi.domain.vo.chat.ChatModelVo;
import org.ruoyi.enums.ChatModeType; import org.ruoyi.enums.ChatModeType;
import org.ruoyi.service.chat.impl.AbstractStreamingChatService; import org.ruoyi.service.chat.impl.AbstractStreamingChatService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -57,8 +42,6 @@ public class OpenAIServiceImpl extends AbstractStreamingChatService {
} }
@Override @Override
public String getProviderName() { public String getProviderName() {
return ChatModeType.OPEN_AI.getCode(); return ChatModeType.OPEN_AI.getCode();

View File

@@ -1,16 +1,34 @@
package org.ruoyi.service.chat.impl.provider; 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.community.model.dashscope.QwenStreamingChatModel;
import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.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.StreamingChatModel;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import dev.langchain4j.service.tool.ToolProvider;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.ruoyi.agent.McpAgent;
import org.ruoyi.config.McpSseConfig;
import org.ruoyi.enums.ChatModeType; 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.ruoyi.service.chat.impl.AbstractStreamingChatService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; 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; import java.util.List;
/** /**
@@ -23,6 +41,17 @@ import java.util.List;
@Slf4j @Slf4j
public class QianWenChatServiceImpl extends AbstractStreamingChatService { 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 @Override
protected StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo,ChatRequest chatRequest) { protected StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo,ChatRequest chatRequest) {
return QwenStreamingChatModel.builder() return QwenStreamingChatModel.builder()
@@ -35,7 +64,98 @@ public class QianWenChatServiceImpl extends AbstractStreamingChatService {
protected void doChat(ChatModelVo chatModelVo,ChatRequest chatRequest,List<ChatMessage> messagesWithMemory, protected void doChat(ChatModelVo chatModelVo,ChatRequest chatRequest,List<ChatMessage> messagesWithMemory,
StreamingChatResponseHandler handler) { StreamingChatResponseHandler handler) {
StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo,chatRequest); StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo,chatRequest);
streamingChatModel.chat(messagesWithMemory, handler); // 判断是否存在需要使用阿里千问的文档解析功能
List<ChatMessage> chatMessages = hasFileIdData(messagesWithMemory);
streamingChatModel.chat(chatMessages, handler);
}
/**
* 检查是否包含fileId数据
*/
private List<ChatMessage> hasFileIdData(List<ChatMessage> messagesWithMemory) {
if (CollectionUtils.isEmpty(messagesWithMemory)) {
return messagesWithMemory;
}
// 找到包含阿里上传文件前缀的用户信息
var foundUserMessage = messagesWithMemory.stream()
.filter(message -> message instanceof UserMessage)
.map(message -> (UserMessage) message)
.filter(userMessage ->
userMessage.singleText().toLowerCase().contains(UPLOAD_FILE_API_PREFIX.toLowerCase())
)
.findFirst();
// 找到原本SystemMessage
var systemMessage = messagesWithMemory.stream()
.filter(message -> message instanceof SystemMessage)
.map(message -> (SystemMessage) message)
.findFirst();
// 判断是否存在并重新构建信息体(符合千问文档解析格式)
return foundUserMessage.map(userMsg -> {
List<ChatMessage> messages = new ArrayList<>();
messages.add(new SystemMessage(userMsg.singleText()));
systemMessage.ifPresent(sysMsg -> messages.add(new UserMessage(sysMsg.text())));
return messages;
}).orElse(messagesWithMemory);
}
/**
* 调用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<ToolSpecification> 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 @Override

View File

@@ -1,7 +1,7 @@
package org.ruoyi.service.embed; package org.ruoyi.service.embed;
import dev.langchain4j.model.embedding.EmbeddingModel; 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; import org.ruoyi.enums.ModalityType;

View File

@@ -5,7 +5,7 @@ import dev.langchain4j.community.model.dashscope.QwenEmbeddingModel;
import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.output.Response; 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.springframework.stereotype.Component;
import org.ruoyi.enums.ModalityType; import org.ruoyi.enums.ModalityType;

View File

@@ -7,10 +7,10 @@ import dev.langchain4j.model.output.Response;
import dev.langchain4j.model.output.TokenUsage; import dev.langchain4j.model.output.TokenUsage;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import okhttp3.*; import okhttp3.*;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.domain.dto.MultiModalInput; import org.ruoyi.domain.dto.MultiModalInput;
import org.ruoyi.domain.dto.request.AliyunMultiModalEmbedRequest; import org.ruoyi.domain.dto.request.AliyunMultiModalEmbedRequest;
import org.ruoyi.domain.dto.response.AliyunMultiModalEmbedResponse; import org.ruoyi.domain.dto.response.AliyunMultiModalEmbedResponse;
import org.ruoyi.domain.vo.chat.ChatModelVo;
import org.ruoyi.enums.ModalityType; import org.ruoyi.enums.ModalityType;
import org.ruoyi.service.embed.MultiModalEmbedModelService; import org.ruoyi.service.embed.MultiModalEmbedModelService;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;

View File

@@ -4,7 +4,7 @@ import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.ollama.OllamaEmbeddingModel; import dev.langchain4j.model.ollama.OllamaEmbeddingModel;
import dev.langchain4j.model.output.Response; 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.enums.ModalityType;
import org.ruoyi.service.embed.BaseEmbedModelService; import org.ruoyi.service.embed.BaseEmbedModelService;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;

View File

@@ -4,7 +4,7 @@ import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.openai.OpenAiEmbeddingModel; import dev.langchain4j.model.openai.OpenAiEmbeddingModel;
import dev.langchain4j.model.output.Response; 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.enums.ModalityType;
import org.ruoyi.service.embed.BaseEmbedModelService; import org.ruoyi.service.embed.BaseEmbedModelService;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;

View File

@@ -1,7 +1,7 @@
package org.ruoyi.service.graph; package org.ruoyi.service.graph;
import org.ruoyi.domain.vo.chat.ChatModelVo; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
/** /**
* 图谱LLM服务接口 * 图谱LLM服务接口

Some files were not shown because too many files have changed in this diff Show More