package com.supervision.service.impl; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import com.supervision.dto.roundAsk.EntityQuestionDTO; import com.supervision.dto.roundAsk.SessionParamDTO; import com.supervision.enums.EntityQuestionEnum; import com.supervision.enums.IdentifyIntentEnum; import com.supervision.enums.RetireRoleEnum; import com.supervision.exception.BusinessException; import com.supervision.exception.ItemExtractException; import com.supervision.handler.gpt.AnswerQuestionHandler; import com.supervision.handler.gpt.ConditionJudgeHandler; import com.supervision.handler.gpt.IdentifyIntentHandler; import com.supervision.handler.gpt.ItemExtractHandler; import com.supervision.handler.graph.FindConditionPathHandler; import com.supervision.handler.graph.FindItemDetailHandler; import com.supervision.handler.graph.FindItemNodeHandler; import com.supervision.ngbatis.domain.tag.Condition; import com.supervision.ngbatis.domain.tag.ItemLeaf; import com.supervision.service.AskService; import com.supervision.vo.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; /** * 多轮对话核心业务 */ @Slf4j @Service @RequiredArgsConstructor public class AskServiceImpl implements AskService { private final RedisTemplate redisTemplate; private final IdentifyIntentHandler identifyIntentHandler; private final ItemExtractHandler itemExtractHandler; private final FindItemNodeHandler findItemNodeHandler; private final FindConditionPathHandler findConditionPathHandler; private final ConditionJudgeHandler conditionJudgeHandler; private final FindItemDetailHandler findItemDetailHandler; private final AnswerQuestionHandler answerQuestionHandler; public static final String SESSION_PARAM = "KBQA:ASK:SESSION_PARAM:"; public static final String USER_PARAM = "KBQA:ASK:USER_ROLE:"; /** * 开启多轮对话 * * @param roundTalkReqVO 页面信息 * @return 返回结果 */ @Override public RoundTalkResVO roundTalk(RoundTalkReqVO roundTalkReqVO) { String sessionId = roundTalkReqVO.getSessionId(); // 去Redis中,首先判断session处在哪个阶段,是否有识别的意图 Object cache = redisTemplate.opsForValue().get(SESSION_PARAM + sessionId); SessionParamDTO sessionParamDTO; if (ObjectUtil.isEmpty(cache)) { sessionParamDTO = new SessionParamDTO(); sessionParamDTO.setOriginalQuestion(roundTalkReqVO.getUserTalk()); sessionParamDTO.setSessionId(sessionId); sessionParamDTO.setAlreadyMatchEntitySet(new HashSet<>()); sessionParamDTO.setTalkRecord(new HashMap<>()); redisTemplate.opsForValue().set(SESSION_PARAM + sessionId, sessionParamDTO); } else { sessionParamDTO = BeanUtil.toBean(cache, SessionParamDTO.class); } // 判断意图是否为空,如果意图为空,进行识别意图 if (StrUtil.isBlank(sessionParamDTO.getIntent())) { String intent = identifyIntentHandler.identifyIntent(roundTalkReqVO.getUserTalk()); sessionParamDTO.setIntent(intent); redisTemplate.opsForValue().set(SESSION_PARAM + sessionId, sessionParamDTO); } // 识别出来意图之后,再去判断是否识别过实体 if (CollUtil.isEmpty(sessionParamDTO.getEntityValueByExtract())) { // 识别实体(先从图中获取所有的节点名称,然后识别) List allItemNode = findItemNodeHandler.findAllItemNode(); List extractValue = itemExtractHandler.itemExtractByPossibleItemWithExample(sessionParamDTO.getOriginalQuestion(), allItemNode); // 识别问题中,用户可能问的业务是什么 // String extractValue = itemExtractHandler.itemExtractBusiness(sessionParamDTO.getOriginalQuestion()); sessionParamDTO.setEntityValueByExtract(extractValue); // 根据提取的内容,开始在知识图谱中寻找节点(首先找叶子节点,如果叶子节点有数据,直接返回,如果叶子节点没数据,再去找分支节点) List allMatchLeafNode = findItemNodeHandler.findAllMatchLeafNode(extractValue); // 如果找到的节点只有1个,那么说明问的就是这个节点,那么直接缓存起来进行下一步 if (allMatchLeafNode.size() == 1) { ItemLeaf itemLeaf = allMatchLeafNode.get(0); sessionParamDTO.setMatchItemLeaf(itemLeaf); redisTemplate.opsForValue().set(SESSION_PARAM + sessionId, sessionParamDTO); } else { // 如果不等于1,说明可能有不确定的节点,这时就要开始找节点 Map waitMatchItemLeafMap = allMatchLeafNode.stream().collect(Collectors.toMap(ItemLeaf::getVid, Function.identity(), (k1, k2) -> k1)); // 所有的实体类型以及出现次数计数 Map entityCountMap = new HashMap<>(); // 用来存放所有的节点以及节点路径(key是节点ID,value是节点路径) Map>> conditionPathMap = new HashMap<>(); // 进行遍历.寻找所有的路径 for (String leafId : waitMatchItemLeafMap.keySet()) { List> conditionPath = findConditionPathHandler.findConditionPath(leafId); if (CollUtil.isEmpty(conditionPath)) { waitMatchItemLeafMap.remove(leafId); } else { // 如果路径不为空,则放到缓存中去 conditionPathMap.put(leafId, conditionPath); // 然后根据条件进行计数 for (List conditions : conditionPath) { for (Condition condition : conditions) { // 如果不存在,就添加进计数.如果存在,就+1 entityCountMap.compute(condition.getEntityType(), (k, v) -> v == null ? 1 : v + 1); } } } } // 缓存到Redis中 sessionParamDTO.setWaitMatchItemLeafMap(waitMatchItemLeafMap); sessionParamDTO.setConditionPathMap(conditionPathMap); sessionParamDTO.setEntityCountMap(entityCountMap); redisTemplate.opsForValue().set(SESSION_PARAM + sessionId, sessionParamDTO); } } // 如果判断过实体,这时就要判断是否已经确认了节点,如果没有确认,在这里进行确认 match: if (ObjectUtil.isEmpty(sessionParamDTO.getMatchItemLeaf())) { // 如果没有确定节点,且没有路径可供匹配,抛出异常 if (CollUtil.isEmpty(sessionParamDTO.getConditionPathMap())) { throw new BusinessException("未找到条件判断路径"); } // 判断待匹配的节点是不是只有一个了,如果有多个,就从路径中选择一个问题问前端 if (sessionParamDTO.getWaitMatchItemLeafMap().size() != 1) { if (ObjectUtil.isNotEmpty(sessionParamDTO.getCurrentEntity())) { // 如果当前对话实体不为空,说明当前问答就是上一个问题的回复,这个时候,就去GPT中进行匹配并排除路径 filterPath(sessionParamDTO, roundTalkReqVO.getUserTalk()); // 如果排除后只剩一个了,这时跳出多轮问答 if (sessionParamDTO.getWaitMatchItemLeafMap().size() == 1) { sessionParamDTO.setMatchItemLeaf(sessionParamDTO.getWaitMatchItemLeafMap().values().iterator().next()); redisTemplate.opsForValue().set(SESSION_PARAM + sessionId, sessionParamDTO); break match; } } // 首先获取出现次数最多的实体类型 Map newCountMap = countCondition(sessionParamDTO); String mostFrequentType = newCountMap.entrySet().stream() .filter(entry -> !sessionParamDTO.getAlreadyMatchEntitySet().contains(entry.getKey())) .max(Map.Entry.comparingByValue(Integer::compareTo)) .map(Map.Entry::getKey).orElseThrow(() -> new BusinessException("未找到条件判断路径")); // 获取这个类型对应的问题 String question = EntityQuestionEnum.getQuestionByEntityType(mostFrequentType); Optional.ofNullable(question).orElseThrow(() -> new BusinessException("未找到条件判断路径")); sessionParamDTO.setCurrentEntity(EntityQuestionDTO.builder().currentEntityType(mostFrequentType).currentQuestion(question).build()); redisTemplate.opsForValue().set(SESSION_PARAM + sessionId, sessionParamDTO); // 返回这个问题给前端(触发了多轮问法) return RoundTalkResVO.builder().sessionId(sessionId).replyQuestion(question).build(); } else { // 获取到唯一节点 ItemLeaf itemLeaf = sessionParamDTO.getWaitMatchItemLeafMap().values().stream().findFirst().orElseThrow(() -> new BusinessException("未找到条件判断路径")); sessionParamDTO.setMatchItemLeaf(itemLeaf); } } // 走到这里,说明就只有一个节点了,那么就可以进行下一步了 log.info("走到这里,说明找到了匹配的节点,开始根据用户的意图生成"); return afterMatchReturnAnswer(sessionParamDTO); } /** * 匹配完成之后,返回结果 */ private RoundTalkResVO afterMatchReturnAnswer(SessionParamDTO sessionParamDTO) { String intent = sessionParamDTO.getIntent(); ItemLeaf matchItemLeaf = sessionParamDTO.getMatchItemLeaf(); // 根据用户的意图找到对应的节点 IdentifyIntentEnum intentEnum = IdentifyIntentEnum.getEnumByIntent(intent); if (ObjectUtil.isEmpty(intentEnum) || null == intentEnum) { throw new BusinessException("暂不支持该意图的问答"); } // 根据意图和节点,找到对应的结果 List itemDetail = findItemDetailHandler.findItemDetail(matchItemLeaf.getVid(), intentEnum.getTagType(), intentEnum.getEdgeType()); if (CollUtil.isEmpty(itemDetail)) { throw new BusinessException("未找到意图,暂不支持回答"); } // 提交GPT,问问题的答案 String answer = answerQuestionHandler.answerQuestion(sessionParamDTO.getOriginalQuestion(), itemDetail, sessionParamDTO.getTalkRecord()); if (StrUtil.isBlank(answer)) { throw new BusinessException("未找到答案,暂不支持回答"); } // 清空问话实体 sessionParamDTO.setCurrentEntity(null); redisTemplate.opsForValue().set(SESSION_PARAM + sessionParamDTO.getSessionId(), sessionParamDTO); return RoundTalkResVO.builder().sessionId(sessionParamDTO.getSessionId()).answerText(answer).build(); } private void filterPath(SessionParamDTO sessionParamDTO, String userTalk) { // 遍历所有的path,找到回答 Set possibleAnswerSet = findPossibleAnswerSet(sessionParamDTO); Set judgeResultSet = conditionJudgeHandler.conditionJudgeAll(sessionParamDTO.getCurrentEntity().getCurrentQuestion(), possibleAnswerSet, userTalk); // 筛选路径,如果某个路径的结果不在比较结果中,说明这个结果不对,排除这个路径 pathFilterByJudgeResult(sessionParamDTO.getCurrentEntity().getCurrentEntityType(), judgeResultSet, sessionParamDTO); filterNotMatchNode(sessionParamDTO); // 加到已匹配的实体类型,下次不再匹配 sessionParamDTO.getAlreadyMatchEntitySet().add(sessionParamDTO.getCurrentEntity().getCurrentEntityType()); // 保存用户的对话记录 sessionParamDTO.getTalkRecord().put(sessionParamDTO.getCurrentEntity().getCurrentEntityType(), userTalk); // 缓存到Redis中 redisTemplate.opsForValue().set(SESSION_PARAM + sessionParamDTO.getSessionId(), sessionParamDTO); } private Set findPossibleAnswerSet(SessionParamDTO sessionParamDTO) { Map>> conditionPathMap = sessionParamDTO.getConditionPathMap(); Set possibleAnswerSet = new HashSet<>(); for (Map.Entry>> entry : conditionPathMap.entrySet()) { List> conditionPath = entry.getValue(); // 遍历所有的路径,找到回答 for (List conditions : conditionPath) { // 遍历所有的条件,找到回答 for (Condition condition : conditions) { // 如果当前对话实体和条件中的实体类型相同,就添加到possibleAnswerSet中 if (sessionParamDTO.getCurrentEntity().getCurrentEntityType().equals(condition.getEntityType())) { possibleAnswerSet.add(condition.getCondition()); } } } } return possibleAnswerSet; } private Map countCondition(SessionParamDTO sessionParamDTO) { Map>> conditionPathMap = sessionParamDTO.getConditionPathMap(); // 所有的实体类型以及出现次数计数 Map entityCountMap = new HashMap<>(); for (Map.Entry>> entry : conditionPathMap.entrySet()) { // 然后根据条件进行计数 for (List conditions : entry.getValue()) { for (Condition condition : conditions) { // 如果不存在,就添加进计数.如果存在,就+1 entityCountMap.compute(condition.getEntityType(), (k, v) -> v == null ? 1 : v + 1); } } } sessionParamDTO.setEntityCountMap(entityCountMap); redisTemplate.opsForValue().set(SESSION_PARAM + sessionParamDTO.getSessionId(), sessionParamDTO); return entityCountMap; } /** * 根据比较结果进行路径筛选,把不符合的路径进行移除 */ private void pathFilterByJudgeResult(String entityType, Set judgeResultSet, SessionParamDTO sessionParamDTO) { Map>> conditionPathMap = sessionParamDTO.getConditionPathMap(); for (Map.Entry>> entry : conditionPathMap.entrySet()) { List> conditionPath = entry.getValue(); List> toRemove = new ArrayList<>(); // 遍历所有的路径,找到需要移除的路径 for (List conditions : conditionPath) { // 遍历所有的条件,判断是否需要移除该路径 boolean shouldRemove = false; for (Condition condition : conditions) { if (entityType.equals(condition.getEntityType()) && !judgeResultSet.contains(condition.getCondition())) { shouldRemove = true; break; } } if (shouldRemove) { toRemove.add(conditions); } } // 移除所有需要移除的路径 conditionPath.removeAll(toRemove); } } /** * 过滤掉不匹配的路径 */ private void filterNotMatchNode(SessionParamDTO sessionParamDTO) { Set emptyPathNodeIdSet = new HashSet<>(); Map>> conditionPathMap = sessionParamDTO.getConditionPathMap(); Iterator>>> iterator = conditionPathMap.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry>> entry = iterator.next(); if (CollUtil.isEmpty(entry.getValue())) { emptyPathNodeIdSet.add(entry.getKey()); iterator.remove(); } } Map waitMatchItemLeafMap = sessionParamDTO.getWaitMatchItemLeafMap(); for (String nodeId : emptyPathNodeIdSet) { if (emptyPathNodeIdSet.contains(nodeId)) { waitMatchItemLeafMap.remove(nodeId); } } } @Override public SingleTalkResVO singleTalk(SingleTalkReqVO singleTalkReqVO) { // 单轮对话,直接根据用户的问题去找意图 String intent = identifyIntentHandler.identifyIntent(singleTalkReqVO.getUserTalk()); // 根据用户的意图找到对应的节点 IdentifyIntentEnum intentEnum = IdentifyIntentEnum.getEnumByIntent(intent); if (ObjectUtil.isEmpty(intentEnum) || null == intentEnum) { throw new BusinessException("暂不支持该意图的问答"); } List allItemNode = findItemNodeHandler.findAllItemNode(); List extractValue; try { extractValue = itemExtractHandler.itemExtractByPossibleItemWithExample(singleTalkReqVO.getUserTalk(), allItemNode); }catch (ItemExtractException e){ log.info("为提取到相关实体,暂时还不会回答这个问题"); return SingleTalkResVO.builder().answerText("您好,我暂时还不会回答这个问题哦!").build(); } // String extractValue = itemExtractHandler.itemExtractBusiness(singleTalkReqVO.getUserTalk()); // 根据提取的内容,开始在知识图谱中寻找节点(首先找叶子节点,如果叶子节点有数据,直接返回,如果叶子节点没数据,再去找分支节点) List allMatchLeafNode = findItemNodeHandler.findAllMatchLeafNode(extractValue); // 然后根据叶子节点,还有意图,去找到所有的对应数据 Map> detailMap = new HashMap<>(); for (ItemLeaf itemLeaf : allMatchLeafNode) { try { // 根据意图和节点,找到对应的结果 List itemDetailList = findItemDetailHandler.findItemDetail(itemLeaf.getVid(), intentEnum.getTagType(), intentEnum.getEdgeType()); if (CollUtil.isNotEmpty(itemDetailList)) { detailMap.put(itemLeaf.getItemName(), itemDetailList); } } catch (BusinessException e) { log.info("未找到对应节点,忽略"); return SingleTalkResVO.builder().answerText("您好,我暂时还不会回答这个问题哦!").build(); } } if (CollUtil.isEmpty(detailMap)) { return SingleTalkResVO.builder().answerText("暂时还不会回答这个问题哦!").build(); } // 提交GPT,问问题的答案 String answer = answerQuestionHandler.answerSingleQuestionNew(singleTalkReqVO.getUserTalk(), detailMap); if (StrUtil.isBlank(answer)) { return SingleTalkResVO.builder().answerText("暂时还不会回答这个问题哦!").build(); } return SingleTalkResVO.builder().answerText(answer).build(); } @Override public RoundTalkResVO saveUserParam(RoleSetReqVO paramReqVO) { // 缓存到Redis中 if (CollUtil.isNotEmpty(paramReqVO.getParamMap()) && StrUtil.isNotBlank(paramReqVO.getSessionId())) { redisTemplate.opsForValue().set(USER_PARAM + paramReqVO.getSessionId(), paramReqVO.getParamMap()); // 这里获取session进行筛选,将不匹配的路径移除掉 Object cache = redisTemplate.opsForValue().get(SESSION_PARAM + paramReqVO.getSessionId()); Optional.ofNullable(cache).orElseThrow(() -> new BusinessException("未找到的会话ID")); SessionParamDTO sessionParamDTO = BeanUtil.toBean(cache, SessionParamDTO.class); for (Map.Entry entry : paramReqVO.getParamMap().entrySet()) { String key = entry.getKey(); // 去枚举里面去找 RetireRoleEnum roleEnum = Arrays.stream(RetireRoleEnum.values()).filter(e -> e.getCode().equals(key)).findFirst().orElseThrow(() -> new BusinessException("未找到的参数")); // 如果匹配的实体不为空,就去校验是否存在该实体一致的判断对象 if (null != roleEnum.getEntityEnum()) { Map entityCountMap = sessionParamDTO.getEntityCountMap(); // 如果包含,就去尝试排除路径 if (entityCountMap.containsKey(roleEnum.getEntityEnum().getEntityType())) { filterPath(sessionParamDTO, entry.getValue()); } } } // 在这里,判断是不是只生一个匹配的了,如果只剩一个匹配的了,就直接回复了,不需要再问了 // 如果排除后只剩一个了,这时跳出多轮问答 if (sessionParamDTO.getWaitMatchItemLeafMap().size() == 1) { sessionParamDTO.setMatchItemLeaf(sessionParamDTO.getWaitMatchItemLeafMap().values().iterator().next()); redisTemplate.opsForValue().set(SESSION_PARAM + paramReqVO.getSessionId(), sessionParamDTO); return afterMatchReturnAnswer(sessionParamDTO); } } return null; } @Override public RoleSetResVO queryUserNeedParam(String sessionId) { // 暂时是写死的退休的事项列表,后面再调整优化根据事项修改逻辑 List itemList = Arrays.stream(RetireRoleEnum.values()).map(RetireRoleEnum::getCode).collect(Collectors.toList()); RoleSetResVO roleSetResVO = new RoleSetResVO(); roleSetResVO.setSessionId(sessionId); roleSetResVO.setParam(itemList); return roleSetResVO; } }