|
@@ -3,12 +3,16 @@ package cn.iocoder.yudao.module.ai.service.chat;
|
|
import cn.hutool.core.collection.CollUtil;
|
|
import cn.hutool.core.collection.CollUtil;
|
|
import cn.hutool.core.util.ObjUtil;
|
|
import cn.hutool.core.util.ObjUtil;
|
|
import cn.hutool.core.util.StrUtil;
|
|
import cn.hutool.core.util.StrUtil;
|
|
|
|
+import cn.hutool.json.JSONObject;
|
|
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
|
|
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
|
|
import cn.iocoder.yudao.framework.ai.core.util.AiUtils;
|
|
import cn.iocoder.yudao.framework.ai.core.util.AiUtils;
|
|
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
|
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
|
|
|
+import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
|
|
|
|
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
|
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
|
|
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
|
|
|
|
+import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationUpdateMyReqVO;
|
|
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO;
|
|
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO;
|
|
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageRespVO;
|
|
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageRespVO;
|
|
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO;
|
|
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO;
|
|
@@ -21,6 +25,7 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
|
|
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO;
|
|
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO;
|
|
import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper;
|
|
import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper;
|
|
import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
|
|
import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
|
|
|
|
+import cn.iocoder.yudao.module.ai.service.chat.vo.DifyFlowResponse;
|
|
import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService;
|
|
import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService;
|
|
import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService;
|
|
import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService;
|
|
import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO;
|
|
import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO;
|
|
@@ -39,8 +44,10 @@ import org.springframework.ai.chat.model.ChatResponse;
|
|
import org.springframework.ai.chat.model.StreamingChatModel;
|
|
import org.springframework.ai.chat.model.StreamingChatModel;
|
|
import org.springframework.ai.chat.prompt.ChatOptions;
|
|
import org.springframework.ai.chat.prompt.ChatOptions;
|
|
import org.springframework.ai.chat.prompt.Prompt;
|
|
import org.springframework.ai.chat.prompt.Prompt;
|
|
|
|
+import org.springframework.http.MediaType;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
+import org.springframework.web.reactive.function.client.WebClient;
|
|
import reactor.core.publisher.Flux;
|
|
import reactor.core.publisher.Flux;
|
|
|
|
|
|
import java.time.LocalDateTime;
|
|
import java.time.LocalDateTime;
|
|
@@ -70,10 +77,9 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
|
private static final String KNOWLEDGE_USER_MESSAGE_TEMPLATE = "使用 <Reference></Reference> 标记中的内容作为本次对话的参考:\n\n" +
|
|
private static final String KNOWLEDGE_USER_MESSAGE_TEMPLATE = "使用 <Reference></Reference> 标记中的内容作为本次对话的参考:\n\n" +
|
|
"%s\n\n" + // 多个 <Reference></Reference> 的拼接
|
|
"%s\n\n" + // 多个 <Reference></Reference> 的拼接
|
|
"回答要求:\n- 避免提及你是从 <Reference></Reference> 获取的知识。";
|
|
"回答要求:\n- 避免提及你是从 <Reference></Reference> 获取的知识。";
|
|
-
|
|
|
|
|
|
+ private final WebClient webClient;
|
|
@Resource
|
|
@Resource
|
|
private AiChatMessageMapper chatMessageMapper;
|
|
private AiChatMessageMapper chatMessageMapper;
|
|
-
|
|
|
|
@Resource
|
|
@Resource
|
|
private AiChatConversationService chatConversationService;
|
|
private AiChatConversationService chatConversationService;
|
|
@Resource
|
|
@Resource
|
|
@@ -86,7 +92,12 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
|
private AiKnowledgeDocumentService knowledgeDocumentService;
|
|
private AiKnowledgeDocumentService knowledgeDocumentService;
|
|
@Resource
|
|
@Resource
|
|
private AiToolService toolService;
|
|
private AiToolService toolService;
|
|
-
|
|
|
|
|
|
+
|
|
|
|
+ public AiChatMessageServiceImpl(WebClient.Builder webClientBuilder) {
|
|
|
|
+ this.webClient = webClientBuilder.baseUrl("http://42.194.163.46:9502").build();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Override
|
|
@Transactional(rollbackFor = Exception.class)
|
|
@Transactional(rollbackFor = Exception.class)
|
|
public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) {
|
|
public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) {
|
|
// 1.1 校验对话存在
|
|
// 1.1 校验对话存在
|
|
@@ -134,7 +145,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
|
.setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class)
|
|
.setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class)
|
|
.setContent(newContent).setSegments(segments));
|
|
.setContent(newContent).setSegments(segments));
|
|
}
|
|
}
|
|
-
|
|
|
|
|
|
+
|
|
@Override
|
|
@Override
|
|
public Flux<CommonResult<AiChatMessageSendRespVO>> sendChatMessageStream(AiChatMessageSendReqVO sendReqVO,
|
|
public Flux<CommonResult<AiChatMessageSendRespVO>> sendChatMessageStream(AiChatMessageSendReqVO sendReqVO,
|
|
Long userId) {
|
|
Long userId) {
|
|
@@ -199,7 +210,129 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
|
new AiChatMessageDO().setId(assistantMessage.getId()).setContent(throwable.getMessage())));
|
|
new AiChatMessageDO().setId(assistantMessage.getId()).setContent(throwable.getMessage())));
|
|
}).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR)));
|
|
}).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR)));
|
|
}
|
|
}
|
|
-
|
|
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ public Flux<CommonResult<AiChatMessageSendRespVO>> difyChatMessageStream(AiChatMessageSendReqVO sendReqVO,
|
|
|
|
+ Long userId) {
|
|
|
|
+ // 1.1 校验对话存在
|
|
|
|
+ AiChatConversationDO conversation = chatConversationService
|
|
|
|
+ .validateChatConversationExists(sendReqVO.getConversationId());
|
|
|
|
+ Map<String, Object> inputs = new HashMap<>();
|
|
|
|
+ if (ObjUtil.notEqual(conversation.getUserId(), userId)) {
|
|
|
|
+ throw exception(CHAT_CONVERSATION_NOT_EXISTS);
|
|
|
|
+ }
|
|
|
|
+ // 1.2 校验模型
|
|
|
|
+ AiModelDO model = new AiModelDO();
|
|
|
|
+ List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = new ArrayList<>();
|
|
|
|
+ model.setModel("dify");
|
|
|
|
+ model.setId(99L);
|
|
|
|
+ // 3. 插入 user 发送消息
|
|
|
|
+ AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model,
|
|
|
|
+ userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(),
|
|
|
|
+ null);
|
|
|
|
+ String apiKey = DictFrameworkUtils.parseDictDataValue("ai_key", "多轮对话");
|
|
|
|
+ JSONObject requestBody = new JSONObject();
|
|
|
|
+ if (StrUtil.isNotBlank(conversation.getDifyConversationId())) {
|
|
|
|
+ requestBody.set("conversation_id", conversation.getDifyConversationId());
|
|
|
|
+ }
|
|
|
|
+ String type = "";
|
|
|
|
+ if (userId == 1L) {
|
|
|
|
+ inputs.put("type", "系统客服");
|
|
|
|
+ type = "系统客服";
|
|
|
|
+ } else if (userId == 2L) {
|
|
|
|
+ inputs.put("type", "贷款专家");
|
|
|
|
+ type = "贷款专家";
|
|
|
|
+ } else if (userId == 3L) {
|
|
|
|
+ inputs.put("type", "律师专家");
|
|
|
|
+ type = "律师专家";
|
|
|
|
+ } else {
|
|
|
|
+ inputs.put("type", "其他");
|
|
|
|
+ type = "其他";
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ inputs.put("token", sendReqVO.getToken());
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ requestBody.set("response_mode", "streaming");
|
|
|
|
+ requestBody.set("user", SecurityFrameworkUtils.getLoginUserId());
|
|
|
|
+ // 如果difyConversationId为0 则为 开场白
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ if (CollUtil.isNotEmpty(sendReqVO.getFileUrls())) {
|
|
|
|
+ inputs.put("fileUrls", StrUtil.join(",", sendReqVO.getFileUrls()));
|
|
|
|
+ if (CollUtil.isNotEmpty(sendReqVO.getFileUrls())) {
|
|
|
|
+ List<Map<String, Object>> docs = new ArrayList<>();
|
|
|
|
+ for (String image : sendReqVO.getFileUrls()) {
|
|
|
|
+ Map<String, Object> variableValue = new HashMap<>();
|
|
|
|
+ variableValue.put("transfer_method", "remote_url");
|
|
|
|
+ variableValue.put("url", image);
|
|
|
|
+ if (StrUtil.containsAny(image, ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx")) {
|
|
|
|
+ variableValue.put("type", "document");
|
|
|
|
+ } else {
|
|
|
|
+ variableValue.put("type", "image");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ docs.add(variableValue);
|
|
|
|
+ }
|
|
|
|
+ requestBody.set("files", docs);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ requestBody.set("inputs", inputs);
|
|
|
|
+ requestBody.set("query", sendReqVO.getContent());
|
|
|
|
+ AiChatMessageSendRespVO result = new AiChatMessageSendRespVO();
|
|
|
|
+ result.setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class));
|
|
|
|
+ // 使用 WebClient 发起非阻塞请求
|
|
|
|
+ Flux<AiChatMessageSendReqVO> flux = sendMessageToDify(apiKey, requestBody,
|
|
|
|
+ sendReqVO);
|
|
|
|
+
|
|
|
|
+ // 3.2 流式返回
|
|
|
|
+ StringBuffer contentBuffer = new StringBuffer();
|
|
|
|
+ StringBuffer difyId = new StringBuffer();
|
|
|
|
+ AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model,
|
|
|
|
+ userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(),
|
|
|
|
+ knowledgeSegments);
|
|
|
|
+ return flux.map(chunk -> {
|
|
|
|
+ String newContent = chunk.getContent();
|
|
|
|
+ contentBuffer.append(newContent);
|
|
|
|
+ if (chunk.getEvent().equals("message_end")) {
|
|
|
|
+ difyId.append(chunk.getDifyConversationId());
|
|
|
|
+ }
|
|
|
|
+ assistantMessage.setContent(newContent);
|
|
|
|
+ AiChatMessageSendRespVO.Message receive = BeanUtils.toBean(assistantMessage,
|
|
|
|
+ AiChatMessageSendRespVO.Message.class);
|
|
|
|
+ receive.setEvent(chunk.getEvent());
|
|
|
|
+ result.setReceive(receive);
|
|
|
|
+ // 响应结果
|
|
|
|
+ return success(result);
|
|
|
|
+ }).doOnComplete(() -> {
|
|
|
|
+ // 忽略租户,因为 Flux 异步无法透传租户
|
|
|
|
+ assistantMessage.setContent(contentBuffer.toString());
|
|
|
|
+ // 1.2 保存消息
|
|
|
|
+ //更新 创建时间 方便排序
|
|
|
|
+ assistantMessage.setCreateTime(LocalDateTime.now());
|
|
|
|
+ TenantUtils.executeIgnore(() ->
|
|
|
|
+ chatMessageMapper.updateById(assistantMessage));
|
|
|
|
+ AiChatConversationUpdateMyReqVO updateReqVO = new AiChatConversationUpdateMyReqVO();
|
|
|
|
+ updateReqVO.setId(conversation.getId());
|
|
|
|
+ updateReqVO.setDifyConversationId(difyId.toString());
|
|
|
|
+ updateReqVO.setTitle(StrUtil.subPre(sendReqVO.getContent(), 20));
|
|
|
|
+ TenantUtils.executeIgnore(() ->
|
|
|
|
+ chatConversationService.updateConversation(updateReqVO));
|
|
|
|
+
|
|
|
|
+ }).doOnError(throwable -> {
|
|
|
|
+ log.error("[generateWriteCon-tent][generateReqVO({}) 发生异常]", null, throwable);
|
|
|
|
+ // 忽略租户,因为 Flux 异步无法透传租户
|
|
|
|
+
|
|
|
|
+ }).onErrorResume(error -> Flux.just(null));
|
|
|
|
+ // 4.1 插入 assistant 接收消息
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ // 4.2 构建 Prompt,并进行调用
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
private List<AiKnowledgeSegmentSearchRespBO> recallKnowledgeSegment(String content,
|
|
private List<AiKnowledgeSegmentSearchRespBO> recallKnowledgeSegment(String content,
|
|
AiChatConversationDO conversation) {
|
|
AiChatConversationDO conversation) {
|
|
// 1. 查询聊天角色
|
|
// 1. 查询聊天角色
|
|
@@ -219,7 +352,65 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
|
}
|
|
}
|
|
return knowledgeSegments;
|
|
return knowledgeSegments;
|
|
}
|
|
}
|
|
-
|
|
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 向dify发送消息
|
|
|
|
+ *
|
|
|
|
+ * @param apiKey api密钥
|
|
|
|
+ * @param requestBody 请求正文
|
|
|
|
+ * @param message 信息
|
|
|
|
+ * @return {@link Flux }<{@link AiChatMessageSendReqVO }>
|
|
|
|
+ */
|
|
|
|
+ public Flux<AiChatMessageSendReqVO> sendMessageToDify(String apiKey, JSONObject requestBody,
|
|
|
|
+ AiChatMessageSendReqVO message) {
|
|
|
|
+ return webClient.post()
|
|
|
|
+ .uri("/v1/chat-messages") // 假设的API路径,根据实际情况修改
|
|
|
|
+ .headers(httpHeaders -> {
|
|
|
|
+ httpHeaders.setContentType(MediaType.APPLICATION_JSON);
|
|
|
|
+ httpHeaders.setBearerAuth(apiKey);
|
|
|
|
+ })
|
|
|
|
+ .bodyValue(requestBody)
|
|
|
|
+ .retrieve()
|
|
|
|
+ .bodyToFlux(DifyFlowResponse.class)
|
|
|
|
+ .map(difyResponse -> convertToKeFuMessage(difyResponse, message))
|
|
|
|
+ .filter(this::shouldInclude)
|
|
|
|
+ .doOnComplete(() -> {
|
|
|
|
+
|
|
|
|
+ })
|
|
|
|
+ .doOnTerminate(() -> System.out.println("请求结束"));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 应包括
|
|
|
|
+ *
|
|
|
|
+ * @param streamResponse 流响应
|
|
|
|
+ * @return boolean
|
|
|
|
+ */
|
|
|
|
+ private boolean shouldInclude(AiChatMessageSendReqVO streamResponse) {
|
|
|
|
+ // 示例:只要message节点的数据和message_end节点的数据
|
|
|
|
+ if (streamResponse.getEvent().equals("message")
|
|
|
|
+ || streamResponse.getEvent().equals("message_end")) {
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private AiChatMessageSendReqVO convertToKeFuMessage(DifyFlowResponse response, AiChatMessageSendReqVO message) {
|
|
|
|
+ // 实现 DifyResponse 到 KeFuMessageRespVO 的转换逻辑
|
|
|
|
+ // 设置其他必要的字段...
|
|
|
|
+ StringBuffer contentBuffer = new StringBuffer();
|
|
|
|
+ if (StrUtil.equals("message", response.getEvent())) {
|
|
|
|
+ message.setContent(response.getAnswer());
|
|
|
|
+ message.setDifyConversationId(response.getConversation_id());
|
|
|
|
+ contentBuffer.append(response.getAnswer());
|
|
|
|
+ } else if (StrUtil.equals("message_end", response.getEvent())) {
|
|
|
|
+
|
|
|
|
+ } else {
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ message.setEvent(response.getEvent());
|
|
|
|
+ return message;
|
|
|
|
+ }
|
|
private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages,
|
|
private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages,
|
|
List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments,
|
|
List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments,
|
|
AiModelDO model, AiChatMessageSendReqVO sendReqVO) {
|
|
AiModelDO model, AiChatMessageSendReqVO sendReqVO) {
|