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();