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.
- package/README.md +161 -1159
- package/dist/adaptive-query-fusion.js +397 -0
- package/dist/dynamic-fusion.js +63 -8
- package/dist/explainable-retrieval.js +552 -0
- package/dist/hierarchical-memory.js +358 -0
- package/dist/proactive-suggestions.js +382 -0
- package/dist/temporal-conflict-resolution.js +386 -0
- package/dist/temporal-pattern-detection-backup.js +358 -0
- package/dist/temporal-pattern-detection.js +482 -0
- package/dist/test-adaptive-query-fusion.js +208 -0
- package/dist/test-explainable-retrieval.js +408 -0
- package/dist/test-hierarchical-and-patterns.js +17 -0
- package/dist/test-hierarchical-memory.js +205 -0
- package/dist/test-proactive-suggestions.js +240 -0
- package/dist/test-temporal-conflict-resolution.js +228 -0
- package/dist/test-temporal-patterns.js +317 -0
- package/package.json +1 -1
|
@@ -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;
|