8 Commits

Author SHA1 Message Date
ageerle
d4f8f91893 Merge pull request #259 from StevenJack666/v3.0.0
chat对话接口如参改为ChatContext,方便后续扩展
2026-02-15 15:54:21 +08:00
zhang
f25ebdf9ec 去掉多余字符 2026-02-14 22:07:09 +08:00
zhang
32b8144b56 Merge branch 'work_flow_v3.0' into v3.0.0
# Conflicts:
#	ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/Service/IChatService.java
#	ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowUtil.java
#	ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/AbstractStreamingChatService.java
#	ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatServiceFacade.java
#	ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/QianWenChatServiceImpl.java
2026-02-14 21:14:50 +08:00
zengxb
b40805cb57 context:公共聊天接口调整为对话上下文对象传输 2026-02-14 10:55:53 +08:00
steve
008f64617f Merge pull request #4 from StevenJack666/feature-v3.0.0
Feature v3.0.0
2026-02-14 10:49:52 +08:00
zhang
3539e222d2 init 2026-02-14 10:48:43 +08:00
zhang
c4d1ea974d init 2026-02-14 10:48:33 +08:00
zengxb
420e05ecf3 context:工作流与大模型聊天对话整合(新增Common-Chat公共对话接口) 2026-02-13 17:56:55 +08:00
9 changed files with 174 additions and 81 deletions

View File

@@ -16,7 +16,7 @@ public class RuoYiAIApplication {
SpringApplication application = new SpringApplication(RuoYiAIApplication.class);
application.setApplicationStartup(new BufferingApplicationStartup(2048));
application.run(args);
System.out.println("(♥◠‿◠)ノ゙ RuoYi-AI启动成功 ლ(´ڡ`ლ)゙");
System.out.println("(♥◠‿◠)ノ゙ RuoYi-AI启动成功 ლ(´ڡ`ლ)゙");
}
}

View File

@@ -11,6 +11,17 @@ spring.boot.admin.client:
username: @monitor.username@
password: @monitor.password@
--- # mcp配置信息
mcp:
sse:
enabled: false
url: http://localhost:8085/sse
--- # 上传文件地址
sys:
upload:
path: D:\\DownLoad
--- # snail-job 配置
snail-job:
enabled: false
@@ -47,9 +58,9 @@ spring:
driverClassName: com.mysql.cj.jdbc.Driver
# jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
url: jdbc:mysql://127.0.0.1:3306/ruoyi-ai-v3?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
username: ruoyi-ai-v3
password: ruoyi-ai-v3
url: jdbc:mysql://127.0.0.1:3306/mysql?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
username: root
password: 123456
hikari:
# 最大连接池数量
maxPoolSize: 20
@@ -87,7 +98,7 @@ spring.data:
# 数据库索引
database: 0
# redis 密码必须配置
password: 123456
# password: 123456
# 连接超时时间
timeout: 10s
# 是否开启ssl

View File

@@ -124,6 +124,7 @@ security:
- /*/api-docs
- /*/api-docs/**
- /warm-flow-ui/config
- /workflow/run
# 多租户配置
tenant:

View File

@@ -1,8 +1,7 @@
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 jakarta.validation.Valid;
import org.ruoyi.common.chat.domain.entity.chat.ChatContext;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
/**
@@ -13,12 +12,7 @@ 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);
SseEmitter chat(@Valid ChatContext chatContext);
/**
* 获取服务提供商名称

View File

@@ -0,0 +1,57 @@
package org.ruoyi.common.chat.domain.entity.chat;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
/**
* 聊天对话上下文对象
*
* @author zengxb
* @date 2026-02-14
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Builder
public class ChatContext {
/**
* 模型管理视图对象
*/
@NotNull(message = "模型管理视图对象不能为空")
private ChatModelVo chatModelVo;
/**
* 对话请求对象
*/
@NotNull(message = "对话请求对象不能为空")
private ChatRequest chatRequest;
/**
* SSe连接对象
*/
@NotNull(message = "SSe连接对象不能为空")
private SseEmitter emitter;
/**
* 用户ID
*/
@NotNull(message = "用户ID不能为空")
private Long userId;
/**
* Token
*/
@NotNull(message = "Token不能为空")
private String tokenValue;
/**
* 响应处理器
*/
private StreamingChatResponseHandler handler;
}

View File

