cozo-memory 1.0.9 → 1.1.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.
@@ -94,9 +94,16 @@ class EmbeddingService {
94
94
  tokenizer = null;
95
95
  modelId;
96
96
  dimensions;
97
+ useOllama;
98
+ ollamaModel;
99
+ ollamaBaseUrl;
97
100
  queue = Promise.resolve();
98
101
  constructor() {
99
102
  this.cache = new LRUCache(1000, 3600000); // 1000 entries, 1h TTL
103
+ // Check if Ollama should be used
104
+ this.useOllama = process.env.USE_OLLAMA === 'true';
105
+ this.ollamaModel = process.env.OLLAMA_EMBEDDING_MODEL || 'argus-ai/pplx-embed-v1-0.6b:q8_0';
106
+ this.ollamaBaseUrl = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
100
107
  // Support multiple embedding models via environment variable
101
108
  this.modelId = process.env.EMBEDDING_MODEL || "Xenova/bge-m3";
102
109
  // Set dimensions based on model
@@ -106,9 +113,20 @@ class EmbeddingService {
106
113
  "Xenova/bge-small-en-v1.5": 384,
107
114
  "Xenova/nomic-embed-text-v1": 768,
108
115
  "onnx-community/Qwen3-Embedding-0.6B-ONNX": 1024,
116
+ // Note: perplexity-ai models require manual ONNX file placement
117
+ // See PPLX_EMBED_INTEGRATION.md for instructions
118
+ "perplexity-ai/pplx-embed-v1-0.6b": 1024,
119
+ "perplexity-ai/pplx-embed-v1-4b": 2560,
120
+ // Ollama models
121
+ "argus-ai/pplx-embed-v1-0.6b:q8_0": 1024,
109
122
  };
110
- this.dimensions = dimensionMap[this.modelId] || 1024;
111
- console.error(`[EmbeddingService] Using model: ${this.modelId} (${this.dimensions} dimensions)`);
123
+ this.dimensions = dimensionMap[this.useOllama ? this.ollamaModel : this.modelId] || 1024;
124
+ if (this.useOllama) {
125
+ console.error(`[EmbeddingService] Using Ollama: ${this.ollamaModel} @ ${this.ollamaBaseUrl} (${this.dimensions} dimensions)`);
126
+ }
127
+ else {
128
+ console.error(`[EmbeddingService] Using ONNX model: ${this.modelId} (${this.dimensions} dimensions)`);
129
+ }
112
130
  }
113
131
  // Public getter for dimensions
114
132
  getDimensions() {
@@ -125,6 +143,11 @@ class EmbeddingService {
125
143
  async init() {
126
144
  if (this.session && this.tokenizer)
127
145
  return;
146
+ // Skip ONNX initialization if using Ollama
147
+ if (this.useOllama) {
148
+ console.error('[EmbeddingService] Using Ollama backend, skipping ONNX initialization');
149
+ return;
150
+ }
128
151
  try {
129
152
  // 1. Check if model needs to be downloaded
130
153
  // Extract namespace and model name from modelId (e.g., "Xenova/bge-m3" or "onnx-community/Qwen3-Embedding-0.6B-ONNX")
@@ -139,10 +162,23 @@ class EmbeddingService {
139
162
  if (!fs.existsSync(fp32Path) && !fs.existsSync(quantizedPath)) {
140
163
  console.log(`[EmbeddingService] Model not found, downloading ${this.modelId}...`);
141
164
  console.log(`[EmbeddingService] This may take a few minutes on first run.`);
142
- // Import AutoModel dynamically to trigger download
143
- const { AutoModel } = await import("@xenova/transformers");
144
- await AutoModel.from_pretrained(this.modelId, { quantized: false });
145
- console.log(`[EmbeddingService] Model download completed.`);
165
+ // Check if this is a Xenova-compatible model
166
+ if (namespace === 'Xenova' || namespace === 'onnx-community') {
167
+ // Import AutoModel dynamically to trigger download
168
+ const { AutoModel } = await import("@xenova/transformers");
169
+ await AutoModel.from_pretrained(this.modelId, { quantized: false });
170
+ console.log(`[EmbeddingService] Model download completed.`);
171
+ }
172
+ else {
173
+ // For non-Xenova models (like perplexity-ai), provide manual download instructions
174
+ console.error(`[EmbeddingService] ERROR: Model ${this.modelId} is not available via @xenova/transformers`);
175
+ console.error(`[EmbeddingService] Please download the model manually:`);
176
+ console.error(`[EmbeddingService] 1. Visit: https://huggingface.co/${this.modelId}`);
177
+ console.error(`[EmbeddingService] 2. Download the 'onnx' folder contents`);
178
+ console.error(`[EmbeddingService] 3. Place files in: ${baseDir}`);
179
+ console.error(`[EmbeddingService] See PPLX_EMBED_INTEGRATION.md for detailed instructions`);
180
+ throw new Error(`Model ${this.modelId} requires manual download. See error messages above.`);
181
+ }
146
182
  }
147
183
  // 2. Load Tokenizer
148
184
  if (!this.tokenizer) {
@@ -188,6 +224,10 @@ class EmbeddingService {
188
224
  return cached;
189
225
  }
190
226
  try {
227
+ // Use Ollama if enabled
228
+ if (this.useOllama) {
229
+ return await this.embedWithOllama(textStr);
230
+ }
191
231
  await this.init();
192
232
  if (!this.session || !this.tokenizer)
193
233
  throw new Error("Session/Tokenizer not initialized");
@@ -240,6 +280,37 @@ class EmbeddingService {
240
280
  }
241
281
  });
242
282
  }
283
+ async embedWithOllama(text) {
284
+ try {
285
+ const response = await fetch(`${this.ollamaBaseUrl}/api/embeddings`, {
286
+ method: 'POST',
287
+ headers: {
288
+ 'Content-Type': 'application/json',
289
+ },
290
+ body: JSON.stringify({
291
+ model: this.ollamaModel,
292
+ prompt: text,
293
+ }),
294
+ });
295
+ if (!response.ok) {
296
+ throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
297
+ }
298
+ const data = await response.json();
299
+ if (!data.embedding || !Array.isArray(data.embedding)) {
300
+ throw new Error('Invalid response from Ollama API');
301
+ }
302
+ const embedding = data.embedding;
303
+ // Normalize the embedding
304
+ const normalized = this.normalize(embedding);
305
+ // Cache it
306
+ this.cache.set(text, normalized);
307
+ return normalized;
308
+ }
309
+ catch (error) {
310
+ console.error(`[EmbeddingService] Ollama error for "${text.substring(0, 20)}...":`, error?.message || error);
311
+ return new Array(this.dimensions).fill(0);
312
+ }
313
+ }
243
314
  // Batch-Embeddings
244
315
  async embedBatch(texts) {
245
316
  // For now, process sequentially via serialized queue to avoid overloading
@@ -306,7 +377,8 @@ class EmbeddingService {
306
377
  return {
307
378
  size: this.cache.size(),
308
379
  maxSize: 1000,
309
- model: this.modelId,
380
+ model: this.useOllama ? this.ollamaModel : this.modelId,
381
+ backend: this.useOllama ? 'ollama' : 'onnx',
310
382
  dimensions: this.dimensions
311
383
  };
312
384
  }
@@ -88,10 +88,14 @@ async function runEvaluation() {
88
88
  }
89
89
  const server = new index_1.MemoryServer(EVAL_DB_PATH);
90
90
  await server.embeddingService.embed("warmup");
91
+ // Warmup reranker
92
+ await server.hybridSearch.advancedSearch({ query: "warmup", limit: 1, rerank: true });
91
93
  await setupEvalData(server);
92
94
  const methods = [
93
95
  { name: "Hybrid Search", func: (q) => server.hybridSearch.search({ query: q, limit: 10 }) },
96
+ { name: "Reranked Search", func: (q) => server.hybridSearch.search({ query: q, limit: 10, rerank: true }) },
94
97
  { name: "Graph-RAG", func: (q) => server.hybridSearch.graphRag({ query: q, limit: 10, graphConstraints: { maxDepth: 2 } }) },
98
+ { name: "Graph-RAG (Reranked)", func: (q) => server.hybridSearch.graphRag({ query: q, limit: 10, graphConstraints: { maxDepth: 2 }, rerank: true }) },
95
99
  { name: "Graph-Walking", func: (q) => server.graph_walking({ query: q, limit: 10, max_depth: 3 }) }
96
100
  ];
97
101
  const summary = [];
@@ -101,6 +105,9 @@ async function runEvaluation() {
101
105
  let totalRecall10 = 0;
102
106
  let totalMRR = 0;
103
107
  let totalLatency = 0;
108
+ const n = EVAL_DATASET.length;
109
+ // Reset cache between methods to get accurate latency
110
+ await server.hybridSearch.clearCache();
104
111
  for (const task of EVAL_DATASET) {
105
112
  const t0 = perf_hooks_1.performance.now();
106
113
  const results = await method.func(task.query);
@@ -113,7 +120,6 @@ async function runEvaluation() {
113
120
  totalMRR += mrr;
114
121
  totalLatency += (t1 - t0);
115
122
  }
116
- const n = EVAL_DATASET.length;
117
123
  summary.push({
118
124
  Method: method.name,
119
125
  "Recall@3": (totalRecall3 / n).toFixed(3),
@@ -5,15 +5,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.HybridSearch = void 0;
7
7
  const crypto_1 = __importDefault(require("crypto"));
8
+ const reranker_service_1 = require("./reranker-service");
8
9
  const SEMANTIC_CACHE_THRESHOLD = 0.95;
9
10
  class HybridSearch {
10
11
  db;
11
12
  embeddingService;
13
+ rerankerService;
12
14
  searchCache = new Map();
13
15
  CACHE_TTL = 300000; // 5 minutes cache
14
16
  constructor(db, embeddingService) {
15
17
  this.db = db;
16
18
  this.embeddingService = embeddingService;
19
+ this.rerankerService = new reranker_service_1.RerankerService();
17
20
  }
18
21
  getCacheKey(options) {
19
22
  const str = JSON.stringify({
@@ -75,6 +78,65 @@ class HybridSearch {
75
78
  return { ...r, score };
76
79
  });
77
80
  }
81
+ applyContextBoost(results, options) {
82
+ const { session_id, task_id } = options;
83
+ if (!session_id && !task_id)
84
+ return results;
85
+ return results.map(result => {
86
+ let boost = 1.0;
87
+ let reasons = [];
88
+ const metadata = result.metadata || {};
89
+ if (task_id && metadata.task_id === task_id) {
90
+ boost += 0.5;
91
+ reasons.push("Task Match");
92
+ }
93
+ if (session_id && metadata.session_id === session_id) {
94
+ boost += 0.3;
95
+ reasons.push("Session Match");
96
+ }
97
+ if (boost > 1.0) {
98
+ // Cap the score at 1.0 to stay within standard search score range
99
+ const newScore = Math.min(1.0, result.score * boost);
100
+ return {
101
+ ...result,
102
+ score: newScore,
103
+ explanation: (typeof result.explanation === 'string' ? result.explanation : JSON.stringify(result.explanation)) +
104
+ ` | Context Boost (x${boost.toFixed(1)}): ${reasons.join(', ')}`
105
+ };
106
+ }
107
+ return result;
108
+ });
109
+ }
110
+ async applyReranking(query, results) {
111
+ if (results.length <= 1)
112
+ return results;
113
+ console.error(`[HybridSearch] Reranking ${results.length} candidates...`);
114
+ const documents = results.map(r => {
115
+ const parts = [
116
+ r.name ? `Name: ${r.name}` : '',
117
+ r.type ? `Type: ${r.type}` : '',
118
+ r.text ? `Description: ${r.text}` : '',
119
+ r.metadata ? `Details: ${JSON.stringify(r.metadata)}` : ''
120
+ ].filter(p => p !== '');
121
+ return parts.join(' | ');
122
+ });
123
+ try {
124
+ const rerankedOrder = await this.rerankerService.rerank(query, documents);
125
+ return rerankedOrder.map((item, i) => {
126
+ const original = results[item.index];
127
+ return {
128
+ ...original,
129
+ score: (item.score + 1.0) / 2.0, // Normalize to 0-1 range if it's logits, or just use as is
130
+ explanation: (typeof original.explanation === 'string' ? original.explanation : JSON.stringify(original.explanation)) +
131
+ ` | Reranked (Rank ${i + 1}, Cross-Encoder Score: ${item.score.toFixed(4)})`
132
+ };
133
+ });
134
+ }
135
+ catch (e) {
136
+ console.error(`[HybridSearch] Reranking failed, returning original results:`, e);
137
+ return results;
138
+ }
139
+ }
78
140
  async advancedSearch(options) {
79
141
  console.error("[HybridSearch] Starting advancedSearch with options:", JSON.stringify(options, null, 2));
80
142
  const { query, limit = 10, filters, graphConstraints, vectorParams } = options;
@@ -181,10 +243,6 @@ class HybridSearch {
181
243
  `:sort -score`,
182
244
  `:limit $limit`
183
245
  ].join('\n').trim();
184
- console.error('--- DEBUG: Cozo Datalog Query ---');
185
- console.error(datalogQuery);
186
- console.error('--- DEBUG: Params ---');
187
- console.error(JSON.stringify(params, null, 2));
188
246
  try {
189
247
  const results = await this.db.run(datalogQuery, params);
190
248
  let searchResults = results.rows.map((r) => ({
@@ -211,7 +269,14 @@ class HybridSearch {
211
269
  return Object.entries(filters.metadata).every(([key, val]) => r.metadata[key] === val);
212
270
  });
213
271
  }
214
- const finalResults = this.applyTimeDecay(searchResults);
272
+ const timeDecayedResults = this.applyTimeDecay(searchResults);
273
+ const finalResults = this.applyContextBoost(timeDecayedResults, options);
274
+ // Phase 3: Reranking
275
+ if (options.rerank) {
276
+ const rerankedResults = await this.applyReranking(options.query, finalResults);
277
+ await this.updateCache(options, queryEmbedding, rerankedResults);
278
+ return rerankedResults;
279
+ }
215
280
  await this.updateCache(options, queryEmbedding, finalResults);
216
281
  return finalResults;
217
282
  }
@@ -300,7 +365,6 @@ class HybridSearch {
300
365
  :sort -score
301
366
  :limit $limit
302
367
  `.trim();
303
- console.error("[HybridSearch] Graph-RAG Datalog Query:\n", datalogQuery);
304
368
  try {
305
369
  const results = await this.db.run(datalogQuery, params);
306
370
  let searchResults = results.rows.map((r) => ({
@@ -330,7 +394,11 @@ class HybridSearch {
330
394
  return Object.entries(filters.metadata).every(([key, val]) => r.metadata[key] === val);
331
395
  });
332
396
  }
333
- return this.applyTimeDecay(searchResults);
397
+ const decayedResults = this.applyTimeDecay(searchResults);
398
+ if (options.rerank) {
399
+ return await this.applyReranking(options.query, decayedResults);
400
+ }
401
+ return decayedResults;
334
402
  }
335
403
  catch (e) {
336
404
  console.error("[HybridSearch] Error in graphRag:", e.message);
@@ -414,7 +482,8 @@ No markdown, no explanation. Just the JSON.`;
414
482
  filters: {
415
483
  ...options.filters,
416
484
  entityTypes: ["CommunitySummary"]
417
- }
485
+ },
486
+ rerank: options.rerank
418
487
  });
419
488
  // If no community summaries found, fallback to standard search
420
489
  if (results.length === 0) {
@@ -437,5 +506,15 @@ No markdown, no explanation. Just the JSON.`;
437
506
  }
438
507
  }));
439
508
  }
509
+ async clearCache() {
510
+ this.searchCache.clear();
511
+ try {
512
+ await this.db.run(`{ ?[query_hash] := *search_cache{query_hash} :rm search_cache {query_hash} }`);
513
+ console.error("[HybridSearch] Cache cleared successfully.");
514
+ }
515
+ catch (e) {
516
+ console.error("[HybridSearch] Error clearing cache:", e);
517
+ }
518
+ }
440
519
  }
441
520
  exports.HybridSearch = HybridSearch;