|
|
@@ -1,11 +1,15 @@
|
|
|
package com.fs.app.controller;
|
|
|
|
|
|
import cn.hutool.core.util.StrUtil;
|
|
|
+import cn.hutool.http.HttpUtil;
|
|
|
import com.alibaba.fastjson.JSON;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.fs.chat.service.IChatDatasetService;
|
|
|
import com.fs.common.core.domain.R;
|
|
|
-import com.fs.common.core.redis.RedisCache;
|
|
|
import com.fs.common.utils.StringUtils;
|
|
|
import com.fs.common.utils.uuid.IdUtils;
|
|
|
+import com.fs.company.domain.AiKnowledgeBase;
|
|
|
+import com.fs.company.service.AiKnowledgeBaseService;
|
|
|
import com.fs.fastGpt.domain.FastGptRole;
|
|
|
import com.fs.fastGpt.service.AiHookService;
|
|
|
import com.fs.fastGpt.service.IFastGptRoleService;
|
|
|
@@ -28,7 +32,6 @@ import com.fs.qw.service.IQwUserVoiceLogService;
|
|
|
import com.fs.sop.mapper.QwSopLogsMapper;
|
|
|
import com.fs.sop.mapper.SopUserLogsInfoMapper;
|
|
|
import com.fs.sop.params.GetQwSopLogsByJsApiParam;
|
|
|
-import com.fs.tenant.service.TenantInfoService;
|
|
|
import com.fs.voice.utils.StringUtil;
|
|
|
import com.fs.wxwork.dto.*;
|
|
|
import com.fs.wxwork.service.WxWorkService;
|
|
|
@@ -37,6 +40,7 @@ import lombok.extern.slf4j.Slf4j;
|
|
|
import org.json.JSONObject;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.beans.factory.annotation.Qualifier;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
import org.springframework.data.redis.core.RedisTemplate;
|
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
|
|
|
@@ -88,6 +92,14 @@ public class QwMsgController {
|
|
|
@Autowired
|
|
|
private TenantDataSourceUtil tenantDataSourceUtil;
|
|
|
|
|
|
+ @Value("${ai.api.base-url}")
|
|
|
+ private String aiApiBaseUrl;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private IChatDatasetService chatDatasetService;
|
|
|
+ @Autowired
|
|
|
+ private AiKnowledgeBaseService aiKnowledgeBaseService;
|
|
|
+
|
|
|
|
|
|
@GetMapping("/sendExpressInfo/{orderId}")
|
|
|
public R sendExpressInfo(@PathVariable Long orderId){
|
|
|
@@ -371,7 +383,10 @@ public class QwMsgController {
|
|
|
final String finalContent = content;
|
|
|
if (2000000000000000L-receiver>0){
|
|
|
log.info("id:{}, 客户发送", id);
|
|
|
- aiHookService.qwHookNotifyAiReply(id,sender,finalContent,wxWorkMsgResp.getUuid(),wxWorkMessageDTO.getMsgtype(),tenantId);
|
|
|
+// aiHookService.qwHookNotifyAiReply(id,sender,finalContent,wxWorkMsgResp.getUuid(),wxWorkMessageDTO.getMsgtype(),tenantId);
|
|
|
+ // ====== Qdrant RAG AI 回复(替代 FastGPT) ======
|
|
|
+ qdrantRagAiReply(id, sender, finalContent, wxWorkMsgResp.getUuid(),
|
|
|
+ wxWorkMessageDTO.getMsgtype(), tenantId, serverId);
|
|
|
}else {
|
|
|
log.info("销售发送");
|
|
|
aiHookService.qwHookNotifyAddMsgNew(id,receiver,content,wxWorkMsgResp.getUuid(),1);
|
|
|
@@ -582,4 +597,242 @@ public class QwMsgController {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // ========== Qdrant RAG AI 回复辅助方法 ==========
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Qdrant RAG AI 回复主流程
|
|
|
+ * 向量化 → Qdrant 搜索 → 组装 Prompt → LLM 生成 → 回复用户
|
|
|
+ */
|
|
|
+ private void qdrantRagAiReply(Long qwUserId, Long sender, String content, String uid,
|
|
|
+ Integer msgType, Long tenantId, Long serverId) {
|
|
|
+ // 1. 校验用户和角色
|
|
|
+ QwUser user = qwUserMapper.selectQwUserById(qwUserId);
|
|
|
+ if (user == null) {
|
|
|
+ log.warn("qdrantRagAiReply: 用户为空, qwUserId={}", qwUserId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (user.getFastGptRoleId() == null) {
|
|
|
+ log.warn("qdrantRagAiReply: 未绑定角色, qwUserId={}", qwUserId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (user.getAiStatus() != null && user.getAiStatus() == 1) {
|
|
|
+ log.warn("qdrantRagAiReply: AI已下线, qwUserId={}", qwUserId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ FastGptRole role = fastGptRoleService.selectFastGptRoleByRoleId(user.getFastGptRoleId());
|
|
|
+ if (role == null) {
|
|
|
+ log.warn("qdrantRagAiReply: 角色不存在, roleId={}", user.getFastGptRoleId());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 2. 调用 Embedding API 获取向量
|
|
|
+ log.info("qdrantRagAiReply: 开始向量化, qwUserId={}", qwUserId);
|
|
|
+ List<Float> vector = callEmbeddingApi(content);
|
|
|
+ if (vector == null || vector.isEmpty()) {
|
|
|
+ log.error("qdrantRagAiReply: 向量化返回空结果");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ log.info("qdrantRagAiReply: 向量化成功, 维度={}", vector.size());
|
|
|
+
|
|
|
+ // 3. 查询知识库
|
|
|
+ LambdaQueryWrapper<AiKnowledgeBase> lqw = new LambdaQueryWrapper<>();
|
|
|
+ lqw.eq(AiKnowledgeBase::getDelFlag, 0);
|
|
|
+ List<AiKnowledgeBase> list = aiKnowledgeBaseService.list(lqw);
|
|
|
+
|
|
|
+ // 4. 调用 Qdrant 搜索,获取知识片段
|
|
|
+ StringBuilder knowledgeSb = new StringBuilder();
|
|
|
+ if (list != null && !list.isEmpty()) {
|
|
|
+ log.info("qdrantRagAiReply: 找到 {} 个知识库", list.size());
|
|
|
+ int chunkIdx = 1;
|
|
|
+ for (AiKnowledgeBase kb : list) {
|
|
|
+ String collectionName = kb.getCollectionName();
|
|
|
+ List<Map<String, Object>> results = callQdrantSearch(collectionName, vector, 5);
|
|
|
+ if (results != null && !results.isEmpty()) {
|
|
|
+ for (Map<String, Object> item : results) {
|
|
|
+ Map<String, Object> payload = (Map<String, Object>) item.get("payload");
|
|
|
+ if (payload != null && payload.get("document") != null) {
|
|
|
+ knowledgeSb.append("【知识片段").append(chunkIdx++).append("】\n")
|
|
|
+ .append(payload.get("document")).append("\n\n");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ log.info("qdrantRagAiReply: 共获取 {} 个知识片段", chunkIdx - 1);
|
|
|
+ } else {
|
|
|
+ log.info("qdrantRagAiReply: 角色未关联知识库, 仅使用角色提示词");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 构建 Prompt
|
|
|
+ List<Map<String, String>> messages = new ArrayList<>();
|
|
|
+ StringBuilder systemPrompt = new StringBuilder();
|
|
|
+
|
|
|
+ // 角色提示词
|
|
|
+ if (role.getReminderWords() != null && !role.getReminderWords().isEmpty()) {
|
|
|
+ systemPrompt.append(role.getReminderWords()).append("\n\n");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 知识库上下文
|
|
|
+ if (knowledgeSb.length() > 0) {
|
|
|
+ systemPrompt.append("请根据以下知识库内容回答用户问题:\n\n")
|
|
|
+ .append(knowledgeSb)
|
|
|
+ .append("\n")
|
|
|
+ .append("要求:\n")
|
|
|
+ .append("1. 优先基于以上参考内容回答\n")
|
|
|
+ .append("2. 如果参考内容不足以回答,请根据你的知识回答\n")
|
|
|
+ .append("3. 回答需专业、准确、易懂");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (systemPrompt.length() > 0) {
|
|
|
+ Map<String, String> sysMsg = new HashMap<>();
|
|
|
+ sysMsg.put("role", "system");
|
|
|
+ sysMsg.put("content", systemPrompt.toString());
|
|
|
+ messages.add(sysMsg);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 用户消息
|
|
|
+ Map<String, String> userMsg = new HashMap<>();
|
|
|
+ userMsg.put("role", "user");
|
|
|
+ userMsg.put("content", content);
|
|
|
+ messages.add(userMsg);
|
|
|
+
|
|
|
+ // 6. 调用 LLM
|
|
|
+ log.info("qdrantRagAiReply: 开始调用LLM, qwUserId={}", qwUserId);
|
|
|
+ String reply = callLlmApi(messages);
|
|
|
+ if (reply == null || reply.trim().isEmpty()) {
|
|
|
+ log.error("qdrantRagAiReply: LLM 返回为空");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ log.info("qdrantRagAiReply: LLM回复成功, 内容长度={}", reply.length());
|
|
|
+
|
|
|
+ // 7. 发送回复给客户
|
|
|
+ sendAiMsgToUser(reply, sender, uid, serverId);
|
|
|
+ log.info("qdrantRagAiReply: 消息发送成功, qwUserId={}, sender={}", qwUserId, sender);
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("qdrantRagAiReply 异常: {}", e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 调用 Embedding API 获取文本向量
|
|
|
+ */
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private List<Float> callEmbeddingApi(String text) {
|
|
|
+ try {
|
|
|
+ Map<String, Object> reqBody = new HashMap<>();
|
|
|
+ reqBody.put("text", text);
|
|
|
+ String url = aiApiBaseUrl + "/ai/embedding/create";
|
|
|
+ String respStr = HttpUtil.createPost(url)
|
|
|
+ .header("Content-Type", "application/json")
|
|
|
+ .body(JSON.toJSONString(reqBody))
|
|
|
+ .timeout(30000)
|
|
|
+ .execute()
|
|
|
+ .body();
|
|
|
+
|
|
|
+ Map<String, Object> respMap = JSON.parseObject(respStr);
|
|
|
+ Object dataObj = respMap.get("data");
|
|
|
+ if (dataObj instanceof List) {
|
|
|
+ List<Float> vector = new ArrayList<>();
|
|
|
+ for (Object item : (List<?>) dataObj) {
|
|
|
+ if (item instanceof Number) {
|
|
|
+ vector.add(((Number) item).floatValue());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return vector;
|
|
|
+ }
|
|
|
+ log.error("callEmbeddingApi: data 不是数组, resp={}", respStr);
|
|
|
+ return null;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("callEmbeddingApi 异常: {}", e.getMessage(), e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 调用 Qdrant 搜索 API
|
|
|
+ */
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private List<Map<String, Object>> callQdrantSearch(String collectionName, List<Float> vector, int topK) {
|
|
|
+ try {
|
|
|
+ Map<String, Object> reqBody = new HashMap<>();
|
|
|
+ reqBody.put("collectionName", collectionName);
|
|
|
+ reqBody.put("vector", vector);
|
|
|
+ reqBody.put("topK", topK);
|
|
|
+
|
|
|
+ String url = aiApiBaseUrl + "/qdrant/point/search";
|
|
|
+ String respStr = HttpUtil.createPost(url)
|
|
|
+ .header("Content-Type", "application/json")
|
|
|
+ .body(JSON.toJSONString(reqBody))
|
|
|
+ .timeout(30000)
|
|
|
+ .execute()
|
|
|
+ .body();
|
|
|
+
|
|
|
+ Map<String, Object> respMap = JSON.parseObject(respStr);
|
|
|
+ Object dataObj = respMap.get("data");
|
|
|
+ if (dataObj instanceof List) {
|
|
|
+ List<Map<String, Object>> results = new ArrayList<>();
|
|
|
+ for (Object item : (List<?>) dataObj) {
|
|
|
+ if (item instanceof Map) {
|
|
|
+ Map<String, Object> result = (Map<String, Object>) item;
|
|
|
+ results.add(result);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return results;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("callQdrantSearch 异常, collectionName={}: {}", collectionName, e.getMessage());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 调用 LLM API
|
|
|
+ */
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private String callLlmApi(List<Map<String, String>> messages) {
|
|
|
+ try {
|
|
|
+ Map<String, Object> reqBody = new HashMap<>();
|
|
|
+ reqBody.put("messages", messages);
|
|
|
+
|
|
|
+ String url = aiApiBaseUrl + "/ai/llm/chat";
|
|
|
+ String respStr = HttpUtil.createPost(url)
|
|
|
+ .header("Content-Type", "application/json")
|
|
|
+ .body(JSON.toJSONString(reqBody))
|
|
|
+ .timeout(60000)
|
|
|
+ .execute()
|
|
|
+ .body();
|
|
|
+
|
|
|
+ Map<String, Object> respMap = JSON.parseObject(respStr);
|
|
|
+ Object dataObj = respMap.get("data");
|
|
|
+ if (dataObj instanceof Map) {
|
|
|
+ Map<String, Object> data = (Map<String, Object>) dataObj;
|
|
|
+ Object content = data.get("content");
|
|
|
+ return content != null ? content.toString() : null;
|
|
|
+ }
|
|
|
+ log.error("callLlmApi: data 格式异常, resp={}", respStr);
|
|
|
+ return null;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("callLlmApi 异常: {}", e.getMessage(), e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 通过企微 iPad 协议发送文本消息给客户
|
|
|
+ */
|
|
|
+ private void sendAiMsgToUser(String content, Long sendId, String uuid, Long serverId) {
|
|
|
+ try {
|
|
|
+ WxWorkSendTextMsgDTO textDTO = new WxWorkSendTextMsgDTO();
|
|
|
+ textDTO.setSend_userid(sendId);
|
|
|
+ textDTO.setUuid(uuid);
|
|
|
+ textDTO.setContent(content);
|
|
|
+ textDTO.setIsRoom(false);
|
|
|
+ wxWorkService.SendTextMsg(textDTO, serverId);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("sendAiMsgToUser 异常: {}", e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|