lexguard-backend / src /services /graphRagService.js
github-actions[bot]
Deploy to Hugging Face
b921752
Raw
History Blame Contribute Delete
4.68 kB
const graphDriver = require('./graphDriver');
const lawRetrieverService = require('./lawRetrieverService');
class GraphRagService {
/**
* Sanitizes alphanumeric codes to completely nuke Cypher injection vectors
*/
sanitizeIdentifier(input) {
if (typeof input !== 'string') return '';
// Retain clean section labels and numbers (e.g., "27", "Exception 1", "ICA")
return input.replace(/[^a-zA-Z0-9\s-_]/g, '').trim();
}
async retrieveAugmentedContext(contractCategory, clauseType, clauseText, jurisdiction, municipality = '', executionDate = null, logger = null) {
// Step 1: Use the existing high-accuracy Vector Search to find the primary entry point
const vectorHits = await lawRetrieverService.retrieveComplianceContext(
contractCategory,
clauseType,
clauseText,
jurisdiction,
municipality,
executionDate
);
if (!vectorHits || vectorHits.length === 0 || vectorHits === 'No specific statutory framework mapped.') return '';
if (vectorHits.startsWith('Fallback Notice:')) return vectorHits; // Pass through fallback messages
let augmentedContextStr = `=== GRAPH RAG COMPLIANCE CONTEXT ===\n\n`;
augmentedContextStr += vectorHits + '\n\n';
// Parse the Act and Section from the vector hits string
const regex = /AUTHORITATIVE STATUTE:\s(.*?)\s-\sSection\s(.*?)\s?(?:\(Chunk|\n|$)/g;
let match;
const targets = [];
while ((match = regex.exec(vectorHits)) !== null) {
targets.push({
act: this.sanitizeIdentifier(match[1]),
section: this.sanitizeIdentifier(match[2])
});
}
if (targets.length === 0) return vectorHits;
for (const target of targets.slice(0, 2)) { // Limit graph traversal to top 2 primary hits
const cypherQuery = `
MATCH (s:Statute)
WHERE s.act CONTAINS $act AND s.section CONTAINS $section
OPTIONAL MATCH (s)-[r:EXCEPTS|DEFINED_BY]->(dependency:Statute)
OPTIONAL MATCH (precedent:Precedent)-[r2:STARE_DECISIS]->(s)
RETURN
collect(DISTINCT { type: type(r), act: dependency.act, section: dependency.section }) AS dependencies,
collect(DISTINCT { type: type(r2), caseName: precedent.caseName, citation: precedent.citation }) AS precedents
`;
let retries = 2;
let delay = 300; // ms
let success = false;
while (retries >= 0 && !success) {
try {
const result = await graphDriver.read(cypherQuery, { act: target.act, section: target.section });
if (logger) {
const edgeCount = (result.records[0]?.get('dependencies')?.length || 0) +
(result.records[0]?.get('precedents')?.length || 0);
logger.logGraphTraversal(edgeCount, false);
}
if (result.records.length > 0) {
const record = result.records[0];
const dependencies = record.get('dependencies') || [];
const precedents = record.get('precedents') || [];
if (dependencies.length > 0) {
augmentedContextStr += `[Structural Dependencies for ${target.section}]:\n`;
for (const d of dependencies) {
if (d.act) {
augmentedContextStr += `> ${d.type} -> ${d.act}, ${d.section}\n`;
}
}
augmentedContextStr += `\n`;
}
if (precedents.length > 0) {
augmentedContextStr += `[Binding Precedents for ${target.section}]:\n`;
for (const p of precedents) {
if (p.caseName) {
augmentedContextStr += `> ${p.caseName} (${p.citation})\n`;
}
}
augmentedContextStr += `\n`;
}
}
success = true;
} catch (error) {
console.warn(`⚠️ [GraphRAG] Neo4j error encountered for ${target.act} ${target.section}. Retries remaining: ${retries}. Error: ${error.message}`);
if (retries === 0) {
console.error("🚨 Circuit Breaker Tripped: Routing directly to Flat Vector Fallback.");
if (logger) logger.logGraphTraversal(0, true);
// GRACEFUL FALLBACK: Return the standard vector hits unmodified
return vectorHits;
}
// Linear backoff delay
await new Promise(resolve => setTimeout(resolve, delay));
retries--;
delay *= 2;
}
}
}
return augmentedContextStr;
}
}
module.exports = new GraphRagService();