cozo-memory 1.1.8 → 1.2.1

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.
@@ -322,6 +322,11 @@ class AdaptiveGraphRetrieval {
322
322
  * Main adaptive retrieval method
323
323
  */
324
324
  async retrieve(query, limit = 10) {
325
+ // Validate limit to prevent errors
326
+ if (limit <= 0) {
327
+ console.error('[AdaptiveRetrieval] Invalid limit value:', limit, '- must be positive. Defaulting to 10.');
328
+ limit = 10;
329
+ }
325
330
  // 1. Classify query complexity
326
331
  const complexity = this.classifyQueryComplexity(query);
327
332
  console.error(`[AdaptiveRetrieval] Query complexity: ${complexity}`);
@@ -338,6 +343,11 @@ class AdaptiveGraphRetrieval {
338
343
  }
339
344
  // ==================== Strategy Implementations ====================
340
345
  async vectorSearch(embedding, limit) {
346
+ // Validate limit
347
+ if (limit <= 0) {
348
+ console.error('[AdaptiveRetrieval] Invalid limit in vectorSearch:', limit);
349
+ return [];
350
+ }
341
351
  const result = await this.db.run(`
342
352
  ?[id, name, type, score] :=
343
353
  ~entity:semantic{id | query: vec($embedding), k: $limit, ef: 100, bind_distance: dist},
@@ -74,10 +74,23 @@ class DynamicFusionSearch {
74
74
  */
75
75
  async search(query, config = {}) {
76
76
  const startTime = Date.now();
77
+ // Merge config with defaults first
78
+ const fullConfig = this.mergeConfig(config);
79
+ // Validate topK values to prevent errors
80
+ if (fullConfig.vector && fullConfig.vector.topK <= 0) {
81
+ console.error('[DynamicFusion] Invalid vector.topK:', fullConfig.vector.topK, '- must be positive. Defaulting to 20.');
82
+ fullConfig.vector.topK = 20;
83
+ }
84
+ if (fullConfig.sparse && fullConfig.sparse.topK <= 0) {
85
+ console.error('[DynamicFusion] Invalid sparse.topK:', fullConfig.sparse.topK, '- must be positive. Defaulting to 20.');
86
+ fullConfig.sparse.topK = 20;
87
+ }
88
+ if (fullConfig.fts && fullConfig.fts.topK <= 0) {
89
+ console.error('[DynamicFusion] Invalid fts.topK:', fullConfig.fts.topK, '- must be positive. Defaulting to 20.');
90
+ fullConfig.fts.topK = 20;
91
+ }
77
92
  // Get adaptive weights based on query classification
78
93
  const adaptiveWeights = await this.adaptiveQueryFusion.getAdaptiveWeights(query);
79
- // Merge config with defaults first, then apply adaptive weights
80
- const fullConfig = this.mergeConfig(config);
81
94
  // Override weights with adaptive values
82
95
  fullConfig.vector.weight = adaptiveWeights.vector;
83
96
  fullConfig.sparse.weight = adaptiveWeights.sparse;
@@ -0,0 +1,295 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EmotionalSalienceService = void 0;
4
+ /**
5
+ * Emotional Salience Keywords
6
+ *
7
+ * Organized by intensity and category based on psychological research
8
+ * and biological memory enhancement patterns.
9
+ */
10
+ const SALIENCE_KEYWORDS = {
11
+ // High Intensity (0.8-1.0) - Critical, urgent, never forget
12
+ critical: {
13
+ weight: 1.0,
14
+ keywords: [
15
+ 'critical', 'crucial', 'vital', 'essential', 'must', 'never forget',
16
+ 'always remember', 'extremely important', 'life-changing', 'breakthrough',
17
+ 'emergency', 'urgent', 'immediate', 'asap', 'priority one'
18
+ ]
19
+ },
20
+ // High Importance (0.6-0.8) - Important, significant
21
+ important: {
22
+ weight: 0.8,
23
+ keywords: [
24
+ 'important', 'significant', 'key', 'major', 'primary', 'fundamental',
25
+ 'noteworthy', 'remarkable', 'substantial', 'considerable', 'priority',
26
+ 'high priority', 'top priority', 'remember this', 'don\'t forget'
27
+ ]
28
+ },
29
+ // Emotional Arousal (0.5-0.7) - Surprise, excitement, concern
30
+ emotional: {
31
+ weight: 0.7,
32
+ keywords: [
33
+ 'surprising', 'unexpected', 'shocking', 'amazing', 'incredible',
34
+ 'exciting', 'thrilling', 'alarming', 'concerning', 'worrying',
35
+ 'breakthrough', 'discovery', 'revelation', 'game-changer'
36
+ ]
37
+ },
38
+ // Temporal Urgency (0.5-0.7) - Time-sensitive
39
+ temporal: {
40
+ weight: 0.6,
41
+ keywords: [
42
+ 'deadline', 'due', 'expires', 'time-sensitive', 'limited time',
43
+ 'soon', 'quickly', 'immediately', 'now', 'today', 'this week',
44
+ 'before', 'until', 'by'
45
+ ]
46
+ },
47
+ // Negative Salience (0.4-0.6) - Problems, risks, warnings
48
+ negative: {
49
+ weight: 0.5,
50
+ keywords: [
51
+ 'problem', 'issue', 'bug', 'error', 'failure', 'risk', 'danger',
52
+ 'warning', 'alert', 'caution', 'concern', 'threat', 'vulnerability',
53
+ 'critical bug', 'security issue', 'data loss'
54
+ ]
55
+ },
56
+ // Positive Salience (0.4-0.6) - Success, achievement
57
+ positive: {
58
+ weight: 0.5,
59
+ keywords: [
60
+ 'success', 'achievement', 'milestone', 'completed', 'solved',
61
+ 'accomplished', 'victory', 'win', 'breakthrough', 'innovation',
62
+ 'excellent', 'outstanding', 'exceptional'
63
+ ]
64
+ },
65
+ // Moderate Importance (0.3-0.5) - Notable, relevant
66
+ moderate: {
67
+ weight: 0.4,
68
+ keywords: [
69
+ 'notable', 'relevant', 'useful', 'helpful', 'valuable', 'worth noting',
70
+ 'interesting', 'attention', 'note', 'reminder', 'tip', 'advice'
71
+ ]
72
+ }
73
+ };
74
+ class EmotionalSalienceService {
75
+ db;
76
+ config;
77
+ constructor(db, config = {}) {
78
+ this.db = db;
79
+ this.config = {
80
+ enableSalience: true,
81
+ salienceBoostFactor: 2.0,
82
+ decaySlowdownFactor: 0.5,
83
+ minSalienceThreshold: 0.3,
84
+ updateExistingObservations: false,
85
+ ...config
86
+ };
87
+ }
88
+ /**
89
+ * Calculate emotional salience score for a text
90
+ */
91
+ calculateSalienceScore(text) {
92
+ const lowerText = text.toLowerCase();
93
+ const detectedKeywords = [];
94
+ // Scan for keywords in each category
95
+ for (const [categoryName, categoryData] of Object.entries(SALIENCE_KEYWORDS)) {
96
+ for (const keyword of categoryData.keywords) {
97
+ // Use word boundaries for accurate matching
98
+ const regex = new RegExp(`\\b${keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
99
+ if (regex.test(text)) {
100
+ detectedKeywords.push({
101
+ keyword,
102
+ weight: categoryData.weight,
103
+ category: categoryName
104
+ });
105
+ }
106
+ }
107
+ }
108
+ // Calculate weighted score
109
+ let totalScore = 0;
110
+ let maxWeight = 0;
111
+ for (const detected of detectedKeywords) {
112
+ totalScore += detected.weight;
113
+ maxWeight = Math.max(maxWeight, detected.weight);
114
+ }
115
+ // Normalize: Use max weight if multiple keywords, with diminishing returns
116
+ const score = detectedKeywords.length === 0
117
+ ? 0
118
+ : Math.min(1.0, maxWeight + (totalScore - maxWeight) * 0.2);
119
+ // Categorize
120
+ let category;
121
+ if (score >= 0.7)
122
+ category = 'high';
123
+ else if (score >= 0.4)
124
+ category = 'medium';
125
+ else if (score >= 0.3)
126
+ category = 'low';
127
+ else
128
+ category = 'neutral';
129
+ // Generate reason
130
+ const uniqueKeywords = [...new Set(detectedKeywords.map(d => d.keyword))];
131
+ const reason = detectedKeywords.length === 0
132
+ ? 'No emotional salience keywords detected'
133
+ : `Detected ${detectedKeywords.length} salience keyword(s): ${uniqueKeywords.slice(0, 3).join(', ')}${uniqueKeywords.length > 3 ? '...' : ''}`;
134
+ return {
135
+ score,
136
+ keywords: uniqueKeywords,
137
+ category,
138
+ reason
139
+ };
140
+ }
141
+ /**
142
+ * Calculate salience boost for memory activation
143
+ */
144
+ calculateBoost(salienceScore) {
145
+ if (salienceScore < this.config.minSalienceThreshold) {
146
+ return { strengthMultiplier: 1.0, decayReduction: 0.0 };
147
+ }
148
+ // Strength multiplier: 1.0 to salienceBoostFactor (default: 2.0)
149
+ const strengthMultiplier = 1.0 + (salienceScore * (this.config.salienceBoostFactor - 1.0));
150
+ // Decay reduction: 0.0 to decaySlowdownFactor (default: 0.5)
151
+ // Higher salience = slower decay
152
+ const decayReduction = salienceScore * this.config.decaySlowdownFactor;
153
+ return { strengthMultiplier, decayReduction };
154
+ }
155
+ /**
156
+ * Score all observations for emotional salience
157
+ */
158
+ async scoreAllObservations() {
159
+ if (!this.config.enableSalience) {
160
+ return [];
161
+ }
162
+ try {
163
+ // Fetch all observations (without time travel if not supported)
164
+ const result = await this.db.run(`
165
+ ?[id, text, entity_id] := *observation{id, text, entity_id}
166
+ `);
167
+ const scores = [];
168
+ for (const row of result.rows) {
169
+ const [id, text, entityId] = row;
170
+ const { score, keywords, category, reason } = this.calculateSalienceScore(text);
171
+ const boost = this.calculateBoost(score);
172
+ scores.push({
173
+ observationId: id,
174
+ text: text,
175
+ salienceScore: score,
176
+ detectedKeywords: keywords,
177
+ category,
178
+ boost,
179
+ reason
180
+ });
181
+ }
182
+ return scores.sort((a, b) => b.salienceScore - a.salienceScore);
183
+ }
184
+ catch (error) {
185
+ console.error('[EmotionalSalience] Error scoring observations:', error);
186
+ return [];
187
+ }
188
+ }
189
+ /**
190
+ * Apply salience metadata to observations
191
+ */
192
+ async applySalienceMetadata(dryRun = false) {
193
+ const scores = await this.scoreAllObservations();
194
+ const toUpdate = scores.filter(s => s.salienceScore >= this.config.minSalienceThreshold);
195
+ if (dryRun) {
196
+ return { updated: 0, scores: toUpdate, dryRun: true };
197
+ }
198
+ let updated = 0;
199
+ for (const score of toUpdate) {
200
+ try {
201
+ // Update observation metadata with salience information
202
+ await this.db.run(`
203
+ ?[id, entity_id, text, metadata] :=
204
+ *observation{id, entity_id, text, metadata},
205
+ id = $id,
206
+ new_metadata = {
207
+ "emotional_salience": $salience_score,
208
+ "salience_category": $category,
209
+ "salience_keywords": $keywords,
210
+ "salience_boost_strength": $strength_multiplier,
211
+ "salience_boost_decay": $decay_reduction
212
+ },
213
+ metadata = if(is_null(metadata), new_metadata, concat(metadata, new_metadata))
214
+
215
+ :put observation {id, entity_id, text, metadata}
216
+ `, {
217
+ id: score.observationId,
218
+ salience_score: score.salienceScore,
219
+ category: score.category,
220
+ keywords: JSON.stringify(score.detectedKeywords),
221
+ strength_multiplier: score.boost.strengthMultiplier,
222
+ decay_reduction: score.boost.decayReduction
223
+ });
224
+ updated++;
225
+ }
226
+ catch (error) {
227
+ console.error(`[EmotionalSalience] Error updating observation ${score.observationId}:`, error);
228
+ }
229
+ }
230
+ return { updated, scores: toUpdate, dryRun: false };
231
+ }
232
+ /**
233
+ * Get salience statistics
234
+ */
235
+ async getSalienceStats() {
236
+ const scores = await this.scoreAllObservations();
237
+ const distribution = {
238
+ high: scores.filter(s => s.category === 'high').length,
239
+ medium: scores.filter(s => s.category === 'medium').length,
240
+ low: scores.filter(s => s.category === 'low').length,
241
+ neutral: scores.filter(s => s.category === 'neutral').length
242
+ };
243
+ const averageSalience = scores.length > 0
244
+ ? scores.reduce((sum, s) => sum + s.salienceScore, 0) / scores.length
245
+ : 0;
246
+ // Count keyword frequencies
247
+ const keywordCounts = new Map();
248
+ for (const score of scores) {
249
+ for (const keyword of score.detectedKeywords) {
250
+ keywordCounts.set(keyword, (keywordCounts.get(keyword) || 0) + 1);
251
+ }
252
+ }
253
+ const topKeywords = Array.from(keywordCounts.entries())
254
+ .map(([keyword, count]) => ({ keyword, count }))
255
+ .sort((a, b) => b.count - a.count)
256
+ .slice(0, 10);
257
+ return {
258
+ totalObservations: scores.length,
259
+ withSalience: scores.filter(s => s.salienceScore >= this.config.minSalienceThreshold).length,
260
+ distribution,
261
+ averageSalience,
262
+ topKeywords
263
+ };
264
+ }
265
+ /**
266
+ * Get salience score for a specific observation
267
+ */
268
+ async getObservationSalience(observationId) {
269
+ try {
270
+ const result = await this.db.run(`
271
+ ?[id, text] := *observation{id, text}, id = $id
272
+ `, { id: observationId });
273
+ if (result.rows.length === 0) {
274
+ return null;
275
+ }
276
+ const [id, text] = result.rows[0];
277
+ const { score, keywords, category, reason } = this.calculateSalienceScore(text);
278
+ const boost = this.calculateBoost(score);
279
+ return {
280
+ observationId: id,
281
+ text: text,
282
+ salienceScore: score,
283
+ detectedKeywords: keywords,
284
+ category,
285
+ boost,
286
+ reason
287
+ };
288
+ }
289
+ catch (error) {
290
+ console.error('[EmotionalSalience] Error getting observation salience:', error);
291
+ return null;
292
+ }
293
+ }
294
+ }
295
+ exports.EmotionalSalienceService = EmotionalSalienceService;
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.HybridSearch = void 0;
7
7
  const crypto_1 = __importDefault(require("crypto"));
8
8
  const reranker_service_1 = require("./reranker-service");
9
+ const logger_1 = require("./logger");
10
+ const performance_monitor_1 = require("./performance-monitor");
9
11
  const SEMANTIC_CACHE_THRESHOLD = 0.95;
10
12
  class HybridSearch {
11
13
  db;
@@ -138,22 +140,30 @@ class HybridSearch {
138
140
  }
139
141
  }
140
142
  async advancedSearch(options) {
141
- console.error("[HybridSearch] Starting advancedSearch with options:", JSON.stringify(options, null, 2));
143
+ logger_1.logger.debug('HybridSearch', 'Starting advancedSearch', { query: options.query, limit: options.limit });
142
144
  const { query, limit = 10, filters, graphConstraints, vectorParams } = options;
145
+ // Validate limit to prevent infinite loops
146
+ if (limit <= 0) {
147
+ logger_1.logger.warn('HybridSearch', `Invalid limit value: ${limit} - must be positive. Defaulting to 10.`);
148
+ options.limit = 10;
149
+ }
150
+ const endTimer = performance_monitor_1.perfMonitor.startTimer('advancedSearch');
143
151
  let queryEmbedding;
144
152
  try {
145
153
  queryEmbedding = await this.embeddingService.embed(query);
146
154
  }
147
155
  catch (e) {
148
- console.error("[HybridSearch] Embedding failed", e);
156
+ logger_1.logger.error('HybridSearch', 'Embedding failed', e);
157
+ endTimer();
149
158
  throw e;
150
159
  }
151
160
  const cachedResults = await this.tryCacheLookup(options, queryEmbedding);
152
161
  if (cachedResults !== null) {
153
- console.error("[HybridSearch] Cache hit for advancedSearch");
162
+ logger_1.logger.debug('HybridSearch', 'Cache hit for advancedSearch');
163
+ endTimer();
154
164
  return cachedResults;
155
165
  }
156
- console.error("[HybridSearch] Cache miss, executing Datalog query...");
166
+ logger_1.logger.trace('HybridSearch', 'Cache miss, executing Datalog query...');
157
167
  let topk = limit * 2;
158
168
  const hasFilters = (filters?.metadata && Object.keys(filters.metadata).length > 0) ||
159
169
  (filters?.entityTypes && filters.entityTypes.length > 0);
@@ -204,7 +214,7 @@ class HybridSearch {
204
214
  semanticCall += `, filter: ${hnswFilters.join(" && ")}`;
205
215
  }
206
216
  semanticCall += `}`;
207
- let bodyConstraints = [semanticCall, `*entity{id, name, type, metadata, created_at, @ "NOW"}`];
217
+ let bodyConstraints = [semanticCall, `*entity{id, name, type, metadata, created_at}`];
208
218
  if (metaJoins.length > 0) {
209
219
  bodyConstraints.push(...metaJoins);
210
220
  }
@@ -229,13 +239,13 @@ class HybridSearch {
229
239
  }
230
240
  const helperRules = [
231
241
  `rank_val[id, r] := *entity_rank{entity_id: id, pagerank: r}`,
232
- `rank_val[id, r] := *entity{id, @ "NOW"}, not *entity_rank{entity_id: id}, r = 0.0`
242
+ `rank_val[id, r] := *entity{id}, not *entity_rank{entity_id: id}, r = 0.0`
233
243
  ];
234
244
  if (graphConstraints?.requiredRelations && graphConstraints.requiredRelations.length > 0) {
235
- helperRules.push(`rel_match[id, rel_type] := *relationship{from_id: id, relation_type: rel_type, @ "NOW"}`, `rel_match[id, rel_type] := *relationship{to_id: id, relation_type: rel_type, @ "NOW"}`);
245
+ helperRules.push(`rel_match[id, rel_type] := *relationship{from_id: id, relation_type: rel_type}`, `rel_match[id, rel_type] := *relationship{to_id: id, relation_type: rel_type}`);
236
246
  }
237
247
  if (graphConstraints?.targetEntityIds && graphConstraints.targetEntityIds.length > 0) {
238
- helperRules.push(`target_match[id, target_id] := *relationship{from_id: id, to_id: target_id, @ "NOW"}`, `target_match[id, target_id] := *relationship{to_id: id, from_id: target_id, @ "NOW"}`);
248
+ helperRules.push(`target_match[id, target_id] := *relationship{from_id: id, to_id: target_id}`, `target_match[id, target_id] := *relationship{to_id: id, from_id: target_id}`);
239
249
  }
240
250
  const datalogQuery = [
241
251
  ...helperRules,
@@ -278,11 +288,16 @@ class HybridSearch {
278
288
  return rerankedResults;
279
289
  }
280
290
  await this.updateCache(options, queryEmbedding, finalResults);
291
+ endTimer();
281
292
  return finalResults;
282
293
  }
283
294
  catch (e) {
284
- console.error("[HybridSearch] Error in advancedSearch:", e.message);
285
- return this.search(options);
295
+ logger_1.logger.error('HybridSearch', 'Error in advancedSearch', e.message);
296
+ performance_monitor_1.perfMonitor.recordMetric('advancedSearch', 0, true);
297
+ endTimer();
298
+ // Prevent infinite recursion by returning empty results instead of calling search()
299
+ logger_1.logger.warn('HybridSearch', 'Returning empty results to prevent infinite loop');
300
+ return [];
286
301
  }
287
302
  }
288
303
  async search(options) {
@@ -308,8 +323,14 @@ class HybridSearch {
308
323
  });
309
324
  }
