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,397 @@
1
+ "use strict";
2
+ /**
3
+ * Adaptive Query Fusion with Dynamic Weights
4
+ *
5
+ * Based on 2026 SOTA Research:
6
+ * - Meilisearch Adaptive RAG (2025)
7
+ * - FEEG Framework: Finder, Evaluator, Explainer, Generator (Samuel et al., 2026)
8
+ * - ORCAS-I Intent Classifier (Alexander et al., 2022)
9
+ * - Query Intent Classification Research (2025)
10
+ *
11
+ * Combines:
12
+ * 1. Heuristic keyword-based classification (fast, reliable)
13
+ * 2. LLM-based classification (accurate, semantic understanding)
14
+ * 3. Adaptive weight selection based on query type
15
+ * 4. Fallback mechanisms for robustness
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.AdaptiveQueryFusion = exports.QueryComplexity = exports.QueryIntent = void 0;
19
+ // Query Intent Types (FEEG Framework)
20
+ var QueryIntent;
21
+ (function (QueryIntent) {
22
+ QueryIntent["FINDER"] = "finder";
23
+ QueryIntent["EVALUATOR"] = "evaluator";
24
+ QueryIntent["EXPLAINER"] = "explainer";
25
+ QueryIntent["GENERATOR"] = "generator"; // Generate new content (G)
26
+ })(QueryIntent || (exports.QueryIntent = QueryIntent = {}));
27
+ // Query Complexity Levels
28
+ var QueryComplexity;
29
+ (function (QueryComplexity) {
30
+ QueryComplexity["SIMPLE"] = "simple";
31
+ QueryComplexity["MODERATE"] = "moderate";
32
+ QueryComplexity["COMPLEX"] = "complex";
33
+ QueryComplexity["EXPLORATORY"] = "exploratory"; // Open-ended, broad search
34
+ })(QueryComplexity || (exports.QueryComplexity = QueryComplexity = {}));
35
+ /**
36
+ * Adaptive Query Fusion Engine
37
+ *
38
+ * Classifies queries and selects optimal search weights dynamically
39
+ */
40
+ class AdaptiveQueryFusion {
41
+ db;
42
+ embeddingService;
43
+ config;
44
+ classificationCache;
45
+ // Keyword patterns for heuristic classification
46
+ FINDER_KEYWORDS = [
47
+ 'what', 'when', 'where', 'who', 'how many', 'specific',
48
+ 'find', 'search', 'look for', 'get', 'retrieve',
49
+ 'latest', 'recent', 'current', 'today', 'news'
50
+ ];
51
+ EVALUATOR_KEYWORDS = [
52
+ 'compare', 'difference', 'versus', 'vs', 'better',
53
+ 'evaluate', 'assess', 'analyze', 'review', 'rate',
54
+ 'pros', 'cons', 'advantages', 'disadvantages'
55
+ ];
56
+ EXPLAINER_KEYWORDS = [
57
+ 'why', 'how', 'explain', 'understand', 'about',
58
+ 'describe', 'tell me', 'what is', 'concept',
59
+ 'meaning', 'definition', 'work', 'process'
60
+ ];
61
+ GENERATOR_KEYWORDS = [
62
+ 'create', 'generate', 'write', 'make', 'build',
63
+ 'suggest', 'recommend', 'example', 'template',
64
+ 'code', 'script', 'plan', 'strategy'
65
+ ];
66
+ // Predefined weight configurations for each intent
67
+ WEIGHT_CONFIGS = {
68
+ [QueryIntent.FINDER]: {
69
+ [QueryComplexity.SIMPLE]: {
70
+ vector: 0.4,
71
+ sparse: 0.5, // High keyword matching for factual queries
72
+ fts: 0.1,
73
+ graph: 0.0
74
+ },
75
+ [QueryComplexity.MODERATE]: {
76
+ vector: 0.45,
77
+ sparse: 0.35,
78
+ fts: 0.15,
79
+ graph: 0.05
80
+ },
81
+ [QueryComplexity.COMPLEX]: {
82
+ vector: 0.5,
83
+ sparse: 0.25,
84
+ fts: 0.15,
85
+ graph: 0.1
86
+ },
87
+ [QueryComplexity.EXPLORATORY]: {
88
+ vector: 0.4,
89
+ sparse: 0.2,
90
+ fts: 0.2,
91
+ graph: 0.2
92
+ }
93
+ },
94
+ [QueryIntent.EVALUATOR]: {
95
+ [QueryComplexity.SIMPLE]: {
96
+ vector: 0.5,
97
+ sparse: 0.3,
98
+ fts: 0.1,
99
+ graph: 0.1
100
+ },
101
+ [QueryComplexity.MODERATE]: {
102
+ vector: 0.5,
103
+ sparse: 0.2,
104
+ fts: 0.1,
105
+ graph: 0.2
106
+ },
107
+ [QueryComplexity.COMPLEX]: {
108
+ vector: 0.45,
109
+ sparse: 0.15,
110
+ fts: 0.1,
111
+ graph: 0.3
112
+ },
113
+ [QueryComplexity.EXPLORATORY]: {
114
+ vector: 0.4,
115
+ sparse: 0.1,
116
+ fts: 0.1,
117
+ graph: 0.4
118
+ }
119
+ },
120
+ [QueryIntent.EXPLAINER]: {
121
+ [QueryComplexity.SIMPLE]: {
122
+ vector: 0.6,
123
+ sparse: 0.2,
124
+ fts: 0.1,
125
+ graph: 0.1
126
+ },
127
+ [QueryComplexity.MODERATE]: {
128
+ vector: 0.55,
129
+ sparse: 0.15,
130
+ fts: 0.1,
131
+ graph: 0.2
132
+ },
133
+ [QueryComplexity.COMPLEX]: {
134
+ vector: 0.5,
135
+ sparse: 0.1,
136
+ fts: 0.1,
137
+ graph: 0.3
138
+ },
139
+ [QueryComplexity.EXPLORATORY]: {
140
+ vector: 0.45,
141
+ sparse: 0.1,
142
+ fts: 0.1,
143
+ graph: 0.35
144
+ }
145
+ },
146
+ [QueryIntent.GENERATOR]: {
147
+ [QueryComplexity.SIMPLE]: {
148
+ vector: 0.5,
149
+ sparse: 0.3,
150
+ fts: 0.1,
151
+ graph: 0.1
152
+ },
153
+ [QueryComplexity.MODERATE]: {
154
+ vector: 0.45,
155
+ sparse: 0.25,
156
+ fts: 0.15,
157
+ graph: 0.15
158
+ },
159
+ [QueryComplexity.COMPLEX]: {
160
+ vector: 0.4,
161
+ sparse: 0.2,
162
+ fts: 0.2,
163
+ graph: 0.2
164
+ },
165
+ [QueryComplexity.EXPLORATORY]: {
166
+ vector: 0.35,
167
+ sparse: 0.15,
168
+ fts: 0.2,
169
+ graph: 0.3
170
+ }
171
+ }
172
+ };
173
+ constructor(db, embeddingService, config) {
174
+ this.db = db;
175
+ this.embeddingService = embeddingService;
176
+ this.classificationCache = new Map();
177
+ this.config = {
178
+ enableLLM: true,
179
+ llmModel: 'demyagent-4b-i1:Q6_K',
180
+ heuristicThreshold: 0.7,
181
+ cacheClassifications: true,
182
+ maxCacheSize: 1000,
183
+ ...config
184
+ };
185
+ }
186
+ /**
187
+ * Classify query using hybrid approach (heuristic + LLM)
188
+ */
189
+ async classifyQuery(query) {
190
+ // Check cache first
191
+ if (this.config.cacheClassifications) {
192
+ const cached = this.classificationCache.get(query);
193
+ if (cached) {
194
+ console.error(`[AdaptiveQueryFusion] Cache hit for query: "${query}"`);
195
+ return cached;
196
+ }
197
+ }
198
+ // Step 1: Heuristic classification (fast)
199
+ const heuristicResult = this.classifyHeuristic(query);
200
+ console.error(`[AdaptiveQueryFusion] Heuristic result: ${heuristicResult.intent} (confidence: ${heuristicResult.confidence.toFixed(2)})`);
201
+ // Step 2: If heuristic confidence is high enough, use it
202
+ if (heuristicResult.confidence >= this.config.heuristicThreshold) {
203
+ const result = {
204
+ ...heuristicResult,
205
+ method: 'heuristic'
206
+ };
207
+ this.cacheClassification(query, result);
208
+ return result;
209
+ }
210
+ // Step 3: Try LLM classification if enabled and heuristic confidence is low
211
+ if (this.config.enableLLM) {
212
+ try {
213
+ const llmResult = await this.classifyLLM(query);
214
+ console.error(`[AdaptiveQueryFusion] LLM result: ${llmResult.intent} (confidence: ${llmResult.confidence.toFixed(2)})`);
215
+ const result = {
216
+ ...llmResult,
217
+ method: 'llm'
218
+ };
219
+ this.cacheClassification(query, result);
220
+ return result;
221
+ }
222
+ catch (error) {
223
+ console.error(`[AdaptiveQueryFusion] LLM classification failed, falling back to heuristic:`, error);
224
+ }
225
+ }
226
+ // Step 4: Fallback to heuristic
227
+ const result = {
228
+ ...heuristicResult,
229
+ method: 'hybrid'
230
+ };
231
+ this.cacheClassification(query, result);
232
+ return result;
233
+ }
234
+ /**
235
+ * Heuristic-based query classification using keywords
236
+ */
237
+ classifyHeuristic(query) {
238
+ const queryLower = query.toLowerCase();
239
+ const words = queryLower.split(/\s+/);
240
+ // Count keyword matches for each intent
241
+ const intentScores = {
242
+ [QueryIntent.FINDER]: this.countMatches(queryLower, this.FINDER_KEYWORDS),
243
+ [QueryIntent.EVALUATOR]: this.countMatches(queryLower, this.EVALUATOR_KEYWORDS),
244
+ [QueryIntent.EXPLAINER]: this.countMatches(queryLower, this.EXPLAINER_KEYWORDS),
245
+ [QueryIntent.GENERATOR]: this.countMatches(queryLower, this.GENERATOR_KEYWORDS)
246
+ };
247
+ // Find dominant intent
248
+ let dominantIntent = QueryIntent.FINDER;
249
+ let maxScore = 0;
250
+ for (const [intent, score] of Object.entries(intentScores)) {
251
+ if (score > maxScore) {
252
+ maxScore = score;
253
+ dominantIntent = intent;
254
+ }
255
+ }
256
+ // Calculate confidence (0.0 - 1.0)
257
+ const totalScore = Object.values(intentScores).reduce((a, b) => a + b, 0);
258
+ const confidence = totalScore > 0 ? maxScore / totalScore : 0.5;
259
+ // Classify complexity
260
+ const complexity = this.classifyComplexity(query, words);
261
+ // Extract keywords
262
+ const keywords = words.filter(w => w.length > 3);
263
+ return {
264
+ intent: dominantIntent,
265
+ complexity,
266
+ confidence,
267
+ keywords,
268
+ reasoning: `Heuristic classification: ${dominantIntent} (${(confidence * 100).toFixed(0)}% confidence)`
269
+ };
270
+ }
271
+ /**
272
+ * LLM-based query classification using Ollama
273
+ */
274
+ async classifyLLM(query) {
275
+ try {
276
+ // Dynamic import to avoid hard dependency
277
+ const ollamaModule = await import('ollama');
278
+ const ollama = ollamaModule?.default ?? ollamaModule;
279
+ const systemPrompt = `You are a query classification expert. Classify the user's query into one of these categories:
280
+ - "finder": Seeking factual information (what, when, where, who, how many)
281
+ - "evaluator": Comparing or evaluating content (compare, difference, better)
282
+ - "explainer": Understanding concepts (why, how, explain)
283
+ - "generator": Creating or generating content (create, write, suggest)
284
+
285
+ Also classify complexity as: "simple", "moderate", "complex", or "exploratory"
286
+
287
+ Respond with JSON: {"intent": "...", "complexity": "...", "confidence": 0.0-1.0, "reasoning": "..."}`;
288
+ const response = await ollama.chat({
289
+ model: this.config.llmModel,
290
+ messages: [
291
+ { role: 'system', content: systemPrompt },
292
+ { role: 'user', content: query }
293
+ ],
294
+ format: 'json'
295
+ });
296
+ let responseText = response?.message?.content?.trim?.() ?? '';
297
+ // Clean markdown formatting if present
298
+ if (responseText.startsWith('```json')) {
299
+ responseText = responseText.replace(/```json/g, '').replace(/```/g, '').trim();
300
+ }
301
+ else if (responseText.startsWith('```')) {
302
+ responseText = responseText.replace(/```/g, '').trim();
303
+ }
304
+ const parsed = JSON.parse(responseText);
305
+ return {
306
+ intent: (parsed.intent || QueryIntent.FINDER),
307
+ complexity: (parsed.complexity || QueryComplexity.MODERATE),
308
+ confidence: Math.min(1.0, Math.max(0.0, parsed.confidence || 0.8)),
309
+ keywords: query.split(/\s+/).filter(w => w.length > 3),
310
+ reasoning: parsed.reasoning || 'LLM classification'
311
+ };
312
+ }
313
+ catch (error) {
314
+ console.error('[AdaptiveQueryFusion] LLM classification error:', error);
315
+ throw error;
316
+ }
317
+ }
318
+ /**
319
+ * Classify query complexity based on heuristics
320
+ */
321
+ classifyComplexity(query, words) {
322
+ const queryLower = query.toLowerCase();
323
+ // Simple: Short, direct questions
324
+ if (words.length <= 4 && /^(what|when|where|who|how many)\s/.test(queryLower)) {
325
+ return QueryComplexity.SIMPLE;
326
+ }
327
+ // Exploratory: Open-ended, broad
328
+ if (/^(all|everything|explore|overview|general|summary|list)\b/.test(queryLower)) {
329
+ return QueryComplexity.EXPLORATORY;
330
+ }
331
+ // Complex: Multiple concepts, reasoning required
332
+ const complexIndicators = ['and', 'or', 'but', 'however', 'relationship', 'connection', 'impact'];
333
+ const complexCount = complexIndicators.filter(ind => queryLower.includes(ind)).length;
334
+ if (complexCount >= 2 || words.length > 15) {
335
+ return QueryComplexity.COMPLEX;
336
+ }
337
+ // Moderate: Default
338
+ return QueryComplexity.MODERATE;
339
+ }
340
+ /**
341
+ * Count keyword matches in query
342
+ */
343
+ countMatches(query, keywords) {
344
+ return keywords.filter(keyword => query.includes(keyword)).length;
345
+ }
346
+ /**
347
+ * Cache classification result
348
+ */
349
+ cacheClassification(query, classification) {
350
+ if (!this.config.cacheClassifications)
351
+ return;
352
+ // Simple LRU: remove oldest if cache is full
353
+ if (this.classificationCache.size >= this.config.maxCacheSize) {
354
+ const firstKey = this.classificationCache.keys().next().value;
355
+ if (firstKey !== undefined) {
356
+ this.classificationCache.delete(firstKey);
357
+ }
358
+ }
359
+ this.classificationCache.set(query, classification);
360
+ }
361
+ /**
362
+ * Get adaptive search weights for a query
363
+ */
364
+ async getAdaptiveWeights(query) {
365
+ const classification = await this.classifyQuery(query);
366
+ const weights = this.WEIGHT_CONFIGS[classification.intent][classification.complexity];
367
+ console.error(`[AdaptiveQueryFusion] Weights for "${query}":`, {
368
+ intent: classification.intent,
369
+ complexity: classification.complexity,
370
+ weights
371
+ });
372
+ return weights;
373
+ }
374
+ /**
375
+ * Get classification details for debugging
376
+ */
377
+ async getClassificationDetails(query) {
378
+ return this.classifyQuery(query);
379
+ }
380
+ /**
381
+ * Clear cache
382
+ */
383
+ clearCache() {
384
+ this.classificationCache.clear();
385
+ console.error('[AdaptiveQueryFusion] Cache cleared');
386
+ }
387
+ /**
388
+ * Get cache statistics
389
+ */
390
+ getCacheStats() {
391
+ return {
392
+ size: this.classificationCache.size,
393
+ maxSize: this.config.maxCacheSize
394
+ };
395
+ }
396
+ }
397
+ exports.AdaptiveQueryFusion = AdaptiveQueryFusion;
@@ -14,6 +14,7 @@
14
14
  */
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
16
  exports.DynamicFusionSearch = exports.DEFAULT_FUSION_CONFIG = void 0;
17
+ const adaptive_query_fusion_1 = require("./adaptive-query-fusion");
17
18
  /**
18
19
  * Default fusion configuration
19
20
  */
@@ -58,19 +59,33 @@ exports.DEFAULT_FUSION_CONFIG = {
58
59
  class DynamicFusionSearch {
59
60
  db;
60
61
  embeddings;
62
+ adaptiveQueryFusion;
61
63
  constructor(db, embeddings) {
62
64
  this.db = db;
63
65
  this.embeddings = embeddings;
66
+ this.adaptiveQueryFusion = new adaptive_query_fusion_1.AdaptiveQueryFusion(db, embeddings, {
67
+ enableLLM: true,
68
+ cacheClassifications: true,
69
+ maxCacheSize: 1000
70
+ });
64
71
  }
65
72
  /**
66
- * Execute dynamic fusion search
73
+ * Execute dynamic fusion search with adaptive weights
67
74
  */
68
75
  async search(query, config = {}) {
69
76
  const startTime = Date.now();
70
- // Merge with defaults
77
+ // Get adaptive weights based on query classification
78
+ const adaptiveWeights = await this.adaptiveQueryFusion.getAdaptiveWeights(query);
79
+ // Merge config with defaults first, then apply adaptive weights
71
80
  const fullConfig = this.mergeConfig(config);
72
- console.log('[DynamicFusion] Starting search with config:', {
81
+ // Override weights with adaptive values
82
+ fullConfig.vector.weight = adaptiveWeights.vector;
83
+ fullConfig.sparse.weight = adaptiveWeights.sparse;
84
+ fullConfig.fts.weight = adaptiveWeights.fts;
85
+ fullConfig.graph.weight = adaptiveWeights.graph;
86
+ console.log('[DynamicFusion] Starting search with adaptive weights:', {
73
87
  query,
88
+ weights: adaptiveWeights,
74
89
  enabledPaths: this.getEnabledPaths(fullConfig)
75
90
  });
76
91
  // Execute all enabled paths in parallel
@@ -82,7 +97,8 @@ class DynamicFusionSearch {
82
97
  console.log('[DynamicFusion] Search completed:', {
83
98
  totalResults: fusedResults.length,
84
99
  pathContributions: stats.pathContributions,
85
- fusionTime: stats.fusionTime
100
+ fusionTime: stats.fusionTime,
101
+ adaptiveWeights
86
102
  });
87
103
  return { results: fusedResults, stats };
88
104
  }
@@ -576,10 +592,31 @@ class DynamicFusionSearch {
576
592
  */
577
593
  mergeConfig(config) {
578
594
  return {
579
- vector: { ...exports.DEFAULT_FUSION_CONFIG.vector, ...config.vector },
580
- sparse: { ...exports.DEFAULT_FUSION_CONFIG.sparse, ...config.sparse },
581
- fts: { ...exports.DEFAULT_FUSION_CONFIG.fts, ...config.fts },
582
- graph: { ...exports.DEFAULT_FUSION_CONFIG.graph, ...config.graph },
595
+ vector: {
596
+ enabled: config.vector?.enabled ?? exports.DEFAULT_FUSION_CONFIG.vector.enabled,
597
+ weight: config.vector?.weight ?? exports.DEFAULT_FUSION_CONFIG.vector.weight,
598
+ topK: config.vector?.topK ?? exports.DEFAULT_FUSION_CONFIG.vector.topK,
599
+ efSearch: config.vector?.efSearch ?? exports.DEFAULT_FUSION_CONFIG.vector.efSearch
600
+ },
601
+ sparse: {
602
+ enabled: config.sparse?.enabled ?? exports.DEFAULT_FUSION_CONFIG.sparse.enabled,
603
+ weight: config.sparse?.weight ?? exports.DEFAULT_FUSION_CONFIG.sparse.weight,
604
+ topK: config.sparse?.topK ?? exports.DEFAULT_FUSION_CONFIG.sparse.topK,
605
+ minScore: config.sparse?.minScore ?? exports.DEFAULT_FUSION_CONFIG.sparse.minScore
606
+ },
607
+ fts: {
608
+ enabled: config.fts?.enabled ?? exports.DEFAULT_FUSION_CONFIG.fts.enabled,
609
+ weight: config.fts?.weight ?? exports.DEFAULT_FUSION_CONFIG.fts.weight,
610
+ topK: config.fts?.topK ?? exports.DEFAULT_FUSION_CONFIG.fts.topK,
611
+ fuzzy: config.fts?.fuzzy ?? exports.DEFAULT_FUSION_CONFIG.fts.fuzzy
612
+ },
613
+ graph: {
614
+ enabled: config.graph?.enabled ?? exports.DEFAULT_FUSION_CONFIG.graph.enabled,
615
+ weight: config.graph?.weight ?? exports.DEFAULT_FUSION_CONFIG.graph.weight,
616
+ maxDepth: config.graph?.maxDepth ?? exports.DEFAULT_FUSION_CONFIG.graph.maxDepth,
617
+ maxResults: config.graph?.maxResults ?? exports.DEFAULT_FUSION_CONFIG.graph.maxResults,
618
+ relationTypes: config.graph?.relationTypes ?? exports.DEFAULT_FUSION_CONFIG.graph.relationTypes
619
+ },
583
620
  fusion: { ...exports.DEFAULT_FUSION_CONFIG.fusion, ...config.fusion }
584
621
  };
585
622
  }
@@ -598,5 +635,23 @@ class DynamicFusionSearch {
598
635
  paths.push('graph');
599
636
  return paths;
600
637
  }
638
+ /**
639
+ * Get query classification details (for debugging/transparency)
640
+ */
641
+ async getQueryClassification(query) {
642
+ return this.adaptiveQueryFusion.getClassificationDetails(query);
643
+ }
644
+ /**
645
+ * Clear classification cache
646
+ */
647
+ clearClassificationCache() {
648
+ this.adaptiveQueryFusion.clearCache();
649
+ }
650
+ /**
651
+ * Get cache statistics
652
+ */
653
+ getClassificationCacheStats() {
654
+ return this.adaptiveQueryFusion.getCacheStats();
655
+ }
601
656
  }
602
657
  exports.DynamicFusionSearch = DynamicFusionSearch;