mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-04-03 15:06:11 +00:00
AI工作流优化
This commit is contained in:
@@ -26,7 +26,7 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-core</artifactId>
|
||||
<artifactId>ruoyi-common-chat</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -34,13 +34,6 @@
|
||||
<artifactId>ruoyi-common-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-mybatis</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-satoken</artifactId>
|
||||
|
||||
@@ -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 -> {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, List<String>> edges = new HashMap<>();
|
||||
@@ -55,10 +59,13 @@ public class WfState {
|
||||
*/
|
||||
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.user = user;
|
||||
this.uuid = uuid;
|
||||
this.userId = userId;
|
||||
this.tokenValue = tokenValue;
|
||||
this.sseEmitter = sseEmitter;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -68,7 +68,7 @@ public class WorkflowEngine {
|
||||
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.sseEmitter = sseEmitter;
|
||||
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();
|
||||
WorkflowNode startNode = startAndEnds.getLeft();
|
||||
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);
|
||||
|
||||
|
||||
|
||||
@@ -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<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)) {
|
||||
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<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);
|
||||
List<WorkflowComponent> components = workflowComponentService.getAllEnable();
|
||||
List<WorkflowNode> 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
|
||||
|
||||
@@ -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<NodeIOData> values) {
|
||||
// 🔒 关键修复:如果 template 为 null,直接返回 null 或空字符串
|
||||
if (template == null) {
|
||||
@@ -74,8 +87,8 @@ public class WorkflowUtil {
|
||||
|
||||
public static String getHumanFeedbackTip(String nodeUuid, List<WorkflowNode> 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<UserMessage> systemMessage) {
|
||||
log.info("stream invoke, category: {}, modelName: {}", category, modelName);
|
||||
public void streamingInvokeLLM(WfState wfState, WfNodeState state, WorkflowNode node, String modelName,
|
||||
List<SystemMessage> 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<AgentState> 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<NodeIOData> 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<NodeIOData> 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<Message> messages = new ArrayList<>();
|
||||
//
|
||||
// addUserMessage(node, state.getInputs(), messages);
|
||||
//
|
||||
// addSystemMessage(systemMessage, messages);
|
||||
//
|
||||
// ChatRequest chatRequest = new ChatRequest();
|
||||
// chatRequest.setModel(modelName);
|
||||
// chatRequest.setMessages(messages);
|
||||
List<ChatMessage> 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<NodeIOData> userMessage, List<UserMessage> messages) {
|
||||
private void addUserMessage(WorkflowNode node, List<NodeIOData> userMessage, List<ChatMessage> messages) {
|
||||
if (CollUtil.isEmpty(userMessage)) {
|
||||
return;
|
||||
}
|
||||
|
||||
WfNodeInputConfig nodeInputConfig = NodeInputConfigTypeHandler.fillNodeInputConfig(node.getInputConfig());
|
||||
|
||||
List<WfNodeParamRef> refInputs = nodeInputConfig.getRefInputs();
|
||||
|
||||
Set<String> 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<UserMessage> 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<UserMessage> systemMessage, List<UserMessage> messages) {
|
||||
log.info("addSystemMessage received: {}", systemMessage); // 🔥 加这一行
|
||||
|
||||
if (CollUtil.isEmpty(systemMessage)) {
|
||||
return;
|
||||
}
|
||||
systemMessage.stream()
|
||||
.map(userMsg -> getMessage("system", userMsg.singleText()))
|
||||
.forEach(messages::add);
|
||||
private List<UserMessage> buildMessageList(List<NodeIOData> userMessage, Set<String> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<UserMessage> systemMessage = List.of(UserMessage.from(prompt));
|
||||
workflowUtil.streamingInvokeLLM(wfState, state, node, category, modelName, systemMessage);
|
||||
// 转换系统信息结构
|
||||
List<SystemMessage> systemMessage = List.of(new SystemMessage(prompt));
|
||||
workflowUtil.streamingInvokeLLM(wfState, state, node, modelName, systemMessage);
|
||||
return new NodeProcessResult();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<dev.langchain4j.data.message.UserMessage> systemMessage =
|
||||
List.of(dev.langchain4j.data.message.UserMessage.from(prompt));
|
||||
List<SystemMessage> 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
|
||||
);
|
||||
|
||||
@@ -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<NodeIOData> 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<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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据运算符评估条件
|
||||
*/
|
||||
|
||||
@@ -16,17 +16,7 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-mybatis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-excel</artifactId>
|
||||
<artifactId>ruoyi-common-chat</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -34,17 +24,6 @@
|
||||
<artifactId>ruoyi-common-sensitive</artifactId>
|
||||
</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>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-open-ai</artifactId>
|
||||
@@ -139,23 +118,11 @@
|
||||
<artifactId>ruoyi-common-doc</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-sse</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.neo4j.driver</groupId>
|
||||
<artifactId>neo4j-java-driver</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.github.imfangs</groupId>
|
||||
<artifactId>dify-java-client</artifactId>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.*;
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
package org.ruoyi.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 lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import jakarta.validation.constraints.*;
|
||||
|
||||
/**
|
||||
* 模型管理业务对象 chat_model
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-14
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@AutoMapper(target = ChatModel.class, reverseConvertGenerate = false)
|
||||
public class ChatModelBo extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
@NotNull(message = "主键不能为空", groups = { EditGroup.class })
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 模型分类
|
||||
*/
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
private String modelName;
|
||||
|
||||
/**
|
||||
* 模型供应商
|
||||
*/
|
||||
private String providerCode;
|
||||
|
||||
/**
|
||||
* 模型描述
|
||||
*/
|
||||
private String modelDescribe;
|
||||
|
||||
/**
|
||||
* 模型价格
|
||||
*/
|
||||
private Long modelPrice;
|
||||
|
||||
/**
|
||||
* 计费类型
|
||||
*/
|
||||
private String modelType;
|
||||
|
||||
/**
|
||||
* 是否显示
|
||||
*/
|
||||
private String modelShow;
|
||||
|
||||
/**
|
||||
* 是否免费
|
||||
*/
|
||||
private String modelFree;
|
||||
|
||||
/**
|
||||
* 模型优先级(值越大优先级越高)
|
||||
*/
|
||||
private Long priority;
|
||||
|
||||
/**
|
||||
* 请求地址
|
||||
*/
|
||||
private String apiHost;
|
||||
|
||||
/**
|
||||
* 密钥
|
||||
*/
|
||||
private String apiKey;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package org.ruoyi.domain.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 聊天消息DTO - 用于上下文传递
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/12/13
|
||||
*/
|
||||
@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";
|
||||
msg.content = content;
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
package org.ruoyi.domain.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
import org.ruoyi.domain.dto.ChatMessageDTO;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 对话请求对象
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2023-04-08
|
||||
*/
|
||||
@Data
|
||||
public class ChatRequest {
|
||||
|
||||
@NotEmpty(message = "对话消息不能为空")
|
||||
private List<ChatMessageDTO> messages;
|
||||
|
||||
@NotEmpty(message = "传入的模型不能为空")
|
||||
private String model;
|
||||
|
||||
/**
|
||||
* 会话id
|
||||
*/
|
||||
private Long sessionId;
|
||||
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
private String appId;
|
||||
|
||||
/**
|
||||
* 知识库id
|
||||
*/
|
||||
private String knowledgeId;
|
||||
|
||||
/**
|
||||
* 对话id(每个聊天窗口都不一样)
|
||||
*/
|
||||
private Long uuid;
|
||||
|
||||
/**
|
||||
* 是否启用深度思考
|
||||
*/
|
||||
private Boolean enableThinking;
|
||||
|
||||
/**
|
||||
* 是否自动切换模型
|
||||
*/
|
||||
private Boolean autoSelectModel;
|
||||
|
||||
/**
|
||||
* 是否支持联网
|
||||
*/
|
||||
private Boolean enableInternet;
|
||||
|
||||
/**
|
||||
* 会话令牌(为避免在非Web线程中获取Request,入口处注入)
|
||||
*/
|
||||
private String token;
|
||||
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package org.ruoyi.domain.entity.chat;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import org.ruoyi.common.tenant.core.TenantEntity;
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
/**
|
||||
* 模型管理对象 chat_model
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-14
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("chat_model")
|
||||
public class ChatModel extends TenantEntity {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
@TableId(value = "id")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 模型分类
|
||||
*/
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
private String modelName;
|
||||
|
||||
/**
|
||||
* 模型供应商
|
||||
*/
|
||||
private String providerCode;
|
||||
|
||||
/**
|
||||
* 模型描述
|
||||
*/
|
||||
private String modelDescribe;
|
||||
|
||||
/**
|
||||
* 模型价格
|
||||
*/
|
||||
private Long modelPrice;
|
||||
|
||||
/**
|
||||
* 计费类型
|
||||
*/
|
||||
private String modelType;
|
||||
|
||||
/**
|
||||
* 是否显示
|
||||
*/
|
||||
private String modelShow;
|
||||
|
||||
/**
|
||||
* 是否免费
|
||||
*/
|
||||
private String modelFree;
|
||||
|
||||
/**
|
||||
* 模型优先级(值越大优先级越高)
|
||||
*/
|
||||
private Long priority;
|
||||
|
||||
/**
|
||||
* 请求地址
|
||||
*/
|
||||
private String apiHost;
|
||||
|
||||
/**
|
||||
* 密钥
|
||||
*/
|
||||
private String apiKey;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package org.ruoyi.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 java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 模型管理视图对象 chat_model
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-14
|
||||
*/
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
@AutoMapper(target = ChatModel.class)
|
||||
public class ChatModelVo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
@ExcelProperty(value = "主键")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 模型分类
|
||||
*/
|
||||
@ExcelProperty(value = "模型分类")
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
@ExcelProperty(value = "模型名称")
|
||||
private String modelName;
|
||||
|
||||
/**
|
||||
* 模型供应商
|
||||
*/
|
||||
@ExcelProperty(value = "模型供应商")
|
||||
private String providerCode;
|
||||
|
||||
/**
|
||||
* 模型描述
|
||||
*/
|
||||
@ExcelProperty(value = "模型描述")
|
||||
private String modelDescribe;
|
||||
|
||||
/**
|
||||
* 模型价格
|
||||
*/
|
||||
@ExcelProperty(value = "模型价格")
|
||||
private Long modelPrice;
|
||||
|
||||
/**
|
||||
* 计费类型
|
||||
*/
|
||||
@ExcelProperty(value = "计费类型")
|
||||
private String modelType;
|
||||
|
||||
/**
|
||||
* 是否显示
|
||||
*/
|
||||
@ExcelProperty(value = "是否显示")
|
||||
private String modelShow;
|
||||
|
||||
/**
|
||||
* 是否免费
|
||||
*/
|
||||
@ExcelProperty(value = "是否免费")
|
||||
private String modelFree;
|
||||
|
||||
/**
|
||||
* 模型优先级(值越大优先级越高)
|
||||
*/
|
||||
@ExcelProperty(value = "模型优先级(值越大优先级越高)")
|
||||
private Long priority;
|
||||
|
||||
/**
|
||||
* 请求地址
|
||||
*/
|
||||
@ExcelProperty(value = "请求地址")
|
||||
private String apiHost;
|
||||
|
||||
/**
|
||||
* 密钥
|
||||
*/
|
||||
@ExcelProperty(value = "密钥")
|
||||
private String apiKey;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
@ExcelProperty(value = "备注")
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 模型维度
|
||||
*/
|
||||
private Integer dimension;
|
||||
|
||||
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package org.ruoyi.factory;
|
||||
|
||||
import org.ruoyi.service.chat.IChatService;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 聊天服务工厂类
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025-12-13
|
||||
*/
|
||||
@Component
|
||||
public class ChatServiceFactory implements ApplicationContextAware {
|
||||
|
||||
private final Map<String, IChatService> chatServiceMap = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
// 初始化时收集所有IChatService的实现
|
||||
Map<String, IChatService> serviceMap = applicationContext.getBeansOfType(IChatService.class);
|
||||
for (IChatService service : serviceMap.values()) {
|
||||
if (service != null ) {
|
||||
chatServiceMap.put(service.getProviderName(), service);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取原始服务(不包装代理)
|
||||
*/
|
||||
public IChatService getOriginalService(String category) {
|
||||
IChatService service = chatServiceMap.get(category);
|
||||
if (service == null) {
|
||||
throw new IllegalArgumentException("不支持的模型类别: " + category);
|
||||
}
|
||||
return service;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
package org.ruoyi.service.chat;
|
||||
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
import org.ruoyi.domain.bo.chat.ChatModelBo;
|
||||
import org.ruoyi.domain.vo.chat.ChatModelVo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 模型管理Service接口
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-14
|
||||
*/
|
||||
public interface IChatModelService {
|
||||
|
||||
/**
|
||||
* 查询模型管理
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 模型管理
|
||||
*/
|
||||
ChatModelVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 根据模型名称查询模型
|
||||
*
|
||||
* @param modelName 模型名称
|
||||
* @return 模型管理
|
||||
*/
|
||||
ChatModelVo selectModelByName(String modelName);
|
||||
|
||||
/**
|
||||
* 分页查询模型管理列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @return 模型管理分页列表
|
||||
*/
|
||||
TableDataInfo<ChatModelVo> queryPageList(ChatModelBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询符合条件的模型管理列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 模型管理列表
|
||||
*/
|
||||
List<ChatModelVo> queryList(ChatModelBo bo);
|
||||
|
||||
/**
|
||||
* 新增模型管理
|
||||
*
|
||||
* @param bo 模型管理
|
||||
* @return 是否新增成功
|
||||
*/
|
||||
Boolean insertByBo(ChatModelBo bo);
|
||||
|
||||
/**
|
||||
* 修改模型管理
|
||||
*
|
||||
* @param bo 模型管理
|
||||
* @return 是否修改成功
|
||||
*/
|
||||
Boolean updateByBo(ChatModelBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除模型管理信息
|
||||
*
|
||||
* @param ids 待删除的主键集合
|
||||
* @param isValid 是否进行有效性校验
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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<ChatMessage> 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<ChatMessage> buildMessagesWithMemory(ChatRequest chatRequest) {
|
||||
List<ChatMessage> 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<ChatMessage> 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<ChatMessage> messagesWithMemory,StreamingChatResponseHandler handler);
|
||||
protected abstract void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List<ChatMessage> 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();
|
||||
|
||||
|
||||
@@ -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<org.ruoyi.domain.dto.ChatMessageDTO> getMessagesBySessionId(Long sessionId) {
|
||||
public List<ChatMessageDTO> getMessagesBySessionId(Long sessionId) {
|
||||
if (sessionId == null) {
|
||||
return new java.util.ArrayList<>();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<ChatMessage> messagesWithMemory,
|
||||
StreamingChatResponseHandler handler) {
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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服务接口
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<SysOssUploadVo> fileUpload(@RequestPart("file") MultipartFile file) {
|
||||
if (ObjectUtil.isNull(file)) {
|
||||
return R.fail("上传文件不能为空");
|
||||
}
|
||||
return R.ok(ossService.fileUpload(file));
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载OSS对象
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
* 文件下载方法,支持一次性下载完整文件
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user