cozo-memory 1.1.4 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,552 @@
1
+ "use strict";
2
+ /**
3
+ * Explainable Retrieval Service
4
+ *
5
+ * Provides detailed reasoning paths and explanations for retrieval results.
6
+ * Inspired by GraphRAG explainability patterns and reasoning trace research (2025-2026).
7
+ *
8
+ * Features:
9
+ * - Full path visualization for graph traversals
10
+ * - Detailed reasoning chains for hybrid search
11
+ * - Step-by-step explanation of score calculations
12
+ * - Human-readable path descriptions
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.ExplainableRetrievalService = void 0;
16
+ // ============================================================================
17
+ // Explainable Retrieval Service
18
+ // ============================================================================
19
+ class ExplainableRetrievalService {
20
+ db;
21
+ embeddingService;
22
+ constructor(db, embeddingService) {
23
+ this.db = db;
24
+ this.embeddingService = embeddingService;
25
+ }
26
+ /**
27
+ * Enhances search results with detailed explanations
28
+ */
29
+ async explainResults(results, query, searchType, options) {
30
+ const includePathViz = options?.includePathVisualization ?? true;
31
+ const includeSteps = options?.includeReasoningSteps ?? true;
32
+ const includeBreakdown = options?.includeScoreBreakdown ?? true;
33
+ const explainableResults = [];
34
+ for (const result of results) {
35
+ const explanation = await this.generateExplanation(result, query, searchType, { includePathViz, includeSteps, includeBreakdown });
36
+ explainableResults.push({
37
+ id: result.id,
38
+ entity_id: result.entity_id || result.id,
39
+ name: result.name,
40
+ type: result.type,
41
+ score: result.score,
42
+ source: result.source,
43
+ metadata: result.metadata,
44
+ explanation
45
+ });
46
+ }
47
+ return explainableResults;
48
+ }
49
+ /**
50
+ * Generates detailed explanation for a single result
51
+ */
52
+ async generateExplanation(result, query, searchType, options) {
53
+ let steps = [];
54
+ let pathVisualization;
55
+ let summary = '';
56
+ let reasoning = '';
57
+ // Generate explanation based on search type
58
+ switch (searchType) {
59
+ case 'graph_rag':
60
+ ({ summary, reasoning, steps, pathVisualization } = await this.explainGraphRag(result, query, options));
61
+ break;
62
+ case 'multi_hop':
63
+ ({ summary, reasoning, steps, pathVisualization } = await this.explainMultiHop(result, query, options));
64
+ break;
65
+ case 'dynamic_fusion':
66
+ ({ summary, reasoning, steps } = await this.explainDynamicFusion(result, query, options));
67
+ break;
68
+ case 'hybrid':
69
+ default:
70
+ ({ summary, reasoning, steps } = await this.explainHybridSearch(result, query, options));
71
+ break;
72
+ }
73
+ // Generate score breakdown
74
+ const scoreBreakdown = this.generateScoreBreakdown(result, searchType);
75
+ // Extract sources
76
+ const sources = this.extractSources(result);
77
+ // Calculate confidence
78
+ const confidence = this.calculateConfidence(result, scoreBreakdown);
79
+ return {
80
+ summary,
81
+ reasoning,
82
+ steps: options.includeSteps ? steps : [],
83
+ pathVisualization: options.includePathViz ? pathVisualization : undefined,
84
+ scoreBreakdown: options.includeBreakdown ? scoreBreakdown : this.getMinimalScoreBreakdown(result),
85
+ confidence,
86
+ sources
87
+ };
88
+ }
89
+ /**
90
+ * Explains Graph-RAG results with path visualization
91
+ */
92
+ async explainGraphRag(result, query, options) {
93
+ const steps = [];
94
+ // Step 1: Vector seed discovery
95
+ steps.push({
96
+ step: 1,
97
+ operation: 'Vector Seed Discovery',
98
+ description: `Performed semantic search to find initial pivot entities related to "${query}"`,
99
+ score: result.pathScores?.vector || result.score,
100
+ details: {
101
+ method: 'HNSW vector search',
102
+ embedding_model: 'Xenova/bge-m3',
103
+ topK: 10
104
+ }
105
+ });
106
+ // Step 2: Graph expansion
107
+ const depth = result.metadata?.depth || result.depth || 2;
108
+ steps.push({
109
+ step: 2,
110
+ operation: 'Graph Expansion',
111
+ description: `Explored graph relationships up to ${depth} hops from seed entities`,
112
+ details: {
113
+ maxDepth: depth,
114
+ traversalType: 'bidirectional',
115
+ relationshipTypes: 'all'
116
+ }
117
+ });
118
+ // Step 3: Entity discovery
119
+ steps.push({
120
+ step: 3,
121
+ operation: 'Entity Discovery',
122
+ description: `Found "${result.name}" (${result.type}) through graph traversal`,
123
+ score: result.score,
124
+ details: {
125
+ entityId: result.id,
126
+ entityType: result.type,
127
+ pageRank: result.metadata?.pagerank || 0
128
+ }
129
+ });
130
+ // Step 4: Score calculation
131
+ steps.push({
132
+ step: 4,
133
+ operation: 'Score Calculation',
134
+ description: 'Combined vector similarity, graph distance, and PageRank scores',
135
+ score: result.score,
136
+ details: {
137
+ formula: 'seed_score * (1.0 - 0.2 * depth) * (1.0 + pagerank)',
138
+ components: {
139
+ seedScore: result.pathScores?.vector || 0.8,
140
+ depthPenalty: 0.2 * depth,
141
+ pageRankBoost: result.metadata?.pagerank || 0
142
+ }
143
+ }
144
+ });
145
+ // Generate path visualization
146
+ let pathVisualization;
147
+ if (options.includePathViz) {
148
+ pathVisualization = await this.generateGraphPath(result, query);
149
+ }
150
+ const summary = `Found via graph expansion from semantic seed (${depth} hops)`;
151
+ const reasoning = `Started with semantic search for "${query}", then explored graph relationships up to ${depth} hops. ` +
152
+ `Discovered "${result.name}" through ${pathVisualization?.nodes.length || 'multiple'} intermediate entities. ` +
153
+ `Final score combines vector similarity (${(result.pathScores?.vector || 0.8).toFixed(2)}), ` +
154
+ `graph distance penalty (-${(0.2 * depth).toFixed(2)}), and PageRank boost (+${(result.metadata?.pagerank || 0).toFixed(2)}).`;
155
+ return { summary, reasoning, steps, pathVisualization };
156
+ }
157
+ /**
158
+ * Explains Multi-Hop reasoning results
159
+ */
160
+ async explainMultiHop(result, query, options) {
161
+ const steps = [];
162
+ // Step 1: Vector pivot discovery
163
+ steps.push({
164
+ step: 1,
165
+ operation: 'Vector Pivot Discovery',
166
+ description: `Identified semantic pivot points related to "${query}"`,
167
+ score: result.pivotSimilarity || 0.85,
168
+ details: {
169
+ method: 'HNSW vector search',
170
+ pivotCount: result.pivotCount || 3
171
+ }
172
+ });
173
+ // Step 2: Logic-aware traversal
174
+ steps.push({
175
+ step: 2,
176
+ operation: 'Logic-Aware Traversal',
177
+ description: 'Explored graph with relationship context and PageRank weighting',
178
+ details: {
179
+ maxHops: result.maxHops || 3,
180
+ relationshipTypes: result.relationshipTypes || 'all',
181
+ pageRankWeighted: true
182
+ }
183
+ });
184
+ // Step 3: Helpfulness pruning
185
+ steps.push({
186
+ step: 3,
187
+ operation: 'Helpfulness Pruning',
188
+ description: 'Filtered paths by semantic relevance and logical importance',
189
+ score: result.helpfulness || 0.75,
190
+ details: {
191
+ semanticWeight: 0.6,
192
+ logicalWeight: 0.4,
193
+ threshold: 0.5
194
+ }
195
+ });
196
+ // Step 4: Path aggregation
197
+ steps.push({
198
+ step: 4,
199
+ operation: 'Path Aggregation',
200
+ description: `Aggregated ${result.occurrences || 1} path(s) leading to "${result.name}"`,
201
+ score: result.score,
202
+ details: {
203
+ occurrences: result.occurrences || 1,
204
+ avgScore: result.avg_score || result.score,
205
+ minDepth: result.min_depth || result.depth || 1
206
+ }
207
+ });
208
+ // Generate path visualization
209
+ let pathVisualization;
210
+ if (options.includePathViz && result.path) {
211
+ pathVisualization = this.visualizeMultiHopPath(result.path, query);
212
+ }
213
+ const summary = `Found via ${result.occurrences || 1} multi-hop reasoning path(s)`;
214
+ const reasoning = `Used vector pivots as springboard for logic-aware graph traversal. ` +
215
+ `Discovered "${result.name}" through ${result.min_depth || 1}-hop path(s) with helpfulness score ${(result.helpfulness || 0.75).toFixed(2)}. ` +
216
+ `Path aggregation combined ${result.occurrences || 1} occurrence(s) with confidence ${(result.confidence || 0.8).toFixed(2)}.`;
217
+ return { summary, reasoning, steps, pathVisualization };
218
+ }
219
+ /**
220
+ * Explains Dynamic Fusion results
221
+ */
222
+ explainDynamicFusion(result, query, options) {
223
+ const steps = [];
224
+ const pathScores = result.pathScores || {};
225
+ const sources = (result.source || '').split(',').map((s) => s.trim());
226
+ // Step 1: Multi-path retrieval
227
+ const activePaths = sources.filter((s) => s && s !== 'unknown');
228
+ steps.push({
229
+ step: 1,
230
+ operation: 'Multi-Path Retrieval',
231
+ description: `Executed ${activePaths.length} retrieval path(s): ${activePaths.join(', ')}`,
232
+ details: {
233
+ paths: activePaths,
234
+ pathScores: pathScores
235
+ }
236
+ });
237
+ // Step 2: Individual path scores
238
+ let stepNum = 2;
239
+ if (pathScores.vector !== undefined) {
240
+ steps.push({
241
+ step: stepNum++,
242
+ operation: 'Dense Vector Search',
243
+ description: 'Semantic similarity via HNSW index',
244
+ score: pathScores.vector,
245
+ details: { method: 'HNSW', model: 'Xenova/bge-m3' }
246
+ });
247
+ }
248
+ if (pathScores.sparse !== undefined) {
249
+ steps.push({
250
+ step: stepNum++,
251
+ operation: 'Sparse Vector Search',
252
+ description: 'Keyword-based TF-IDF matching',
253
+ score: pathScores.sparse,
254
+ details: { method: 'TF-IDF' }
255
+ });
256
+ }
257
+ if (pathScores.fts !== undefined) {
258
+ steps.push({
259
+ step: stepNum++,
260
+ operation: 'Full-Text Search',
261
+ description: 'BM25 scoring on entity names',
262
+ score: pathScores.fts,
263
+ details: { method: 'BM25' }
264
+ });
265
+ }
266
+ if (pathScores.graph !== undefined) {
267
+ steps.push({
268
+ step: stepNum++,
269
+ operation: 'Graph Traversal',
270
+ description: 'Multi-hop relationship expansion',
271
+ score: pathScores.graph,
272
+ details: { method: 'BFS', maxDepth: 2 }
273
+ });
274
+ }
275
+ // Step 3: Fusion
276
+ steps.push({
277
+ step: stepNum++,
278
+ operation: 'Score Fusion',
279
+ description: 'Combined path scores using Reciprocal Rank Fusion (RRF)',
280
+ score: result.score,
281
+ details: {
282
+ strategy: 'RRF',
283
+ k: 60,
284
+ contributingPaths: activePaths.length
285
+ }
286
+ });
287
+ const summary = `Found via ${activePaths.length}-path fusion (${activePaths.join(', ')})`;
288
+ const pathScoreStr = Object.entries(pathScores)
289
+ .map(([path, score]) => `${path}:${score.toFixed(2)}`)
290
+ .join(', ');
291
+ const reasoning = `Executed parallel retrieval across ${activePaths.length} path(s). ` +
292
+ `Individual scores: ${pathScoreStr}. ` +
293
+ `Combined using RRF fusion to produce final score ${result.score.toFixed(2)}.`;
294
+ return { summary, reasoning, steps };
295
+ }
296
+ /**
297
+ * Explains Hybrid Search results
298
+ */
299
+ explainHybridSearch(result, query, options) {
300
+ const steps = [];
301
+ const source = result.source || 'unknown';
302
+ // Determine primary retrieval method
303
+ steps.push({
304
+ step: 1,
305
+ operation: 'Primary Retrieval',
306
+ description: `Retrieved via ${source} search`,
307
+ score: result.score,
308
+ details: {
309
+ method: source,
310
+ query: query
311
+ }
312
+ });
313
+ // Additional signals
314
+ let stepNum = 2;
315
+ if (result.metadata?.pagerank) {
316
+ steps.push({
317
+ step: stepNum++,
318
+ operation: 'PageRank Boost',
319
+ description: 'Applied entity importance weighting',
320
+ score: result.metadata.pagerank,
321
+ details: { pagerank: result.metadata.pagerank }
322
+ });
323
+ }
324
+ if (result.metadata?.community_id) {
325
+ steps.push({
326
+ step: stepNum++,
327
+ operation: 'Community Expansion',
328
+ description: 'Boosted by community membership',
329
+ details: { communityId: result.metadata.community_id }
330
+ });
331
+ }
332
+ // Temporal decay
333
+ if (result.created_at) {
334
+ const age = Date.now() - result.created_at;
335
+ const ageDays = Math.floor(age / (1000 * 60 * 60 * 24));
336
+ steps.push({
337
+ step: stepNum++,
338
+ operation: 'Temporal Decay',
339
+ description: `Applied time-based score adjustment (age: ${ageDays} days)`,
340
+ details: {
341
+ ageDays,
342
+ halfLife: 90,
343
+ decayFactor: Math.exp(-Math.log(2) * ageDays / 90)
344
+ }
345
+ });
346
+ }
347
+ const summary = `Found via ${source} search`;
348
+ const reasoning = `Retrieved "${result.name}" using ${source} search with score ${result.score.toFixed(2)}. ` +
349
+ (result.metadata?.pagerank ? `Boosted by PageRank (${result.metadata.pagerank.toFixed(2)}). ` : '') +
350
+ (result.created_at ? `Adjusted for recency (${Math.floor((Date.now() - result.created_at) / (1000 * 60 * 60 * 24))} days old). ` : '');
351
+ return { summary, reasoning, steps };
352
+ }
353
+ /**
354
+ * Generates graph path visualization
355
+ */
356
+ async generateGraphPath(result, query) {
357
+ // Try to reconstruct path from result metadata
358
+ const nodes = [];
359
+ const edges = [];
360
+ // Add query as starting node
361
+ nodes.push({
362
+ id: 'query',
363
+ name: query,
364
+ type: 'Query',
365
+ position: 0,
366
+ score: 1.0
367
+ });
368
+ // Try to get path from database if entity_id is available
369
+ if (result.entity_id || result.id) {
370
+ const entityId = result.entity_id || result.id;
371
+ // Query for shortest path from any seed to this entity
372
+ try {
373
+ const pathQuery = `
374
+ ?[path_nodes, path_edges] :=
375
+ *entity{id: $entity_id, name, type, @ "NOW"},
376
+ path_nodes = [name],
377
+ path_edges = []
378
+ `;
379
+ const pathResult = await this.db.run(pathQuery, { entity_id: entityId });
380
+ if (pathResult.rows.length > 0) {
381
+ // Add intermediate nodes if available
382
+ // This is a simplified version - in production, you'd reconstruct the full path
383
+ nodes.push({
384
+ id: entityId,
385
+ name: result.name,
386
+ type: result.type,
387
+ position: 1,
388
+ score: result.score,
389
+ metadata: result.metadata
390
+ });
391
+ edges.push({
392
+ from: 'query',
393
+ to: entityId,
394
+ relationshipType: 'semantic_match',
395
+ strength: result.score,
396
+ label: `semantic:${result.score.toFixed(2)}`
397
+ });
398
+ }
399
+ }
400
+ catch (e) {
401
+ console.error('[ExplainableRetrieval] Error reconstructing path:', e);
402
+ }
403
+ }
404
+ // Generate textual visualization
405
+ const textual = this.generateTextualPath(nodes, edges);
406
+ return {
407
+ textual,
408
+ nodes,
409
+ edges,
410
+ totalHops: nodes.length - 1,
411
+ confidence: result.confidence || result.score || 0.8
412
+ };
413
+ }
414
+ /**
415
+ * Visualizes multi-hop path
416
+ */
417
+ visualizeMultiHopPath(path, query) {
418
+ const nodes = [];
419
+ const edges = [];
420
+ // Add query node
421
+ nodes.push({
422
+ id: 'query',
423
+ name: query,
424
+ type: 'Query',
425
+ position: 0,
426
+ score: 1.0
427
+ });
428
+ // Add path nodes
429
+ path.forEach((node, index) => {
430
+ nodes.push({
431
+ id: node.id,
432
+ name: node.name,
433
+ type: node.type,
434
+ position: index + 1,
435
+ score: node.confidence,
436
+ metadata: node.metadata
437
+ });
438
+ // Add edge
439
+ const fromId = index === 0 ? 'query' : path[index - 1].id;
440
+ edges.push({
441
+ from: fromId,
442
+ to: node.id,
443
+ relationshipType: node.relationshipType || 'related_to',
444
+ strength: node.relationshipStrength || node.confidence || 0.8,
445
+ label: `${node.relationshipType || 'related'}:${(node.relationshipStrength || node.confidence || 0.8).toFixed(2)}`
446
+ });
447
+ });
448
+ const textual = this.generateTextualPath(nodes, edges);
449
+ return {
450
+ textual,
451
+ nodes,
452
+ edges,
453
+ totalHops: nodes.length - 1,
454
+ confidence: path[path.length - 1]?.confidence || 0.8
455
+ };
456
+ }
457
+ /**
458
+ * Generates textual path representation
459
+ */
460
+ generateTextualPath(nodes, edges) {
461
+ if (nodes.length === 0)
462
+ return '';
463
+ if (nodes.length === 1)
464
+ return nodes[0].name;
465
+ const parts = [];
466
+ for (let i = 0; i < nodes.length; i++) {
467
+ parts.push(nodes[i].name);
468
+ if (i < edges.length) {
469
+ parts.push(`--[${edges[i].label}]-->`);
470
+ }
471
+ }
472
+ return parts.join(' ');
473
+ }
474
+ /**
475
+ * Generates score breakdown
476
+ */
477
+ generateScoreBreakdown(result, searchType) {
478
+ const components = {};
479
+ const weights = {};
480
+ let formula = '';
481
+ if (result.pathScores) {
482
+ components.vectorMatch = result.pathScores.vector;
483
+ components.sparseMatch = result.pathScores.sparse;
484
+ components.ftsMatch = result.pathScores.fts;
485
+ components.graphMatch = result.pathScores.graph;
486
+ // Estimate weights (would come from config in production)
487
+ weights.vectorWeight = 0.4;
488
+ weights.sparseWeight = 0.3;
489
+ weights.ftsWeight = 0.2;
490
+ weights.graphWeight = 0.1;
491
+ formula = 'RRF(vector, sparse, fts, graph)';
492
+ }
493
+ else {
494
+ components.vectorMatch = result.score;
495
+ formula = 'base_score';
496
+ }
497
+ if (result.metadata?.pagerank) {
498
+ components.pageRank = result.metadata.pagerank;
499
+ formula += ' * (1 + pagerank)';
500
+ }
501
+ if (result.created_at) {
502
+ const age = Date.now() - result.created_at;
503
+ const ageDays = age / (1000 * 60 * 60 * 24);
504
+ components.temporalDecay = Math.exp(-Math.log(2) * ageDays / 90);
505
+ formula += ' * temporal_decay';
506
+ }
507
+ return {
508
+ finalScore: result.score,
509
+ components,
510
+ weights,
511
+ formula
512
+ };
513
+ }
514
+ /**
515
+ * Gets minimal score breakdown
516
+ */
517
+ getMinimalScoreBreakdown(result) {
518
+ return {
519
+ finalScore: result.score,
520
+ components: {},
521
+ weights: {},
522
+ formula: 'score'
523
+ };
524
+ }
525
+ /**
526
+ * Extracts contributing sources
527
+ */
528
+ extractSources(result) {
529
+ if (result.source) {
530
+ return result.source.split(',').map((s) => s.trim()).filter(Boolean);
531
+ }
532
+ return ['unknown'];
533
+ }
534
+ /**
535
+ * Calculates overall confidence
536
+ */
537
+ calculateConfidence(result, scoreBreakdown) {
538
+ // Base confidence from score
539
+ let confidence = Math.min(result.score, 1.0);
540
+ // Boost confidence if multiple sources agree
541
+ const sources = this.extractSources(result);
542
+ if (sources.length > 1) {
543
+ confidence = Math.min(confidence * 1.1, 1.0);
544
+ }
545
+ // Boost confidence if PageRank is high
546
+ if (result.metadata?.pagerank && result.metadata.pagerank > 0.5) {
547
+ confidence = Math.min(confidence * 1.05, 1.0);
548
+ }
549
+ return confidence;
550
+ }
551
+ }
552
+ exports.ExplainableRetrievalService = ExplainableRetrievalService;