@@ -14,6 +14,7 @@ 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.entity.chat.ChatContext;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.common.chat.factory.ChatServiceFactory;
import org.ruoyi.workflow.base.NodeInputConfigTypeHandler;
@@ -135,9 +136,11 @@ public class WorkflowUtil {
.startingState(state)
.build();
// 获取用户信息和Token以及SSe连接对象对话接口需要使用
Long userId = wfState.getUserId();
String tokenValue = wfState.getTokenValue();
SseEmitter sseEmitter = wfState.getSseEmitter();
StreamingChatResponseHandler handler = streamingGenerator.handler();
// 构建 ruoyi-ai 的 ChatRequest
List<ChatMessage> chatMessages = new ArrayList<>();
@@ -151,9 +154,18 @@ public class WorkflowUtil {
chatRequest.setModel(modelName);
chatRequest.setChatMessages(chatMessages);
//构建聊天对话上下文参数
ChatContext chatContext = ChatContext.builder()
.chatModelVo(chatModelVo)
.chatRequest(chatRequest)
.emitter(sseEmitter)
.userId(userId)
.tokenValue(tokenValue)
.handler(handler)
.build();
// 使用工作流专用方法
StreamingChatResponseHandler handler = streamingGenerator.handler();
chatService.chat(chatModelVo, chatRequest, sseEmitter, userId, tokenValue, handler);
chatService.chat(chatContext);
wfState.getNodeToStreamingGenerator().put(node.getUuid(), streamingGenerator);
}

View File

@@ -26,6 +26,7 @@ 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.entity.chat.ChatContext;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.common.core.utils.ObjectUtils;
import org.ruoyi.common.core.utils.SpringUtils;
@@ -36,6 +37,7 @@ import org.ruoyi.enums.RoleType;
import org.ruoyi.service.chat.IChatMessageService;
import org.ruoyi.service.chat.impl.memory.PersistentChatMemoryStore;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.*;
@@ -55,6 +57,7 @@ import java.util.concurrent.ConcurrentHashMap;
* @date 2025/12/13
*/
@Slf4j
@Validated
public abstract class AbstractStreamingChatService implements IChatService {
/**
@@ -73,29 +76,23 @@ public abstract class AbstractStreamingChatService implements IChatService {
*/
private static final Map<Object, MessageWindowChatMemory> memoryCache = new ConcurrentHashMap<>();
/**
* 定义聊天流程骨架
*/
@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) {
public SseEmitter chat(ChatContext chatContext) {
// 获取模型管理视图对象
ChatModelVo chatModelVo = chatContext.getChatModelVo();
// 获取对话请求对象
ChatRequest chatRequest = chatContext.getChatRequest();
// 获取SSe连接对象
SseEmitter emitter = chatContext.getEmitter();
// 获取用户ID
Long userId = chatContext.getUserId();
// 获取Token
String tokenValue = chatContext.getTokenValue();
// 获取响应处理器
StreamingChatResponseHandler handler = chatContext.getHandler();
try {
String content = Optional.ofNullable(chatRequest.getMessages()).filter(messages -> !messages.isEmpty())
// 对话逻辑:从 messages 筛选第一个元素
@@ -103,11 +100,11 @@ public abstract class AbstractStreamingChatService implements IChatService {
.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(""));
.filter(message -> message instanceof UserMessage um)
.map(message -> ((UserMessage) message).singleText())
.filter(StringUtils::isNotBlank)
.findFirst()
.orElse(""));
// 保存用户消息
saveChatMessage(chatRequest, userId, content, RoleType.USER.getName(), chatModelVo);

View File

@@ -6,6 +6,7 @@ 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.entity.chat.ChatContext;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.common.chat.factory.ChatServiceFactory;
import org.ruoyi.common.satoken.utils.LoginHelper;
@@ -71,7 +72,16 @@ public class ChatServiceFacade {
Long userId = LoginHelper.getUserId();
String tokenValue = StpUtil.getTokenValue();
SseEmitter emitter = sseEmitterManager.connect(userId, tokenValue);
return chatService.chat(chatModelVo, chatRequest,emitter,userId, tokenValue);
// 5. 创建对话上下文对象
ChatContext chatContext = ChatContext.builder()
.chatModelVo(chatModelVo)
.chatRequest(chatRequest)
.emitter(emitter)
.userId(userId)
.tokenValue(tokenValue)
.build();
return chatService.chat(chatContext);
}
/**

View File

@@ -1,18 +1,6 @@
package org.ruoyi.service.chat.impl.provider;
import java.util.ArrayList;
import java.util.List;
import org.ruoyi.agent.EchartsAgent;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.config.McpSseConfig;
import org.ruoyi.enums.ChatModeType;
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 dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.agentic.AgenticServices;
import dev.langchain4j.agentic.supervisor.SupervisorAgent;
import dev.langchain4j.agentic.supervisor.SupervisorResponseStrategy;
@@ -25,11 +13,23 @@ 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.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.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;
/**
* qianWenAI服务调用
@@ -41,6 +41,13 @@ import lombok.extern.slf4j.Slf4j;
@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";
@@ -101,45 +108,49 @@ public class QianWenChatServiceImpl extends AbstractStreamingChatService {
* @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();
McpTransport echart = new StdioMcpTransport.Builder()
.command(List.of("uv",
"--directory",
"/Users/zhangmingming/data/coder/LLM/MCP/cicd-pipeline-example/",
"run",
"text2sql-mcp"
))
.logEvents(true)
.build();
// 步骤2: 创建MCP客户端
McpClient echartClient = new DefaultMcpClient.Builder()
.transport(echart)
.build();
// 步骤3: 配置工具提供者
ToolProvider echartToolProvider = McpToolProvider.builder()
.mcpClients(List.of(
echartClient))
.build();
EchartsAgent chartGenerationAgent = AgenticServices.agentBuilder(
EchartsAgent.class)
// 步骤5将MCP对象由智能体Agent管控
McpAgent mcpAgent = AgenticServices.agentBuilder(McpAgent.class)
.chatModel(qwenChatModel)
.toolProvider(echartToolProvider)
.toolProvider(toolProvider)
.build();
// 步骤6将所有MCP对象由超级智能体管控
SupervisorAgent supervisor = AgenticServices
.supervisorBuilder()
.chatModel(qwenChatModel)
.subAgents(chartGenerationAgent)
.subAgents(mcpAgent)
.responseStrategy(SupervisorResponseStrategy.LAST)
.build();