310
325
  async graphRag(options) {
311
- console.error("[HybridSearch] Starting graphRag with options:", JSON.stringify(options, null, 2));
326
+ logger_1.logger.debug('HybridSearch', 'Starting graphRag', { query: options.query, limit: options.limit });
312
327
  const { query, limit = 5, filters, graphConstraints } = options;
328
+ // Validate limit to prevent infinite loops
329
+ if (limit <= 0) {
330
+ logger_1.logger.warn('HybridSearch', `Invalid limit value: ${limit} - must be positive. Defaulting to 5.`);
331
+ options.limit = 5;
332
+ }
333
+ const endTimer = performance_monitor_1.perfMonitor.startTimer('graphRag');
313
334
  const maxDepth = graphConstraints?.maxDepth || 2;
314
335
  const queryEmbedding = await this.embeddingService.embed(query);
315
336
  const topk = limit * 2;
@@ -350,7 +371,7 @@ class HybridSearch {
350
371
  // 4. Calculate a combined score based on vector distance, graph distance, and PageRank
351
372
  const datalogQuery = `
352
373
  rank_val[id, r] := *entity_rank{entity_id: id, pagerank: r}
353
- rank_val[id, r] := *entity{id, @ "NOW"}, not *entity_rank{entity_id: id}, r = 0.0
374
+ rank_val[id, r] := *entity{id}, not *entity_rank{entity_id: id}, r = 0.0
354
375
 
355
376
  seeds[id, score] := ${seedConstraints.join(", ")}, score = 1.0 - dist
356
377
 
@@ -360,7 +381,7 @@ class HybridSearch {
360
381
 
361
382
  result_entities[id, final_score, depth] := path[seed_id, id, depth], seeds[seed_id, seed_score], rank_val[id, pr], final_score = seed_score * (1.0 - 0.2 * depth)
362
383
 
363
- ?[id, name, type, metadata, created_at, score, source, text] := result_entities[id, score, depth], *entity{id, name, type, metadata, created_at, @ "NOW"}, source = 'graph_rag_entity', text = ''
384
+ ?[id, name, type, metadata, created_at, score, source, text] := result_entities[id, score, depth], *entity{id, name, type, metadata, created_at}, source = 'graph_rag_entity', text = ''
364
385
 
365
386
  :sort -score
366
387
  :limit $limit
@@ -396,19 +417,31 @@ class HybridSearch {
396
417
  }
397
418
  const decayedResults = this.applyTimeDecay(searchResults);
398
419
  if (options.rerank) {
399
- return await this.applyReranking(options.query, decayedResults);
420
+ const reranked = await this.applyReranking(options.query, decayedResults);
421
+ endTimer();
422
+ return reranked;
400
423
  }
424
+ endTimer();
401
425
  return decayedResults;
402
426
  }
403
427
  catch (e) {
404
- console.error("[HybridSearch] Error in graphRag:", e.message);
405
- // Fallback to normal search on error
406
- return this.search(options);
428
+ logger_1.logger.error('HybridSearch', 'Error in graphRag', e.message);
429
+ performance_monitor_1.perfMonitor.recordMetric('graphRag', 0, true);
430
+ endTimer();
431
+ // Prevent infinite recursion by returning empty results
432
+ logger_1.logger.warn('HybridSearch', 'Returning empty results to prevent infinite loop');
433
+ return [];
407
434
  }
408
435
  }
409
436
  async agenticRetrieve(options) {
410
- console.error("[HybridSearch] Starting agenticRetrieve with query:", options.query);
437
+ logger_1.logger.debug('HybridSearch', 'Starting agenticRetrieve', { query: options.query });
411
438
  const { query, routingModel = "demyagent-4b-i1:Q6_K" } = options;
439
+ // Validate limit to prevent infinite loops
440
+ if (options.limit !== undefined && options.limit <= 0) {
441
+ logger_1.logger.warn('HybridSearch', `Invalid limit value: ${options.limit} - must be positive. Defaulting to 10.`);
442
+ options.limit = 10;
443
+ }
444
+ const endTimer = performance_monitor_1.perfMonitor.startTimer('agenticRetrieve');
412
445
  const systemPrompt = `You are a Routing Agent for an advanced Memory/RAG system.
413
446
  Your job is to analyze the user's query and decide which search strategy is the most appropriate.
414
447
  Available strategies: