Neo4jGraphRagCypherGenerationService.java 7.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. package com.agent.management.rag.graph;
  2. import com.agent.management.rag.bridge.RagAiBridgeClient;
  3. import com.agent.management.rag.capability.RagCapabilityProfileService;
  4. import com.agent.management.rag.capability.RagSchemaLinker;
  5. import com.agent.management.rag.capability.RagCapabilityProfile;
  6. import com.agent.management.rag.capability.RagSemanticCatalogService;
  7. import com.agent.management.rag.capability.GraphBusinessSubgraphSelector;
  8. import com.agent.management.rag.capability.RagEntityMentionExtractor;
  9. import com.agent.management.rag.memory.RagExampleMemoryService;
  10. import com.agent.management.rag.model.RagQuery;
  11. import com.agent.management.rag.model.RagSourceType;
  12. import lombok.RequiredArgsConstructor;
  13. import org.springframework.stereotype.Service;
  14. import java.util.*;
  15. @Service
  16. @RequiredArgsConstructor
  17. public class Neo4jGraphRagCypherGenerationService implements CypherGenerationService {
  18. private final RagAiBridgeClient bridge;
  19. private final RagCapabilityProfileService profiles;
  20. private final RagSchemaLinker linker;
  21. private final RagSemanticCatalogService semanticCatalog;
  22. private final GraphBusinessSubgraphSelector subgraphs;
  23. private final GraphSchemaValidator validator;
  24. private final RagExampleMemoryService examples;
  25. private final RagEntityMentionExtractor entities;
  26. public Optional<String> generateCypher(RagQuery query, Long graphSourceId) {
  27. if (!allowed(query)) return Optional.empty();
  28. RagCapabilityProfile profile = profiles.get(RagSourceType.GRAPH, String.valueOf(graphSourceId));
  29. GraphSchemaSnapshot actual = profile.graphSchema();
  30. GraphSchemaSnapshot authorized = actual.filter(stringList(query,"allowedLabels"),
  31. stringList(query,"allowedRelationships"), properties(query));
  32. List<Map<String,Object>> promptExamples = new ArrayList<>(maps(query, "examples"));
  33. promptExamples.addAll(examples.findSimilar(RagSourceType.GRAPH, String.valueOf(graphSourceId), query.getQuery(), 3));
  34. List<String> semanticLabels = semanticCatalog.rank(query.getQuery(), profile, "label:", 8);
  35. List<String> semanticRelationships = semanticCatalog.rank(query.getQuery(), profile, "relationship:", 8);
  36. GraphSchemaSnapshot semanticSchema = authorized.nodes().size() <= 12 || (semanticLabels.isEmpty() && semanticRelationships.isEmpty()) ? authorized
  37. : subgraphs.select(query.getQuery(), profile, authorized, 10, 14);
  38. GraphSchemaSnapshot selected = linker.selectGraphSchema(query.getQuery(), semanticSchema, promptExamples);
  39. if (selected.nodes().isEmpty()) throw new IllegalArgumentException("no relevant authorized graph schema is available for Text-to-Cypher");
  40. int maxDepth = integer(query, "maxDepth", 3);
  41. Map<String,Object> request = new LinkedHashMap<>();
  42. request.put("query",query.getQuery()); request.put("graphSourceId",graphSourceId);
  43. request.put("schema",selected.toPromptText()); request.put("examples",normalizeExamples(promptExamples));
  44. request.put("businessRules",generationContext(query)); request.put("allowedLabels",selected.labelNames());
  45. request.put("allowedRelationships",selected.relationshipTypeNames()); request.put("allowedProperties",allowedProperties(selected));
  46. request.put("maxDepth",maxDepth);
  47. request.put("entityMentions", entities.extract(query.getQuery()));
  48. Object cypher = bridge.textToCypher(request).get("cypher");
  49. if(cypher==null||String.valueOf(cypher).isBlank()) return Optional.empty();
  50. String generated=String.valueOf(cypher);
  51. try {
  52. validator.validate(generated,selected,maxDepth);
  53. return Optional.of(generated);
  54. } catch (IllegalArgumentException validationError) {
  55. Object fixed=bridge.repair(Map.of("language","CYPHER","question",query.getQuery(),"query",generated,
  56. "error",validationError.getMessage(),"schemaText",selected.toPromptText(),"maxRows",50,"maxDepth",maxDepth)).get("query");
  57. if(fixed==null||String.valueOf(fixed).isBlank()) throw validationError;
  58. String repaired=String.valueOf(fixed);validator.validate(repaired,selected,maxDepth);return Optional.of(repaired);
  59. }
  60. }
  61. public Optional<String> repair(RagQuery query,Long graphSourceId,String failedCypher,String error){
  62. GraphSchemaSnapshot schema=profiles.get(RagSourceType.GRAPH,String.valueOf(graphSourceId)).graphSchema()
  63. .filter(stringList(query,"allowedLabels"),stringList(query,"allowedRelationships"),properties(query));
  64. int maxDepth=integer(query,"maxDepth",3);
  65. Object fixed=bridge.repair(Map.of("language","CYPHER","question",query.getQuery(),"query",failedCypher,
  66. "error",error,"schemaText",schema.toPromptText(),"maxRows",50,"maxDepth",maxDepth)).get("query");
  67. if(fixed==null||String.valueOf(fixed).isBlank())return Optional.empty();
  68. validator.validate(String.valueOf(fixed),schema,maxDepth);return Optional.of(String.valueOf(fixed));
  69. }
  70. public void validateExplicit(RagQuery query, Long graphSourceId, String cypher) {
  71. GraphSchemaSnapshot authorized = profiles.get(RagSourceType.GRAPH, String.valueOf(graphSourceId)).graphSchema()
  72. .filter(stringList(query, "allowedLabels"), stringList(query, "allowedRelationships"), properties(query));
  73. validator.validate(cypher, authorized, integer(query, "maxDepth", 3));
  74. }
  75. static boolean allowed(RagQuery q){if(q.getFilters()==null)return false;return Boolean.TRUE.equals(q.getFilters().get("allowTextToCypher"))||"AUTO_GENERATE".equals(String.valueOf(q.getFilters().get("retrievalMode")));}
  76. private static int integer(RagQuery q,String key,int d){Object v=q.getFilters().get(key);return v instanceof Number n?n.intValue():d;}
  77. private static String string(RagQuery q,String key){Object value=q.getFilters().get(key);return value==null?"":String.valueOf(value);}
  78. private static String generationContext(RagQuery query){String existing=string(query,"businessRules");List<String> rules=stringList(query,"generationRules");return rules.isEmpty()?existing:existing+(existing.isBlank()?"":"\n")+"Generation rules:\n- "+String.join("\n- ",rules);}
  79. private static List<String> stringList(RagQuery q,String key){Object value=q.getFilters().get(key);return value instanceof List<?> list?list.stream().map(String::valueOf).toList():List.of();}
  80. private static List<Map<String,Object>> maps(RagQuery q,String key){Object value=q.getFilters().get(key);if(!(value instanceof List<?> list))return List.of();return list.stream().filter(Map.class::isInstance).map(item->{Map<?,?> raw=(Map<?,?>)item;Map<String,Object> map=new LinkedHashMap<>();raw.forEach((k,v)->map.put(String.valueOf(k),v));return map;}).toList();}
  81. private static List<Map<String,Object>> normalizeExamples(List<Map<String,Object>> examples){return examples.stream().map(item->item.containsKey("cypher")?item:Map.<String,Object>of("question",item.get("question"),"cypher",item.get("query"))).toList();}
  82. private static Map<String,List<String>> properties(RagQuery q){Object value=q.getFilters().get("allowedProperties");if(!(value instanceof Map<?,?> map))return Map.of();Map<String,List<String>> result=new LinkedHashMap<>();map.forEach((key,raw)->{if(raw instanceof List<?> list)result.put(String.valueOf(key),list.stream().map(String::valueOf).toList());});return result;}
  83. private static Map<String,List<String>> allowedProperties(GraphSchemaSnapshot schema){Map<String,List<String>> result=new LinkedHashMap<>();schema.nodes().forEach(node->result.put(node.label(),new ArrayList<>(node.properties().keySet())));schema.relationships().forEach(rel->result.put(rel.type(),new ArrayList<>(rel.properties().keySet())));return result;}
  84. }