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.
- package/dist/adaptive-retrieval.js +10 -0
- package/dist/dynamic-fusion.js +15 -2
- package/dist/emotional-salience.js +295 -0
- package/dist/hybrid-search.js +51 -18
- package/dist/index.js +705 -9
- package/dist/logger.js +56 -0
- package/dist/memory-activation.js +64 -30
- package/dist/memory-service.js +68 -0
- package/dist/migrate-logging.js +113 -0
- package/dist/performance-monitor.js +108 -0
- package/dist/pre-storage-reasoning.js +351 -0
- package/dist/temporal-conflict-resolution.js +10 -6
- package/dist/test-activation-mcp.js +118 -0
- package/dist/test-advanced-search-mcp.js +204 -0
- package/dist/test-conflicts-mcp.js +173 -0
- package/dist/test-emotional-salience.js +177 -0
- package/dist/test-hierarchical-mcp.js +135 -0
- package/dist/test-large-dataset.js +502 -0
- package/dist/test-logical-edges-mcp.js +215 -0
- package/dist/test-metadata-check.js +69 -0
- package/dist/test-metadata-update.js +92 -0
- package/dist/test-pre-storage-reasoning.js +149 -0
- package/dist/test-salience-mcp.js +94 -0
- package/dist/test-spreading-mcp.js +155 -0
- package/dist/test-suggest-connections-mcp.js +172 -0
- package/dist/test-zettelkasten-evolution.js +255 -0
- package/dist/test-zettelkasten-fixed.js +74 -0
- package/dist/test-zettelkasten-live.js +117 -0
- package/dist/test-zettelkasten-mcp.js +96 -0
- package/dist/zettelkasten-evolution.js +342 -0
- package/package.json +1 -1
|
@@ -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},
|
package/dist/dynamic-fusion.js
CHANGED
|
@@ -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;
|
package/dist/hybrid-search.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
+
logger_1.logger.debug('HybridSearch', 'Cache hit for advancedSearch');
|
|
163
|
+
endTimer();
|
|
154
164
|
return cachedResults;
|
|
155
165
|
}
|
|
156
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
285
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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:
|