diff --git a/src/main/java/com/supervision/pdfqaserver/cache/PromptCache.java b/src/main/java/com/supervision/pdfqaserver/cache/PromptCache.java index 9ff1499..cb78713 100644 --- a/src/main/java/com/supervision/pdfqaserver/cache/PromptCache.java +++ b/src/main/java/com/supervision/pdfqaserver/cache/PromptCache.java @@ -816,30 +816,39 @@ x {} private static final String TEXT_TO_CYPHER_2_PROMPT = """ - "You are a Cypher‑generating assistant. " - "Your sole reference for generating Cypher scripts is the `neo4j_schema` variable.\\n\\n" - "User question:\\n{question}\\n\\n" - "The schema is defined below in JSON format:\\n" - "{schema}\\n\\n" - "Follow these exact steps for every user query:\\n\\n" - "1. Extract Entities from User Query:\\n" - "- Parse the question for domain concepts and use synonyms or contextual cues to map them to schema elements.\\n" - "- Identify candidate **node types**.\\n" - "- Identify candidate **relationship types**.\\n" - "- Identify relevant **properties**.\\n" - "- Identify **constraints or conditions** (comparisons, flags, temporal filters, shared‑entity references, etc.).\\n\\n" - "2. Validate Against the Schema:\\n" - "- Ensure every node label, relationship type, and property exists in the schema **exactly** (case‑ and character‑sensitive).\\n" - "- If any required element is missing, respond exactly:\\n" - ' \\"I could not generate a Cypher script; the required information is not part of the Neo4j schema.\\"\\n\\n' - "3. Construct the MATCH Pattern:\\n" - "- Use only schema‑validated node labels and relationship types.\\n" - "- Reuse a single variable whenever the query implies that two patterns refer to the same node.\\n" - "- Express simple equality predicates in map patterns and move all other filters to a **WHERE** clause.\\n\\n" - "4. Return Clause Strategy:\\n" - "- RETURN every node and relationship mentioned, unless the user explicitly requests specific properties.\\n\\n" - "5. Final Cypher Script Generation:\\n" - "- Respond with **only** the final Cypher query—no commentary or extra text.\\n" - "- Use OPTIONAL MATCH only if explicitly required by the user and supported by the schema.\\n" + 您是一个生成Cypher查询语句的助手。生成Cypher脚本时,唯一参考的是`neo4j_schema`变量。 + + 用户问题: + {question} + 模式以JSON格式定义如下: + {schema} + 请严格按照以下步骤处理每个用户查询: + + 1. 从用户查询中提取实体: + - 解析问题中的领域概念,并通过同义词或上下文线索将其映射到模式元素 + - 识别候选节点类型 + - 识别候选关系类型 + - 识别相关属性 + - 识别约束条件(比较操作、标志位、时间过滤器、共享实体引用等) + + 2. 验证模式匹配性: + - 确保每个节点标签、关系类型和属性在模式中完全存在(区分大小写和字符) + - 如果缺少任何必需元素,请严格返回: + '\\I could not generate a Cypher script; the required information is not part of the Neo4j schema.\\\\n\\n' + + 3. 构建MATCH模式: + - 仅使用经过模式验证的节点标签和关系类型 + - 当查询暗示两个模式指向同一节点时,重复使用同一变量 + - 在映射模式中表达简单等值谓词,其他过滤条件移至WHERE子句 + + 4. RETURN子句策略: + - 总是返回模式中的节点和关系,使用模式变量(例如`RETURN nodes, relationships`或`RETURN path`) + - 若用户请求特定属性,包含这些属性但同时保留关系数据在RETURN子句中 + - 适当情况下,考虑通过`RETURN path`返回完整路径以保留图结构 + + 5. 生成最终Cypher脚本: + - 仅返回最终Cypher查询语句——不包含任何评论或额外文本 + - 仅当用户显式要求且模式支持时使用OPTIONAL MATCH + - 确保RETURN子句包含关系数据,可通过显式列出关系变量或使用路径变量实现 """; } diff --git a/src/main/java/com/supervision/pdfqaserver/service/DeepSeekApiImpl.java b/src/main/java/com/supervision/pdfqaserver/service/DeepSeekApiImpl.java index a2cf87b..d54ba18 100644 --- a/src/main/java/com/supervision/pdfqaserver/service/DeepSeekApiImpl.java +++ b/src/main/java/com/supervision/pdfqaserver/service/DeepSeekApiImpl.java @@ -8,7 +8,7 @@ import org.springframework.ai.openai.OpenAiChatModel; import reactor.core.publisher.Flux; import org.springframework.stereotype.Service; @Slf4j -@Service +//@Service @RequiredArgsConstructor public class DeepSeekApiImpl implements AiCallService { private final OpenAiChatModel ollamaChatModel; diff --git a/src/main/java/com/supervision/pdfqaserver/service/impl/ChatServiceImpl.java b/src/main/java/com/supervision/pdfqaserver/service/impl/ChatServiceImpl.java index 24202eb..6b1f7eb 100644 --- a/src/main/java/com/supervision/pdfqaserver/service/impl/ChatServiceImpl.java +++ b/src/main/java/com/supervision/pdfqaserver/service/impl/ChatServiceImpl.java @@ -5,20 +5,16 @@ import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.supervision.pdfqaserver.cache.PromptCache; -import com.supervision.pdfqaserver.dao.Neo4jRepository; import com.supervision.pdfqaserver.domain.DocumentTruncation; -import com.supervision.pdfqaserver.domain.Intention; import com.supervision.pdfqaserver.dto.AnswerDetailDTO; -import com.supervision.pdfqaserver.dto.DomainMetadataDTO; -import com.supervision.pdfqaserver.dto.neo4j.RelationObject; +import com.supervision.pdfqaserver.dto.neo4j.NodeDTO; +import com.supervision.pdfqaserver.dto.neo4j.RelationshipValueDTO; import com.supervision.pdfqaserver.service.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.SystemPromptTemplate; -import org.springframework.ai.ollama.OllamaChatModel; -import org.springframework.ai.ollama.api.OllamaOptions; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; import java.util.ArrayList; @@ -40,9 +36,6 @@ public class ChatServiceImpl implements ChatService { private static final String CYPHER_QUERIES = "cypherQueries"; - private final OllamaChatModel ollamaChatModel; - - private final AiCallService aiCallService; private final DocumentTruncationService documentTruncationService; @@ -72,27 +65,102 @@ public class ChatServiceImpl implements ChatService { log.info("生成回答的提示词:{}", generateAnswerMessage); return aiCallService.stream(new Prompt(generateAnswerMessage)) .map(response -> response.getResult().getOutput().getText()) - .concatWith(Flux.just(new JSONObject().set("answerDetails", convertToAnswerDetails(null)).toString())) + .concatWith(Flux.just(new JSONObject().set("answerDetails", convertToAnswerDetails(graphResult)).toString())) .concatWith(Flux.just("[END]")); } - private List convertToAnswerDetails(List relationObjects) { - if (CollUtil.isEmpty(relationObjects)) { + private List convertToAnswerDetails(List> graphResult) { + if (CollUtil.isEmpty(graphResult)){ return new ArrayList<>(); } - List answerDetailDTOList = relationObjects.stream().map(AnswerDetailDTO::new).collect(Collectors.toList()); - if (CollUtil.isNotEmpty(answerDetailDTOList)){ - List truncateIds = answerDetailDTOList.stream().map(AnswerDetailDTO::getTruncateId).distinct().toList(); + List answerDetailDTOS = new ArrayList<>(); + for (Map map : graphResult) { + Long start = null; + Long end = null; + for (Map.Entry entry : map.entrySet()) { + // 先找到头节点和尾节点id + if (entry.getValue() instanceof RelationshipValueDTO value){ + start = value.getStart(); + end = value.getEnd(); + break; + } + } + AnswerDetailDTO answerDetailDTO = new AnswerDetailDTO(); + if (null == start) { + // 没有关系类型 + for (Map.Entry entry : map.entrySet()) { + // 处理头节点 + if(entry.getValue() instanceof NodeDTO nodeDTO){ + Map properties = nodeDTO.getProperties(); + if (StrUtil.isEmpty(answerDetailDTO.getSourceType())){ + answerDetailDTO.setSourceName((String) properties.get("name")); + answerDetailDTO.setSourceType(CollUtil.getFirst(nodeDTO.getLabels())); // 假设第一个标签是源类型 + // 设置truncationId属性 + answerDetailDTO.setTruncateId((String) properties.get("truncationId")); + }else { + answerDetailDTO.setTargetName((String) properties.get("name")); + answerDetailDTO.setTargetType(CollUtil.getFirst(nodeDTO.getLabels())); // 假设第一个标签是目标类型 + } + } + } + answerDetailDTOS.add(answerDetailDTO); + }else { + // 有关系节点 + for (Map.Entry entry : map.entrySet()) { + // 处理头节点 + if(entry.getValue() instanceof NodeDTO nodeDTO){ + if (start.equals(nodeDTO.getId())){ + Map properties = nodeDTO.getProperties(); + answerDetailDTO.setSourceName((String) properties.get("name")); + answerDetailDTO.setSourceType(CollUtil.getFirst(nodeDTO.getLabels())); // 假设第一个标签是源类型 + // 设置truncationId属性 + answerDetailDTO.setTruncateId((String) properties.get("truncationId")); + } + if (end.equals(nodeDTO.getId())){ + Map properties = nodeDTO.getProperties(); + answerDetailDTO.setTargetName((String) properties.get("name")); + answerDetailDTO.setTargetType(CollUtil.getFirst(nodeDTO.getLabels())); // 假设第一个标签是目标类型 + } + } + + if (entry.getValue() instanceof RelationshipValueDTO value) { + // 处理关系 + if (start.equals(value.getStart()) || end.equals(value.getEnd())) { + answerDetailDTO.setRelation(value.getType()); + } + } + } + answerDetailDTOS.add(answerDetailDTO); + } + } + List distinct = new ArrayList<>(); + if (CollUtil.isNotEmpty(answerDetailDTOS)){ + //去重answerDetailDTOS + for (AnswerDetailDTO answerDetailDTO : answerDetailDTOS) { + boolean noned = distinct.stream().noneMatch(i -> + StrUtil.equals(i.getSourceName(), answerDetailDTO.getSourceName()) && + StrUtil.equals(i.getTargetName(), answerDetailDTO.getTargetName()) && + StrUtil.equals(i.getRelation(), answerDetailDTO.getRelation()) && + StrUtil.equals(i.getSourceType(), answerDetailDTO.getSourceType()) && + StrUtil.equals(i.getTargetType(), answerDetailDTO.getTargetType()) && + StrUtil.equals(i.getTruncateId(), answerDetailDTO.getTruncateId()) + ); + if (noned){ + distinct.add(answerDetailDTO); + } + } + + List truncateIds = distinct.stream().map(AnswerDetailDTO::getTruncateId).distinct().toList(); if (CollUtil.isEmpty(truncateIds)){ - return answerDetailDTOList; + return answerDetailDTOS; } List documentTruncations = documentTruncationService.listByIds(truncateIds); Map contentMap = documentTruncations.stream().collect(Collectors.toMap(DocumentTruncation::getId, DocumentTruncation::getContent)); - for (AnswerDetailDTO answerDetailDTO : answerDetailDTOList) { + for (AnswerDetailDTO answerDetailDTO : distinct) { answerDetailDTO.setTruncateContent(contentMap.get(answerDetailDTO.getTruncateId())); } } - return answerDetailDTOList; + return distinct; } } diff --git a/src/main/java/com/supervision/pdfqaserver/service/impl/DomainMetadataServiceImpl.java b/src/main/java/com/supervision/pdfqaserver/service/impl/DomainMetadataServiceImpl.java index 65fefff..4f241c3 100644 --- a/src/main/java/com/supervision/pdfqaserver/service/impl/DomainMetadataServiceImpl.java +++ b/src/main/java/com/supervision/pdfqaserver/service/impl/DomainMetadataServiceImpl.java @@ -91,14 +91,22 @@ public class DomainMetadataServiceImpl extends ServiceImpl nodeAttributes = metadata.getSourceAttributes(); - nodeAttributes.addAll(metadata.getTargetAttributes()); - if (CollUtil.isNotEmpty(nodeAttributes)){ - for (ERAttributeDTO nodeAttribute : nodeAttributes) { - nodeAttribute.setDomainMetadataId(metadata.getId()); - nodeAttribute.setErType("1"); - nodeAttribute.setErLabel(nodeAttribute.getAttrName()); - erAttributeService.saveIfAbsents(nodeAttribute.toErAttribute(), metadata.getId()); + List sourceAttributes = metadata.getSourceAttributes(); + if (CollUtil.isNotEmpty(sourceAttributes)){ + for (ERAttributeDTO sourceAttribute : sourceAttributes) { + sourceAttribute.setDomainMetadataId(metadata.getId()); + sourceAttribute.setErType("1"); + sourceAttribute.setErLabel(metadata.getSourceType()); + erAttributeService.saveIfAbsents(sourceAttribute.toErAttribute(), metadata.getId()); + } + } + List targetAttributes = metadata.getTargetAttributes(); + if (CollUtil.isNotEmpty(targetAttributes)){ + for (ERAttributeDTO targetAttribute : targetAttributes) { + targetAttribute.setDomainMetadataId(metadata.getId()); + targetAttribute.setErType("1"); + targetAttribute.setErLabel(metadata.getTargetType()); + erAttributeService.saveIfAbsents(targetAttribute.toErAttribute(), metadata.getId()); } } } diff --git a/src/main/java/com/supervision/pdfqaserver/service/impl/KnowledgeGraphServiceImpl.java b/src/main/java/com/supervision/pdfqaserver/service/impl/KnowledgeGraphServiceImpl.java index 9f18a31..82ba45b 100644 --- a/src/main/java/com/supervision/pdfqaserver/service/impl/KnowledgeGraphServiceImpl.java +++ b/src/main/java/com/supervision/pdfqaserver/service/impl/KnowledgeGraphServiceImpl.java @@ -101,7 +101,7 @@ public class KnowledgeGraphServiceImpl implements KnowledgeGraphService { log.error("pdfId:{}元数据训练失败...", pdfId, e); pdfInfoService.pdfTrainFail(pdfId); } - log.info("pdfId:{}元数据训练失败,耗时:{}秒", pdfId, timer.intervalSecond()); + log.error("pdfId:{}元数据训练失败,耗时:{}秒", pdfId, timer.intervalSecond(),e); } } diff --git a/src/main/java/com/supervision/pdfqaserver/service/impl/TripleToCypherExecutorImpl.java b/src/main/java/com/supervision/pdfqaserver/service/impl/TripleToCypherExecutorImpl.java index 5c10579..3e960a4 100644 --- a/src/main/java/com/supervision/pdfqaserver/service/impl/TripleToCypherExecutorImpl.java +++ b/src/main/java/com/supervision/pdfqaserver/service/impl/TripleToCypherExecutorImpl.java @@ -65,7 +65,7 @@ public class TripleToCypherExecutorImpl implements TripleToCypherExecutor { String prompt = promptMap.get(TEXT_TO_CYPHER_2); String format = StrUtil.format(prompt, Map.of("question", query, "schema", schemaDTO.format())); String call = aiCallService.call(format); - if (StrUtil.equals(call,"I could not generate a Cypher script; the required information is not part of the Neo4j schema.")){ + if (StrUtil.contains(call,"I could not generate a Cypher script; the required information is not part of the Neo4j schema.")){ log.info("大模型没能生成cypher,query: {}", query); return null; } @@ -133,7 +133,7 @@ public class TripleToCypherExecutorImpl implements TripleToCypherExecutor { continue; } Map attributes = entity.getAttributes().stream().collect(Collectors.toMap( - TruncationERAttributeDTO::getAttributeEn, TruncationERAttributeDTO::getValue + TruncationERAttributeDTO::getAttributeEn, TruncationERAttributeDTO::getValue, (v1, v2) -> v1 )); attributes.put("truncationId", entity.getTruncationId()); attributes.put("name", entity.getName()); diff --git a/src/test/java/com/supervision/pdfqaserver/PdfQaServerApplicationTests.java b/src/test/java/com/supervision/pdfqaserver/PdfQaServerApplicationTests.java index d782961..382f7f0 100644 --- a/src/test/java/com/supervision/pdfqaserver/PdfQaServerApplicationTests.java +++ b/src/test/java/com/supervision/pdfqaserver/PdfQaServerApplicationTests.java @@ -33,7 +33,7 @@ class PdfQaServerApplicationTests { @Test void testGenerateGraph2() { - List eredtos = knowledgeGraphService.listPdfEREDTO("16"); + List eredtos = knowledgeGraphService.listPdfEREDTO("15"); knowledgeGraphService.generateGraphSimple(eredtos); log.info("finish...");