|
|
|
package org.jeecg.modules.airag.zdyrag.controller;
|
|
|
|
|
|
|
|
import cn.hutool.core.collection.CollectionUtil;
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
import dev.langchain4j.data.message.ChatMessage;
|
|
|
|
import dev.langchain4j.data.message.UserMessage;
|
|
|
|
import dev.langchain4j.service.TokenStream;
|
|
|
|
import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore;
|
|
|
|
import io.swagger.v3.oas.annotations.Operation;
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
|
|
import org.apache.shiro.SecurityUtils;
|
|
|
|
import org.apache.shiro.subject.Subject;
|
|
|
|
import org.apache.shiro.util.ThreadContext;
|
|
|
|
import org.jeecg.ai.handler.AIParams;
|
|
|
|
import org.jeecg.ai.handler.LLMHandler;
|
|
|
|
import org.jeecg.common.api.vo.Result;
|
|
|
|
import org.jeecg.common.system.vo.LoginUser;
|
|
|
|
import org.jeecg.modules.airag.app.entity.AiragLog;
|
|
|
|
import org.jeecg.modules.airag.app.entity.QuestionEmbedding;
|
|
|
|
import org.jeecg.modules.airag.app.service.IAiragLogService;
|
|
|
|
import org.jeecg.modules.airag.app.service.IQuestionEmbeddingService;
|
|
|
|
import org.jeecg.modules.airag.app.utils.FileToBase64Util;
|
|
|
|
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
|
|
|
|
import org.jeecg.modules.airag.llm.handler.EmbeddingHandler;
|
|
|
|
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
|
|
|
|
import org.jeecg.modules.airag.zdyrag.service.AiragResponseService;
|
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
|
|
import org.springframework.stereotype.Component;
|
|
|
|
import org.springframework.web.bind.annotation.GetMapping;
|
|
|
|
import org.springframework.web.bind.annotation.RequestMapping;
|
|
|
|
import org.springframework.web.bind.annotation.RequestParam;
|
|
|
|
import org.springframework.web.bind.annotation.RestController;
|
|
|
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
|
|
|
|
|
|
|
import java.io.File;
|
|
|
|
import java.io.IOException;
|
|
|
|
import java.util.concurrent.ExecutorService;
|
|
|
|
import java.util.concurrent.Executors;
|
|
|
|
|
|
|
|
import java.util.*;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 直接回答llm
|
|
|
|
*/
|
|
|
|
@RestController
|
|
|
|
@RequestMapping("/airag/zdyRag")
|
|
|
|
@Slf4j
|
|
|
|
public class ZdyRagController {
|
|
|
|
@Autowired
|
|
|
|
private EmbeddingHandler embeddingHandler;
|
|
|
|
@Autowired
|
|
|
|
IAIChatHandler aiChatHandler;
|
|
|
|
@Autowired
|
|
|
|
private IQuestionEmbeddingService questionEmbeddingService;
|
|
|
|
@Value("${jeecg.upload.path}")
|
|
|
|
private String uploadPath;
|
|
|
|
@Autowired
|
|
|
|
private IAiragLogService airagLogService;
|
|
|
|
|
|
|
|
|
|
|
|
// 用于异步处理的线程池
|
|
|
|
private final ExecutorService executor = Executors.newCachedThreadPool();
|
|
|
|
@Autowired
|
|
|
|
private AiragResponseService airagResponseService;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @author lixiang
|
|
|
|
* @param questionText 问题文本
|
|
|
|
* @param code 快捷按钮code
|
|
|
|
* @param codeType 提问方式,用于记录日志,区分输入框提问还是快捷方式
|
|
|
|
* @param user 提问人
|
|
|
|
* @return 以流式返回回答结果
|
|
|
|
*
|
|
|
|
* 1、将提问文本与问题库匹配,若匹配则回答预设回答结果
|
|
|
|
* 2、若问题库中无匹配预设问题,则查询知识库,将查询到的知识提供给llm模型,生成回答结果
|
|
|
|
* 3、回答时会将当初上传的资料以参考资料的形式进行返回,可进行预览
|
|
|
|
* 4、将本次的问答结果记录日志
|
|
|
|
* todo :增加产品推荐功能?
|
|
|
|
*/
|
|
|
|
|
|
|
|
@Operation(summary = "sendStream")
|
|
|
|
@GetMapping("sendStream")
|
|
|
|
public SseEmitter sendStream(@RequestParam("questionText") String questionText,
|
|
|
|
@RequestParam("code") String code,
|
|
|
|
@RequestParam("codeType") Integer codeType,
|
|
|
|
@RequestParam("user") String user
|
|
|
|
) throws Exception {
|
|
|
|
SseEmitter emitter = new SseEmitter(300000L);
|
|
|
|
|
|
|
|
// 创建日志对象
|
|
|
|
String modelId = "1926875898187878401";
|
|
|
|
AiragLog logRecord = new AiragLog()
|
|
|
|
.setQuestion(questionText)
|
|
|
|
.setCode(code)
|
|
|
|
.setCreateBy(user)
|
|
|
|
.setCodeType(codeType)
|
|
|
|
.setModelId(modelId)
|
|
|
|
.setCreateTime(new Date());
|
|
|
|
|
|
|
|
executor.execute(() -> {
|
|
|
|
try {
|
|
|
|
String knowId = "1926872137990148098";
|
|
|
|
List<QuestionEmbedding> questionEmbeddings = questionEmbeddingService.similaritySearchByQuestion(questionText, 1, 0.8);
|
|
|
|
|
|
|
|
// 如果从问题库中找到匹配
|
|
|
|
if (!questionEmbeddings.isEmpty()) {
|
|
|
|
QuestionEmbedding questionEmbedding = questionEmbeddings.get(0);
|
|
|
|
Map<String, String> data = new HashMap<>();
|
|
|
|
data.put("token", questionEmbedding.getAnswer());
|
|
|
|
emitter.send(SseEmitter.event().data(new ObjectMapper().writeValueAsString(data)));
|
|
|
|
|
|
|
|
// 解析元数据
|
|
|
|
ObjectMapper objectMapper = new ObjectMapper();
|
|
|
|
Map<String, String> metadata = objectMapper.readValue(questionEmbedding.getMetadata(), Map.class);
|
|
|
|
|
|
|
|
// 准备END事件数据
|
|
|
|
Map<String, String> endData = new HashMap<>();
|
|
|
|
endData.put("event", "END");
|
|
|
|
endData.put("similarity", String.valueOf(questionEmbedding.getSimilarity()));
|
|
|
|
if (metadata != null) {
|
|
|
|
String docName = metadata.get("docName");
|
|
|
|
endData.put("fileName", docName);
|
|
|
|
String fileName = generateFilePath(questionEmbedding.getMetadata());
|
|
|
|
if (StringUtils.isNotBlank(fileName)) {
|
|
|
|
endData.put("fileBase64", FileToBase64Util.fileToBase64(uploadPath + fileName));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
emitter.send(SseEmitter.event().data(objectMapper.writeValueAsString(endData)));
|
|
|
|
|
|
|
|
// 记录日志 - 从问题库匹配
|
|
|
|
logRecord.setAnswer(questionEmbedding.getAnswer())
|
|
|
|
.setAnswerType(1);
|
|
|
|
airagLogService.save(logRecord);
|
|
|
|
|
|
|
|
emitter.complete();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 从知识库搜索
|
|
|
|
List<Map<String, Object>> maps = embeddingHandler.searchEmbedding(knowId, questionText, 2, 0.78);
|
|
|
|
if (CollectionUtil.isEmpty(maps)) {
|
|
|
|
Map<String, String> data = new HashMap<>();
|
|
|
|
data.put("token", "该问题未记录在知识库中");
|
|
|
|
emitter.send(SseEmitter.event().data(new ObjectMapper().writeValueAsString(data)));
|
|
|
|
|
|
|
|
// 准备END事件数据
|
|
|
|
Map<String, String> endData = new HashMap<>();
|
|
|
|
endData.put("event", "END");
|
|
|
|
emitter.send(SseEmitter.event().data(new ObjectMapper().writeValueAsString(endData)));
|
|
|
|
|
|
|
|
// 记录日志 - 未命中任何知识库
|
|
|
|
logRecord.setAnswer("该问题未记录在知识库中")
|
|
|
|
.setAnswerType(3)
|
|
|
|
.setIsStorage(0);
|
|
|
|
airagLogService.save(logRecord);
|
|
|
|
|
|
|
|
emitter.complete();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 构建知识库内容
|
|
|
|
StringBuilder content = new StringBuilder();
|
|
|
|
for (Map<String, Object> map : maps) {
|
|
|
|
if (Double.parseDouble(map.get("score").toString()) > 0.78) {
|
|
|
|
content.append(map.get("content").toString()).append("\n");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 获取第一个匹配的元数据用于日志和文件信息
|
|
|
|
Map<String, Object> firstMatch = maps.get(0);
|
|
|
|
String fileName = generateFileDocName(firstMatch.get("metadata").toString());
|
|
|
|
String storedFileName = generateFilePath(firstMatch.get("metadata").toString());
|
|
|
|
|
|
|
|
// 构建问题提示
|
|
|
|
String questin = "你是一个严谨的信息处理助手,请严格按照以下要求处理用户问题:" + questionText + "\n\n" +
|
|
|
|
"处理步骤和要求:\n" +
|
|
|
|
"1. 严格基于参考内容回答,禁止任何超出参考内容的推断或想象\n" +
|
|
|
|
"2. 严格基于参考内容回答,禁止使用参考内容中与问题无关的内容\n" +
|
|
|
|
"3. 回答结构:\n" +
|
|
|
|
" - 首先用一句话直接回答问题核心(仅限参考内容中明确包含的信息)\n" +
|
|
|
|
" - 然后列出支持该答案的具体内容(可直接引用参考内容)\n" +
|
|
|
|
"4. 禁止以下行为:\n" +
|
|
|
|
" - 添加参考内容中不存在的信息\n" +
|
|
|
|
" - 在回答中提及‘参考内容’等字样\n" +
|
|
|
|
" - 在回答中提及其他产品的功能\n" +
|
|
|
|
" - 进行任何推测性陈述\n" +
|
|
|
|
" - 使用模糊或不确定的表达\n" +
|
|
|
|
" - 参考内容为空时应该拒绝回答\n" +
|
|
|
|
"参考内容(请严格限制回答范围于此):\n" + content;
|
|
|
|
|
|
|
|
List<ChatMessage> messages = new ArrayList<>();
|
|
|
|
messages.add(new UserMessage("user", questin));
|
|
|
|
StringBuilder answerBuilder = new StringBuilder();
|
|
|
|
|
|
|
|
TokenStream tokenStream = aiChatHandler.chat(modelId, messages);
|
|
|
|
tokenStream.onNext(token -> {
|
|
|
|
try {
|
|
|
|
answerBuilder.append(token);
|
|
|
|
Map<String, String> data = new HashMap<>();
|
|
|
|
data.put("token", token);
|
|
|
|
emitter.send(SseEmitter.event().data(new ObjectMapper().writeValueAsString(data)));
|
|
|
|
} catch (Exception e) {
|
|
|
|
log.error("发送token失败", e);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
tokenStream.onComplete(response -> {
|
|
|
|
try {
|
|
|
|
// 准备END事件数据
|
|
|
|
Map<String, String> endData = new HashMap<>();
|
|
|
|
endData.put("event", "END");
|
|
|
|
endData.put("similarity", firstMatch.get("score").toString());
|
|
|
|
endData.put("fileName", fileName);
|
|
|
|
endData.put("fileBase64", FileToBase64Util.fileToBase64(uploadPath + storedFileName));
|
|
|
|
emitter.send(SseEmitter.event().data(new ObjectMapper().writeValueAsString(endData)));
|
|
|
|
|
|
|
|
// 记录日志 - 从知识库生成回答
|
|
|
|
logRecord.setAnswer(answerBuilder.toString())
|
|
|
|
.setAnswerType(2);
|
|
|
|
|
|
|
|
System.out.println("回答内容 = " + answerBuilder.toString());
|
|
|
|
airagLogService.save(logRecord);
|
|
|
|
|
|
|
|
emitter.complete();
|
|
|
|
} catch (Exception e) {
|
|
|
|
log.error("流式响应结束时发生错误", e);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
tokenStream.onError(error -> {
|
|
|
|
log.error("生成答案失败", error);
|
|
|
|
// 记录日志 - 错误情况
|
|
|
|
logRecord.setAnswer("生成答案失败: " + error.getMessage())
|
|
|
|
.setAnswerType(4);
|
|
|
|
airagLogService.save(logRecord);
|
|
|
|
emitter.completeWithError(error);
|
|
|
|
});
|
|
|
|
|
|
|
|
tokenStream.start();
|
|
|
|
} catch (Exception e) {
|
|
|
|
log.error("处理请求时发生异常", e);
|
|
|
|
// 记录日志 - 异常情况
|
|
|
|
logRecord.setAnswer("处理请求时发生异常: " + e.getMessage())
|
|
|
|
.setAnswerType(4);
|
|
|
|
airagLogService.save(logRecord);
|
|
|
|
emitter.completeWithError(e);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return emitter;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Operation(summary = "send")
|
|
|
|
@GetMapping("send")
|
|
|
|
public Result<Map<String, Object>> send(String questionText) throws Exception {
|
|
|
|
String knowId = "1926872137990148098";
|
|
|
|
String modelId = "1926875898187878401";
|
|
|
|
Integer topNumber = 1;
|
|
|
|
Double similarity = 0.8;
|
|
|
|
|
|
|
|
// 创建日志对象
|
|
|
|
AiragLog logRecord = new AiragLog()
|
|
|
|
.setQuestion(questionText)
|
|
|
|
.setModelId(modelId)
|
|
|
|
.setCreateTime(new Date());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
HashMap<String, Object> resMap = new HashMap<>();
|
|
|
|
//根据问题相似度进行查询
|
|
|
|
List<QuestionEmbedding> questionEmbeddings = questionEmbeddingService.similaritySearchByQuestion(questionText, 1,0.8);
|
|
|
|
for (QuestionEmbedding questionEmbedding : questionEmbeddings) {
|
|
|
|
resMap.put("question", questionText);
|
|
|
|
resMap.put("answer", questionEmbedding.getAnswer());
|
|
|
|
resMap.put("similarity", questionEmbedding.getSimilarity());
|
|
|
|
|
|
|
|
ObjectMapper objectMapper = new ObjectMapper();
|
|
|
|
Map<String, String> metadata = objectMapper.readValue(questionEmbedding.getMetadata(), Map.class);
|
|
|
|
// 获取docName和docId
|
|
|
|
if (metadata != null) {
|
|
|
|
String docName = metadata.get("docName");
|
|
|
|
resMap.put("fileName", docName);
|
|
|
|
String fileName = generateFilePath(questionEmbedding.getMetadata());
|
|
|
|
|
|
|
|
if (StringUtils.isNotBlank(fileName)) {
|
|
|
|
resMap.put("fileBase64", FileToBase64Util.fileToBase64(uploadPath + fileName));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// 记录日志 - 从问题库匹配
|
|
|
|
logRecord.setAnswer(questionEmbedding.getAnswer());
|
|
|
|
logRecord.setAnswerType(1);
|
|
|
|
airagLogService.save(logRecord);
|
|
|
|
|
|
|
|
log.info("questionEmbedding.getMetadata() = " + questionEmbedding.getMetadata());
|
|
|
|
log.info("questionEmbedding.getQuestion() = " + questionEmbedding.getQuestion());
|
|
|
|
log.info("questionEmbedding.getAnswer() = " + questionEmbedding.getAnswer());
|
|
|
|
log.info("questionEmbedding.getSimilarity() = " + questionEmbedding.getSimilarity());
|
|
|
|
log.info("-------------------------------------------------------------");
|
|
|
|
}
|
|
|
|
//返回问题库命中的问题
|
|
|
|
if (!questionEmbeddings.isEmpty()) {
|
|
|
|
return Result.OK(resMap);
|
|
|
|
}
|
|
|
|
|
|
|
|
List<Map<String, Object>> maps = embeddingHandler.searchEmbedding(knowId, questionText, 3, 0.75);
|
|
|
|
if (CollectionUtil.isEmpty(maps)) {
|
|
|
|
resMap.put("answer", "该问题未记录在知识库中");
|
|
|
|
// 记录日志 - 未命中任何知识库
|
|
|
|
logRecord.setAnswer("该问题未记录在知识库中");
|
|
|
|
logRecord.setAnswerType(3);
|
|
|
|
logRecord.setIsStorage(0);
|
|
|
|
airagLogService.save(logRecord);
|
|
|
|
|
|
|
|
return Result.OK(resMap);
|
|
|
|
}
|
|
|
|
|
|
|
|
StringBuilder content = new StringBuilder();
|
|
|
|
for (Map<String, Object> map : maps) {
|
|
|
|
if (Double.parseDouble(map.get("score").toString()) > 0.78){
|
|
|
|
log.info("score = " + map.get("score").toString());
|
|
|
|
log.info("content = " + map.get("content").toString());
|
|
|
|
content.append(map.get("content").toString()).append("\n");
|
|
|
|
}
|
|
|
|
@RequestParam("user") String user) {
|
|
|
|
return airagResponseService.handleStreamRequest(questionText, code, codeType, user);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
List<ChatMessage> messages = new ArrayList<>();
|
|
|
|
|
|
|
|
String questin = "你是一个严格遵循指令的信息处理助手,请按照以下规范回答用户问题:\n\n" +
|
|
|
|
"# 处理规范\n" +
|
|
|
|
"1. 回答范围:\n" +
|
|
|
|
" - 仅使用提供的参考内容进行回答\n" +
|
|
|
|
" - 禁止任何超出参考内容的推断、想象或补充\n" +
|
|
|
|
" - 当参考内容为空或不相关时,必须拒绝回答\n\n" +
|
|
|
|
"2. 回答结构要求:\n" +
|
|
|
|
" - 首行必须用「回答:」开头,给出最直接的事实性回答\n" +
|
|
|
|
" - 后续每行以「•」开头列出支持证据,每条证据必须:\n" +
|
|
|
|
" * 直接引用参考内容\n" +
|
|
|
|
" * 标注具体出处位置(如段落编号/行号)\n" +
|
|
|
|
" * 保持原句完整性,不得改写\n\n" +
|
|
|
|
"3. 禁止事项:\n" +
|
|
|
|
" - 任何形式的推测(包括\"可能\"、\"应该\"等不确定表述)\n" +
|
|
|
|
" - 回答内容不得提出\"参考内容\"、\"证据\"等字样\n" +
|
|
|
|
" - 参考内容中未明确出现的数字、事实或结论\n" +
|
|
|
|
" - 总结性陈述或观点性表达\n" +
|
|
|
|
" - 多个信息点的合并表述\n\n" +
|
|
|
|
"4. 特殊情形处理:\n" +
|
|
|
|
" - 专业术语必须保持原文表述\n" +
|
|
|
|
" - 数据必须包含原始单位和精度\n\n" +
|
|
|
|
"# 当前任务\n" +
|
|
|
|
"问题:「" + questionText + "」\n\n" +
|
|
|
|
"参考内容(严格限制回答范围):\n" +
|
|
|
|
content;
|
|
|
|
|
|
|
|
messages.add(new UserMessage("user", questin));
|
|
|
|
String chat = aiChatHandler.completions(modelId, messages, null);
|
|
|
|
resMap.put("question", questionText);
|
|
|
|
resMap.put("answer", chat);
|
|
|
|
resMap.put("similarity", maps.get(0).get("score").toString());
|
|
|
|
String fileName = generateFileDocName(maps.get(0).get("metadata").toString());
|
|
|
|
String storedFileName = generateFilePath(maps.get(0).get("metadata").toString());
|
|
|
|
resMap.put("fileName", fileName);
|
|
|
|
resMap.put("fileBase64",FileToBase64Util.fileToBase64(uploadPath + storedFileName));
|
|
|
|
|
|
|
|
|
|
|
|
// 记录日志 - 从知识库生成回答
|
|
|
|
logRecord.setAnswer(chat);
|
|
|
|
logRecord.setAnswerType(2);
|
|
|
|
airagLogService.save(logRecord);
|
|
|
|
|
|
|
|
return Result.OK(resMap);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private String generateFilePath(String metadataJson) throws Exception {
|
|
|
|
if (StringUtils.isEmpty(metadataJson)) {
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
ObjectMapper objectMapper = new ObjectMapper();
|
|
|
|
// 解析JSON字符串
|
|
|
|
Map<String, String> metadata = objectMapper.readValue(metadataJson, Map.class);
|
|
|
|
|
|
|
|
// 获取docName和docId
|
|
|
|
return metadata.get("storedFileName");
|
|
|
|
|
|
|
|
}
|
|
|
|
private String generateFileDocName(String metadataJson) throws Exception {
|
|
|
|
if (StringUtils.isEmpty(metadataJson)) {
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
ObjectMapper objectMapper = new ObjectMapper();
|
|
|
|
// 解析JSON字符串
|
|
|
|
Map<String, String> metadata = objectMapper.readValue(metadataJson, Map.class);
|
|
|
|
|
|
|
|
return metadata.get("docName");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void main(String[] args) {
|
|
|
|
String s = "学生户口复印_efde055d-1207-4b6f-8d46-79eb557ca711.docx";
|
|
|
|
|
|
|
|
String s1 = StringUtils.substringBefore(s, ".");
|
|
|
|
log.info("s1 = " + s1);
|
|
|
|
|
|
|
|
|
|
|
|
String[] split = s.split("_");
|
|
|
|
for (String string : split) {
|
|
|
|
log.info("string = " + string);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} |
|
|
\ No newline at end of file |
...
|
...
|
|