cozo-memory 1.1.2 → 1.1.4

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,390 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MultiHopVectorPivot = void 0;
4
+ /**
5
+ * Multi-Hop Reasoning with Vector Pivots (v2.0 - Logic-Aware)
6
+ *
7
+ * Pattern: Use Vector Search as "springboard" for Graph Traversal with Logic-Aware Reasoning
8
+ *
9
+ * Research:
10
+ * - HopRAG (ACL 2025): Logic-aware RAG with pseudo-queries as edges
11
+ * - Retrieval Pivot Attacks (arXiv:2602.08668): Security & boundary enforcement
12
+ * - Neo4j GraphRAG: Multi-hop reasoning patterns
13
+ *
14
+ * Pipeline (Retrieve-Reason-Prune):
15
+ * 1. Vector Search: Find semantic pivot points using HNSW
16
+ * 2. Reasoning-Augmented Traversal: BFS with LLM-guided hops via pseudo-queries
17
+ * 3. Helpfulness Scoring: Combines textual similarity + logical importance
18
+ * 4. Pruning: Filter by confidence threshold and pivot depth
19
+ * 5. Aggregation: Deduplicate and rank entities
20
+ */
21
+ class MultiHopVectorPivot {
22
+ db;
23
+ embeddingService;
24
+ maxBranchingFactor = 5;
25
+ maxNodesExplored = 100;
26
+ confidenceThreshold = 0.5;
27
+ maxPivotDepth = 3; // Security: limit graph expansion depth
28
+ constructor(db, embeddingService, maxBranchingFactor, maxNodesExplored, confidenceThreshold, maxPivotDepth) {
29
+ this.db = db;
30
+ this.embeddingService = embeddingService;
31
+ if (maxBranchingFactor)
32
+ this.maxBranchingFactor = maxBranchingFactor;
33
+ if (maxNodesExplored)
34
+ this.maxNodesExplored = maxNodesExplored;
35
+ if (confidenceThreshold)
36
+ this.confidenceThreshold = confidenceThreshold;
37
+ if (maxPivotDepth)
38
+ this.maxPivotDepth = maxPivotDepth;
39
+ }
40
+ /**
41
+ * Main entry point: Multi-Hop Reasoning with Vector Pivots
42
+ *
43
+ * Retrieve-Reason-Prune Pipeline:
44
+ * 1. Retrieve: Find vector pivots via semantic search
45
+ * 2. Reason: Traverse graph with logic-aware hops
46
+ * 3. Prune: Filter by helpfulness and confidence
47
+ */
48
+ async multiHopVectorPivot(query, maxHops = 3, limit = 10) {
49
+ try {
50
+ console.error(`[MultiHop] Starting multi-hop reasoning for query: "${query}"`);
51
+ const startTime = Date.now();
52
+ // Step 1: RETRIEVE - Find vector pivots
53
+ const pivots = await this.findVectorPivots(query, this.maxBranchingFactor);
54
+ console.error(`[MultiHop] Found ${pivots.length} vector pivots`);
55
+ if (pivots.length === 0) {
56
+ return {
57
+ query,
58
+ pivots: [],
59
+ paths: [],
60
+ aggregated_results: [],
61
+ total_hops: 0,
62
+ execution_time_ms: Date.now() - startTime,
63
+ status: "no_pivots_found"
64
+ };
65
+ }
66
+ // Step 2: REASON - Traverse graph from each pivot with logic-aware hops
67
+ const allPaths = [];
68
+ const nodesExplored = new Set();
69
+ for (const pivot of pivots) {
70
+ if (nodesExplored.size >= this.maxNodesExplored)
71
+ break;
72
+ const paths = await this.reasoningAugmentedTraversal(pivot, query, maxHops, nodesExplored);
73
+ allPaths.push(...paths);
74
+ }
75
+ console.error(`[MultiHop] Explored ${nodesExplored.size} nodes across ${allPaths.length} paths`);
76
+ // Step 3: PRUNE - Filter by helpfulness and confidence
77
+ const prunedPaths = this.prunePathsByHelpfulness(allPaths, query);
78
+ console.error(`[MultiHop] Pruned to ${prunedPaths.length} high-quality paths`);
79
+ // Step 4: AGGREGATE - Deduplicate and rank
80
+ const aggregated = this.aggregatePathResults(prunedPaths);
81
+ const topResults = aggregated.slice(0, limit);
82
+ const executionTime = Date.now() - startTime;
83
+ console.error(`[MultiHop] Completed in ${executionTime}ms`);
84
+ return {
85
+ query,
86
+ pivots: pivots.map(p => ({ id: p.id, name: p.name, similarity: p.similarity })),
87
+ paths: prunedPaths.slice(0, 5), // Top 5 paths for context
88
+ aggregated_results: topResults,
89
+ total_hops: Math.max(...allPaths.map(p => p.path.length)),
90
+ execution_time_ms: executionTime,
91
+ status: "success"
92
+ };
93
+ }
94
+ catch (error) {
95
+ console.error("[MultiHop] Error:", error.message);
96
+ return {
97
+ query,
98
+ pivots: [],
99
+ paths: [],
100
+ aggregated_results: [],
101
+ total_hops: 0,
102
+ execution_time_ms: 0,
103
+ status: "error",
104
+ error: error.message
105
+ };
106
+ }
107
+ }
108
+ /**
109
+ * Step 1: RETRIEVE - Find vector pivots via semantic search
110
+ */
111
+ async findVectorPivots(query, topK) {
112
+ try {
113
+ const queryEmbedding = await this.embeddingService.embed(query);
114
+ const datalog = `
115
+ ?[id, name, type, similarity] :=
116
+ ~entity:semantic{id | query: vec($query_vector), k: $topk, ef: $ef_search, bind_distance: dist},
117
+ *entity{id, name, type, @ "NOW"},
118
+ similarity = 1.0 - dist
119
+ :sort -similarity
120
+ :limit $topk
121
+ `;
122
+ const result = await this.db.run(datalog, {
123
+ query_vector: queryEmbedding,
124
+ topk: topK,
125
+ ef_search: 100
126
+ });
127
+ return result.rows.map((r) => ({
128
+ id: r[0],
129
+ name: r[1],
130
+ type: r[2],
131
+ similarity: r[3]
132
+ }));
133
+ }
134
+ catch (error) {
135
+ console.error("[MultiHop] Error finding pivots:", error.message);
136
+ return [];
137
+ }
138
+ }
139
+ /**
140
+ * Step 2: REASON - Reasoning-Augmented Graph Traversal
141
+ *
142
+ * Uses BFS with logic-aware hops guided by pseudo-queries and relationship context
143
+ */
144
+ async reasoningAugmentedTraversal(pivot, query, maxHops, nodesExplored) {
145
+ const paths = [];
146
+ const queue = [
147
+ {
148
+ id: pivot.id,
149
+ depth: 0,
150
+ path: [{ id: pivot.id, name: pivot.name, type: pivot.type, confidence: 1.0 }],
151
+ pathScore: pivot.similarity,
152
+ visitedInPath: new Set([pivot.id])
153
+ }
154
+ ];
155
+ const queryEmbedding = await this.embeddingService.embed(query);
156
+ while (queue.length > 0 && nodesExplored.size < this.maxNodesExplored) {
157
+ const current = queue.shift();
158
+ // Pivot Depth Security: Enforce max depth to prevent uncontrolled expansion
159
+ if (current.depth >= maxHops || current.depth >= this.maxPivotDepth) {
160
+ paths.push({
161
+ path: current.path,
162
+ score: current.pathScore,
163
+ depth: current.depth,
164
+ confidence: this.calculatePathConfidence(current.path)
165
+ });
166
+ continue;
167
+ }
168
+ // Get neighbors with relationship context (logic-aware)
169
+ const neighbors = await this.getNeighborsWithContext(current.id, query, queryEmbedding, current.visitedInPath);
170
+ for (const neighbor of neighbors.slice(0, this.maxBranchingFactor)) {
171
+ if (nodesExplored.has(neighbor.id))
172
+ continue;
173
+ nodesExplored.add(neighbor.id);
174
+ // Confidence decay: 0.9^depth for recency weighting
175
+ const depthPenalty = Math.pow(0.9, current.depth + 1);
176
+ const newConfidence = neighbor.confidence * depthPenalty;
177
+ // Pruning: Skip if confidence drops below threshold
178
+ if (newConfidence < this.confidenceThreshold) {
179
+ continue;
180
+ }
181
+ const newPath = [
182
+ ...current.path,
183
+ {
184
+ id: neighbor.id,
185
+ name: neighbor.name,
186
+ type: neighbor.type,
187
+ confidence: newConfidence,
188
+ relationshipType: neighbor.relationshipType,
189
+ relationshipStrength: neighbor.relationshipStrength
190
+ }
191
+ ];
192
+ const newPathScore = current.pathScore * newConfidence;
193
+ const newVisited = new Set(current.visitedInPath);
194
+ newVisited.add(neighbor.id);
195
+ // Add to queue for further exploration
196
+ queue.push({
197
+ id: neighbor.id,
198
+ depth: current.depth + 1,
199
+ path: newPath,
200
+ pathScore: newPathScore,
201
+ visitedInPath: newVisited
202
+ });
203
+ // Also record as a complete path
204
+ paths.push({
205
+ path: newPath,
206
+ score: newPathScore,
207
+ depth: current.depth + 1,
208
+ confidence: newConfidence
209
+ });
210
+ }
211
+ }
212
+ return paths;
213
+ }
214
+ /**
215
+ * Get neighbors with relationship context (logic-aware edges)
216
+ *
217
+ * Considers:
218
+ * - Semantic similarity to query
219
+ * - Relationship type and strength
220
+ * - Entity type compatibility
221
+ */
222
+ async getNeighborsWithContext(entityId, query, queryEmbedding, visitedInPath) {
223
+ try {
224
+ const datalog = `
225
+ rank_val[id, r] := *entity_rank{entity_id: id, pagerank: r}
226
+ rank_val[id, r] := *entity{id, @ "NOW"}, not *entity_rank{entity_id: id}, r = 0.0
227
+
228
+ outgoing[to_id, rel_type, strength] :=
229
+ *relationship{from_id: $entity_id, to_id, relation_type: rel_type, strength, @ "NOW"}
230
+
231
+ incoming[from_id, rel_type, strength] :=
232
+ *relationship{from_id, to_id: $entity_id, relation_type: rel_type, strength, @ "NOW"}
233
+
234
+ neighbors[id, name, type, rel_type, strength, pr] :=
235
+ outgoing[id, rel_type, strength],
236
+ *entity{id, name, type, @ "NOW"},
237
+ rank_val[id, pr]
238
+
239
+ neighbors[id, name, type, rel_type, strength, pr] :=
240
+ incoming[id, rel_type, strength],
241
+ *entity{id, name, type, @ "NOW"},
242
+ rank_val[id, pr]
243
+
244
+ ?[id, name, type, rel_type, strength, pr] := neighbors[id, name, type, rel_type, strength, pr]
245
+ :sort -strength
246
+ :limit $limit
247
+ `;
248
+ const result = await this.db.run(datalog, {
249
+ entity_id: entityId,
250
+ limit: this.maxBranchingFactor * 2
251
+ });
252
+ const neighbors = [];
253
+ for (const row of result.rows) {
254
+ const neighborId = row[0];
255
+ const name = row[1];
256
+ const type = row[2];
257
+ const relType = row[3];
258
+ const strength = row[4];
259
+ const pagerank = row[5];
260
+ // Calculate semantic similarity to query
261
+ try {
262
+ const entityRes = await this.db.run('?[embedding] := *entity{id: $id, embedding, @ "NOW"}', { id: neighborId });
263
+ if (entityRes.rows.length > 0) {
264
+ const embedding = entityRes.rows[0][0];
265
+ const cosineSim = this.cosineSimilarity(queryEmbedding, embedding);
266
+ // Confidence combines: semantic similarity (0.4) + relationship strength (0.3) + pagerank (0.3)
267
+ const confidence = 0.4 * cosineSim + 0.3 * strength + 0.3 * Math.min(pagerank, 1.0);
268
+ neighbors.push({
269
+ id: neighborId,
270
+ name,
271
+ type,
272
+ relationshipType: relType,
273
+ relationshipStrength: strength,
274
+ confidence,
275
+ pagerank
276
+ });
277
+ }
278
+ }
279
+ catch (e) {
280
+ // Fallback: use relationship strength + pagerank only
281
+ const confidence = 0.5 * strength + 0.5 * Math.min(pagerank, 1.0);
282
+ neighbors.push({
283
+ id: neighborId,
284
+ name,
285
+ type,
286
+ relationshipType: relType,
287
+ relationshipStrength: strength,
288
+ confidence,
289
+ pagerank
290
+ });
291
+ }
292
+ }
293
+ return neighbors.sort((a, b) => b.confidence - a.confidence);
294
+ }
295
+ catch (error) {
296
+ console.error("[MultiHop] Error getting neighbors:", error.message);
297
+ return [];
298
+ }
299
+ }
300
+ /**
301
+ * Step 3: PRUNE - Filter paths by Helpfulness Score
302
+ *
303
+ * Helpfulness = textual_similarity (0.6) + logical_importance (0.4)
304
+ * Logical importance = path_length_penalty + confidence + diversity
305
+ */
306
+ prunePathsByHelpfulness(paths, query) {
307
+ return paths
308
+ .map(path => {
309
+ // Textual similarity: average confidence of path nodes
310
+ const textualSim = path.confidence;
311
+ // Logical importance: combination of factors
312
+ const pathLength = path.path.length;
313
+ const lengthPenalty = 1.0 / (1.0 + 0.1 * pathLength); // Prefer shorter paths
314
+ const logicalImportance = lengthPenalty * path.confidence;
315
+ // Helpfulness score
316
+ const helpfulness = 0.6 * textualSim + 0.4 * logicalImportance;
317
+ return {
318
+ ...path,
319
+ helpfulness: helpfulness
320
+ };
321
+ })
322
+ .filter(p => p.helpfulness >= this.confidenceThreshold)
323
+ .sort((a, b) => (b.helpfulness || 0) - (a.helpfulness || 0));
324
+ }
325
+ /**
326
+ * Step 4: AGGREGATE - Deduplicate and rank entities
327
+ */
328
+ aggregatePathResults(paths) {
329
+ const entityMap = new Map();
330
+ for (const path of paths) {
331
+ for (const node of path.path) {
332
+ const existing = entityMap.get(node.id);
333
+ if (existing) {
334
+ existing.occurrences++;
335
+ existing.max_score = Math.max(existing.max_score, path.score);
336
+ existing.avg_score = (existing.avg_score * (existing.occurrences - 1) + path.score) / existing.occurrences;
337
+ existing.min_depth = Math.min(existing.min_depth, path.depth);
338
+ }
339
+ else {
340
+ entityMap.set(node.id, {
341
+ id: node.id,
342
+ name: node.name,
343
+ type: node.type,
344
+ occurrences: 1,
345
+ max_score: path.score,
346
+ avg_score: path.score,
347
+ min_depth: path.depth,
348
+ confidence: node.confidence
349
+ });
350
+ }
351
+ }
352
+ }
353
+ return Array.from(entityMap.values())
354
+ .sort((a, b) => {
355
+ // Sort by: occurrences (frequency) > avg_score > min_depth
356
+ if (b.occurrences !== a.occurrences)
357
+ return b.occurrences - a.occurrences;
358
+ if (b.avg_score !== a.avg_score)
359
+ return b.avg_score - a.avg_score;
360
+ return a.min_depth - b.min_depth;
361
+ });
362
+ }
363
+ /**
364
+ * Calculate path confidence: average confidence of all nodes in path
365
+ */
366
+ calculatePathConfidence(path) {
367
+ if (path.length === 0)
368
+ return 0;
369
+ const sum = path.reduce((acc, node) => acc + node.confidence, 0);
370
+ return sum / path.length;
371
+ }
372
+ /**
373
+ * Cosine similarity between two vectors
374
+ */
375
+ cosineSimilarity(a, b) {
376
+ if (a.length !== b.length)
377
+ return 0;
378
+ let dotProduct = 0;
379
+ let normA = 0;
380
+ let normB = 0;
381
+ for (let i = 0; i < a.length; i++) {
382
+ dotProduct += a[i] * b[i];
383
+ normA += a[i] * a[i];
384
+ normB += b[i] * b[i];
385
+ }
386
+ const denominator = Math.sqrt(normA) * Math.sqrt(normB);
387
+ return denominator === 0 ? 0 : dotProduct / denominator;
388
+ }
389
+ }
390
+ exports.MultiHopVectorPivot = MultiHopVectorPivot;