@sparkleideas/embeddings 3.0.0-alpha.29 → 3.0.0-alpha.56

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.
Files changed (37) hide show
  1. package/dist/__tests__/embedding-service.test.d.ts +2 -0
  2. package/dist/__tests__/embedding-service.test.d.ts.map +1 -0
  3. package/dist/__tests__/embedding-service.test.js +98 -0
  4. package/dist/__tests__/embedding-service.test.js.map +1 -0
  5. package/dist/chunking.d.ts +68 -0
  6. package/dist/chunking.d.ts.map +1 -0
  7. package/dist/chunking.js +251 -0
  8. package/dist/chunking.js.map +1 -0
  9. package/dist/embedding-service.d.ts +207 -0
  10. package/dist/embedding-service.d.ts.map +1 -0
  11. package/dist/embedding-service.js +945 -0
  12. package/dist/embedding-service.js.map +1 -0
  13. package/dist/hyperbolic.d.ts +103 -0
  14. package/dist/hyperbolic.d.ts.map +1 -0
  15. package/dist/hyperbolic.js +343 -0
  16. package/dist/hyperbolic.js.map +1 -0
  17. package/dist/index.d.ts +29 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +33 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/neural-integration.d.ts +203 -0
  22. package/dist/neural-integration.d.ts.map +1 -0
  23. package/dist/neural-integration.js +213 -0
  24. package/dist/neural-integration.js.map +1 -0
  25. package/dist/normalization.d.ts +73 -0
  26. package/dist/normalization.d.ts.map +1 -0
  27. package/dist/normalization.js +192 -0
  28. package/dist/normalization.js.map +1 -0
  29. package/dist/persistent-cache.d.ts +119 -0
  30. package/dist/persistent-cache.d.ts.map +1 -0
  31. package/dist/persistent-cache.js +337 -0
  32. package/dist/persistent-cache.js.map +1 -0
  33. package/dist/types.d.ts +224 -0
  34. package/dist/types.d.ts.map +1 -0
  35. package/dist/types.js +15 -0
  36. package/dist/types.js.map +1 -0
  37. package/package.json +1 -1
@@ -0,0 +1,945 @@
1
+ /**
2
+ * V3 Embedding Service Implementation
3
+ *
4
+ * Production embedding service aligned with agentic-flow@alpha:
5
+ * - OpenAI provider (text-embedding-3-small/large)
6
+ * - Transformers.js provider (local ONNX models)
7
+ * - Mock provider (development/testing)
8
+ *
9
+ * Performance Targets:
10
+ * - Single embedding: <100ms (API), <50ms (local)
11
+ * - Batch embedding: <500ms for 10 items
12
+ * - Cache hit: <1ms
13
+ */
14
+ import { EventEmitter } from 'events';
15
+ import { normalize } from './normalization.js';
16
+ import { PersistentEmbeddingCache } from './persistent-cache.js';
17
+ // ============================================================================
18
+ // LRU Cache Implementation
19
+ // ============================================================================
20
+ class LRUCache {
21
+ maxSize;
22
+ cache = new Map();
23
+ hits = 0;
24
+ misses = 0;
25
+ constructor(maxSize) {
26
+ this.maxSize = maxSize;
27
+ }
28
+ get(key) {
29
+ const value = this.cache.get(key);
30
+ if (value !== undefined) {
31
+ // Move to end (most recently used)
32
+ this.cache.delete(key);
33
+ this.cache.set(key, value);
34
+ this.hits++;
35
+ return value;
36
+ }
37
+ this.misses++;
38
+ return undefined;
39
+ }
40
+ set(key, value) {
41
+ if (this.cache.has(key)) {
42
+ this.cache.delete(key);
43
+ }
44
+ else if (this.cache.size >= this.maxSize) {
45
+ // Remove oldest (first) entry
46
+ const firstKey = this.cache.keys().next().value;
47
+ if (firstKey !== undefined) {
48
+ this.cache.delete(firstKey);
49
+ }
50
+ }
51
+ this.cache.set(key, value);
52
+ }
53
+ clear() {
54
+ this.cache.clear();
55
+ this.hits = 0;
56
+ this.misses = 0;
57
+ }
58
+ get size() {
59
+ return this.cache.size;
60
+ }
61
+ get hitRate() {
62
+ const total = this.hits + this.misses;
63
+ return total > 0 ? this.hits / total : 0;
64
+ }
65
+ getStats() {
66
+ return {
67
+ size: this.cache.size,
68
+ maxSize: this.maxSize,
69
+ hits: this.hits,
70
+ misses: this.misses,
71
+ hitRate: this.hitRate,
72
+ };
73
+ }
74
+ }
75
+ // ============================================================================
76
+ // Base Embedding Service
77
+ // ============================================================================
78
+ class BaseEmbeddingService extends EventEmitter {
79
+ config;
80
+ cache;
81
+ persistentCache = null;
82
+ embeddingListeners = new Set();
83
+ normalizationType;
84
+ constructor(config) {
85
+ super();
86
+ this.config = config;
87
+ this.cache = new LRUCache(config.cacheSize ?? 1000);
88
+ this.normalizationType = config.normalization ?? 'none';
89
+ // Initialize persistent cache if configured
90
+ if (config.persistentCache?.enabled) {
91
+ const pcConfig = config.persistentCache;
92
+ this.persistentCache = new PersistentEmbeddingCache({
93
+ dbPath: pcConfig.dbPath ?? '.cache/embeddings.db',
94
+ maxSize: pcConfig.maxSize ?? 10000,
95
+ ttlMs: pcConfig.ttlMs,
96
+ });
97
+ }
98
+ }
99
+ /**
100
+ * Apply normalization to embedding if configured
101
+ */
102
+ applyNormalization(embedding) {
103
+ if (this.normalizationType === 'none') {
104
+ return embedding;
105
+ }
106
+ return normalize(embedding, { type: this.normalizationType });
107
+ }
108
+ /**
109
+ * Check persistent cache for embedding
110
+ */
111
+ async checkPersistentCache(text) {
112
+ if (!this.persistentCache)
113
+ return null;
114
+ return this.persistentCache.get(text);
115
+ }
116
+ /**
117
+ * Store embedding in persistent cache
118
+ */
119
+ async storePersistentCache(text, embedding) {
120
+ if (!this.persistentCache)
121
+ return;
122
+ await this.persistentCache.set(text, embedding);
123
+ }
124
+ emitEvent(event) {
125
+ for (const listener of this.embeddingListeners) {
126
+ try {
127
+ listener(event);
128
+ }
129
+ catch (error) {
130
+ console.error('Error in embedding event listener:', error);
131
+ }
132
+ }
133
+ this.emit(event.type, event);
134
+ }
135
+ addEventListener(listener) {
136
+ this.embeddingListeners.add(listener);
137
+ }
138
+ removeEventListener(listener) {
139
+ this.embeddingListeners.delete(listener);
140
+ }
141
+ clearCache() {
142
+ const size = this.cache.size;
143
+ this.cache.clear();
144
+ this.emitEvent({ type: 'cache_eviction', size });
145
+ }
146
+ getCacheStats() {
147
+ const stats = this.cache.getStats();
148
+ return {
149
+ size: stats.size,
150
+ maxSize: stats.maxSize,
151
+ hitRate: stats.hitRate,
152
+ };
153
+ }
154
+ async shutdown() {
155
+ this.clearCache();
156
+ this.embeddingListeners.clear();
157
+ }
158
+ }
159
+ // ============================================================================
160
+ // OpenAI Embedding Service
161
+ // ============================================================================
162
+ export class OpenAIEmbeddingService extends BaseEmbeddingService {
163
+ provider = 'openai';
164
+ apiKey;
165
+ model;
166
+ baseURL;
167
+ timeout;
168
+ maxRetries;
169
+ constructor(config) {
170
+ super(config);
171
+ this.apiKey = config.apiKey;
172
+ this.model = config.model ?? 'text-embedding-3-small';
173
+ this.baseURL = config.baseURL ?? 'https://api.openai.com/v1/embeddings';
174
+ this.timeout = config.timeout ?? 30000;
175
+ this.maxRetries = config.maxRetries ?? 3;
176
+ }
177
+ async embed(text) {
178
+ // Check cache
179
+ const cached = this.cache.get(text);
180
+ if (cached) {
181
+ this.emitEvent({ type: 'cache_hit', text });
182
+ return {
183
+ embedding: cached,
184
+ latencyMs: 0,
185
+ cached: true,
186
+ };
187
+ }
188
+ this.emitEvent({ type: 'embed_start', text });
189
+ const startTime = performance.now();
190
+ try {
191
+ const response = await this.callOpenAI([text]);
192
+ const embedding = new Float32Array(response.data[0].embedding);
193
+ // Cache result
194
+ this.cache.set(text, embedding);
195
+ const latencyMs = performance.now() - startTime;
196
+ this.emitEvent({ type: 'embed_complete', text, latencyMs });
197
+ return {
198
+ embedding,
199
+ latencyMs,
200
+ usage: {
201
+ promptTokens: response.usage?.prompt_tokens ?? 0,
202
+ totalTokens: response.usage?.total_tokens ?? 0,
203
+ },
204
+ };
205
+ }
206
+ catch (error) {
207
+ const message = error instanceof Error ? error.message : 'Unknown error';
208
+ this.emitEvent({ type: 'embed_error', text, error: message });
209
+ throw new Error(`OpenAI embedding failed: ${message}`);
210
+ }
211
+ }
212
+ async embedBatch(texts) {
213
+ this.emitEvent({ type: 'batch_start', count: texts.length });
214
+ const startTime = performance.now();
215
+ // Check cache for each text
216
+ const cached = [];
217
+ const uncached = [];
218
+ texts.forEach((text, index) => {
219
+ const cachedEmbedding = this.cache.get(text);
220
+ if (cachedEmbedding) {
221
+ cached.push({ index, embedding: cachedEmbedding });
222
+ this.emitEvent({ type: 'cache_hit', text });
223
+ }
224
+ else {
225
+ uncached.push({ index, text });
226
+ }
227
+ });
228
+ // Fetch uncached embeddings
229
+ let apiEmbeddings = [];
230
+ let usage = { promptTokens: 0, totalTokens: 0 };
231
+ if (uncached.length > 0) {
232
+ const response = await this.callOpenAI(uncached.map(u => u.text));
233
+ apiEmbeddings = response.data.map(d => new Float32Array(d.embedding));
234
+ // Cache results
235
+ uncached.forEach((item, i) => {
236
+ this.cache.set(item.text, apiEmbeddings[i]);
237
+ });
238
+ usage = {
239
+ promptTokens: response.usage?.prompt_tokens ?? 0,
240
+ totalTokens: response.usage?.total_tokens ?? 0,
241
+ };
242
+ }
243
+ // Reconstruct result array in original order
244
+ const embeddings = new Array(texts.length);
245
+ cached.forEach(c => {
246
+ embeddings[c.index] = c.embedding;
247
+ });
248
+ uncached.forEach((u, i) => {
249
+ embeddings[u.index] = apiEmbeddings[i];
250
+ });
251
+ const totalLatencyMs = performance.now() - startTime;
252
+ this.emitEvent({ type: 'batch_complete', count: texts.length, latencyMs: totalLatencyMs });
253
+ return {
254
+ embeddings,
255
+ totalLatencyMs,
256
+ avgLatencyMs: totalLatencyMs / texts.length,
257
+ usage,
258
+ cacheStats: {
259
+ hits: cached.length,
260
+ misses: uncached.length,
261
+ },
262
+ };
263
+ }
264
+ async callOpenAI(texts) {
265
+ const config = this.config;
266
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
267
+ try {
268
+ const controller = new AbortController();
269
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
270
+ const response = await fetch(this.baseURL, {
271
+ method: 'POST',
272
+ headers: {
273
+ 'Content-Type': 'application/json',
274
+ Authorization: `Bearer ${this.apiKey}`,
275
+ },
276
+ body: JSON.stringify({
277
+ model: this.model,
278
+ input: texts,
279
+ dimensions: config.dimensions,
280
+ }),
281
+ signal: controller.signal,
282
+ });
283
+ clearTimeout(timeoutId);
284
+ if (!response.ok) {
285
+ const error = await response.text();
286
+ throw new Error(`OpenAI API error: ${response.status} - ${error}`);
287
+ }
288
+ return await response.json();
289
+ }
290
+ catch (error) {
291
+ if (attempt === this.maxRetries - 1) {
292
+ throw error;
293
+ }
294
+ // Exponential backoff
295
+ await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 100));
296
+ }
297
+ }
298
+ throw new Error('Max retries exceeded');
299
+ }
300
+ }
301
+ // ============================================================================
302
+ // Transformers.js Embedding Service
303
+ // ============================================================================
304
+ export class TransformersEmbeddingService extends BaseEmbeddingService {
305
+ provider = 'transformers';
306
+ pipeline = null;
307
+ modelName;
308
+ initialized = false;
309
+ constructor(config) {
310
+ super(config);
311
+ this.modelName = config.model ?? 'Xenova/all-MiniLM-L6-v2';
312
+ }
313
+ async initialize() {
314
+ if (this.initialized)
315
+ return;
316
+ try {
317
+ const { pipeline } = await import('@xenova/transformers');
318
+ this.pipeline = await pipeline('feature-extraction', this.modelName);
319
+ this.initialized = true;
320
+ }
321
+ catch (error) {
322
+ throw new Error(`Failed to initialize transformers.js: ${error}`);
323
+ }
324
+ }
325
+ async embed(text) {
326
+ await this.initialize();
327
+ // Check cache
328
+ const cached = this.cache.get(text);
329
+ if (cached) {
330
+ this.emitEvent({ type: 'cache_hit', text });
331
+ return {
332
+ embedding: cached,
333
+ latencyMs: 0,
334
+ cached: true,
335
+ };
336
+ }
337
+ this.emitEvent({ type: 'embed_start', text });
338
+ const startTime = performance.now();
339
+ try {
340
+ const output = await this.pipeline(text, { pooling: 'mean', normalize: true });
341
+ const embedding = new Float32Array(output.data);
342
+ // Cache result
343
+ this.cache.set(text, embedding);
344
+ const latencyMs = performance.now() - startTime;
345
+ this.emitEvent({ type: 'embed_complete', text, latencyMs });
346
+ return {
347
+ embedding,
348
+ latencyMs,
349
+ };
350
+ }
351
+ catch (error) {
352
+ const message = error instanceof Error ? error.message : 'Unknown error';
353
+ this.emitEvent({ type: 'embed_error', text, error: message });
354
+ throw new Error(`Transformers.js embedding failed: ${message}`);
355
+ }
356
+ }
357
+ async embedBatch(texts) {
358
+ await this.initialize();
359
+ this.emitEvent({ type: 'batch_start', count: texts.length });
360
+ const startTime = performance.now();
361
+ const embeddings = [];
362
+ let cacheHits = 0;
363
+ for (const text of texts) {
364
+ const cached = this.cache.get(text);
365
+ if (cached) {
366
+ embeddings.push(cached);
367
+ cacheHits++;
368
+ this.emitEvent({ type: 'cache_hit', text });
369
+ }
370
+ else {
371
+ const output = await this.pipeline(text, { pooling: 'mean', normalize: true });
372
+ const embedding = new Float32Array(output.data);
373
+ this.cache.set(text, embedding);
374
+ embeddings.push(embedding);
375
+ }
376
+ }
377
+ const totalLatencyMs = performance.now() - startTime;
378
+ this.emitEvent({ type: 'batch_complete', count: texts.length, latencyMs: totalLatencyMs });
379
+ return {
380
+ embeddings,
381
+ totalLatencyMs,
382
+ avgLatencyMs: totalLatencyMs / texts.length,
383
+ cacheStats: {
384
+ hits: cacheHits,
385
+ misses: texts.length - cacheHits,
386
+ },
387
+ };
388
+ }
389
+ }
390
+ // ============================================================================
391
+ // Mock Embedding Service
392
+ // ============================================================================
393
+ export class MockEmbeddingService extends BaseEmbeddingService {
394
+ provider = 'mock';
395
+ dimensions;
396
+ simulatedLatency;
397
+ constructor(config = {}) {
398
+ const fullConfig = {
399
+ provider: 'mock',
400
+ dimensions: config.dimensions ?? 384,
401
+ cacheSize: config.cacheSize ?? 1000,
402
+ simulatedLatency: config.simulatedLatency ?? 0,
403
+ enableCache: config.enableCache ?? true,
404
+ };
405
+ super(fullConfig);
406
+ this.dimensions = fullConfig.dimensions;
407
+ this.simulatedLatency = fullConfig.simulatedLatency;
408
+ }
409
+ async embed(text) {
410
+ // Check cache
411
+ const cached = this.cache.get(text);
412
+ if (cached) {
413
+ this.emitEvent({ type: 'cache_hit', text });
414
+ return {
415
+ embedding: cached,
416
+ latencyMs: 0,
417
+ cached: true,
418
+ };
419
+ }
420
+ this.emitEvent({ type: 'embed_start', text });
421
+ const startTime = performance.now();
422
+ // Simulate latency
423
+ if (this.simulatedLatency > 0) {
424
+ await new Promise(resolve => setTimeout(resolve, this.simulatedLatency));
425
+ }
426
+ const embedding = this.hashEmbedding(text);
427
+ this.cache.set(text, embedding);
428
+ const latencyMs = performance.now() - startTime;
429
+ this.emitEvent({ type: 'embed_complete', text, latencyMs });
430
+ return {
431
+ embedding,
432
+ latencyMs,
433
+ };
434
+ }
435
+ async embedBatch(texts) {
436
+ this.emitEvent({ type: 'batch_start', count: texts.length });
437
+ const startTime = performance.now();
438
+ const embeddings = [];
439
+ let cacheHits = 0;
440
+ for (const text of texts) {
441
+ const cached = this.cache.get(text);
442
+ if (cached) {
443
+ embeddings.push(cached);
444
+ cacheHits++;
445
+ }
446
+ else {
447
+ const embedding = this.hashEmbedding(text);
448
+ this.cache.set(text, embedding);
449
+ embeddings.push(embedding);
450
+ }
451
+ }
452
+ const totalLatencyMs = performance.now() - startTime;
453
+ this.emitEvent({ type: 'batch_complete', count: texts.length, latencyMs: totalLatencyMs });
454
+ return {
455
+ embeddings,
456
+ totalLatencyMs,
457
+ avgLatencyMs: totalLatencyMs / texts.length,
458
+ cacheStats: {
459
+ hits: cacheHits,
460
+ misses: texts.length - cacheHits,
461
+ },
462
+ };
463
+ }
464
+ /**
465
+ * Generate deterministic hash-based embedding
466
+ */
467
+ hashEmbedding(text) {
468
+ const embedding = new Float32Array(this.dimensions);
469
+ // Seed with text hash
470
+ let hash = 0;
471
+ for (let i = 0; i < text.length; i++) {
472
+ hash = (hash << 5) - hash + text.charCodeAt(i);
473
+ hash = hash & hash;
474
+ }
475
+ // Generate pseudo-random embedding
476
+ for (let i = 0; i < this.dimensions; i++) {
477
+ const seed = hash + i * 2654435761;
478
+ const x = Math.sin(seed) * 10000;
479
+ embedding[i] = x - Math.floor(x);
480
+ }
481
+ // Normalize to unit vector
482
+ const norm = Math.sqrt(embedding.reduce((sum, v) => sum + v * v, 0));
483
+ for (let i = 0; i < this.dimensions; i++) {
484
+ embedding[i] /= norm;
485
+ }
486
+ return embedding;
487
+ }
488
+ }
489
+ // ============================================================================
490
+ // Agentic-Flow Embedding Service
491
+ // ============================================================================
492
+ /**
493
+ * Agentic-Flow embedding service using OptimizedEmbedder
494
+ *
495
+ * Features:
496
+ * - ONNX-based embeddings with SIMD acceleration
497
+ * - 256-entry LRU cache with FNV-1a hash
498
+ * - 8x loop unrolling for cosine similarity
499
+ * - Pre-allocated buffers (no GC pressure)
500
+ * - 3-4x faster batch processing
501
+ */
502
+ export class AgenticFlowEmbeddingService extends BaseEmbeddingService {
503
+ provider = 'agentic-flow';
504
+ embedder = null;
505
+ initialized = false;
506
+ modelId;
507
+ dimensions;
508
+ embedderCacheSize;
509
+ modelDir;
510
+ autoDownload;
511
+ constructor(config) {
512
+ super(config);
513
+ this.modelId = config.modelId ?? 'all-MiniLM-L6-v2';
514
+ this.dimensions = config.dimensions ?? 384;
515
+ this.embedderCacheSize = config.embedderCacheSize ?? 256;
516
+ this.modelDir = config.modelDir;
517
+ this.autoDownload = config.autoDownload ?? false;
518
+ }
519
+ async initialize() {
520
+ if (this.initialized)
521
+ return;
522
+ let lastError;
523
+ const createEmbedder = async (modulePath) => {
524
+ try {
525
+ // Use file:// protocol for absolute paths
526
+ const importPath = modulePath.startsWith('/') ? `file://${modulePath}` : modulePath;
527
+ const module = await import(/* webpackIgnore: true */ importPath);
528
+ const getOptimizedEmbedder = module.getOptimizedEmbedder || module.default?.getOptimizedEmbedder;
529
+ if (!getOptimizedEmbedder) {
530
+ lastError = new Error(`Module loaded but getOptimizedEmbedder not found`);
531
+ return false;
532
+ }
533
+ // Only include defined values to not override defaults
534
+ const embedderConfig = {
535
+ modelId: this.modelId,
536
+ dimension: this.dimensions,
537
+ cacheSize: this.embedderCacheSize,
538
+ autoDownload: this.autoDownload,
539
+ };
540
+ if (this.modelDir !== undefined) {
541
+ embedderConfig.modelDir = this.modelDir;
542
+ }
543
+ this.embedder = getOptimizedEmbedder(embedderConfig);
544
+ await this.embedder.init();
545
+ this.initialized = true;
546
+ return true;
547
+ }
548
+ catch (error) {
549
+ lastError = error instanceof Error ? error : new Error(String(error));
550
+ return false;
551
+ }
552
+ };
553
+ // Build list of possible module paths to try
554
+ const possiblePaths = [];
555
+ // Try proper package exports first (preferred)
556
+ possiblePaths.push('agentic-flow/embeddings');
557
+ // Try node_modules resolution from different locations (for file:// imports)
558
+ try {
559
+ const path = await import('path');
560
+ const { existsSync } = await import('fs');
561
+ const cwd = process.cwd();
562
+ // Prioritize absolute paths that exist (for file:// import fallback)
563
+ const absolutePaths = [
564
+ path.join(cwd, 'node_modules/agentic-flow/dist/embeddings/optimized-embedder.js'),
565
+ path.join(cwd, '../node_modules/agentic-flow/dist/embeddings/optimized-embedder.js'),
566
+ '/workspaces/claude-flow/node_modules/agentic-flow/dist/embeddings/optimized-embedder.js',
567
+ ];
568
+ for (const p of absolutePaths) {
569
+ if (existsSync(p)) {
570
+ possiblePaths.push(p);
571
+ }
572
+ }
573
+ }
574
+ catch {
575
+ // fs/path module not available
576
+ }
577
+ // Try each path
578
+ for (const modulePath of possiblePaths) {
579
+ if (await createEmbedder(modulePath)) {
580
+ return;
581
+ }
582
+ }
583
+ const errorDetail = lastError?.message ? ` Last error: ${lastError.message}` : '';
584
+ throw new Error(`Failed to initialize agentic-flow embeddings.${errorDetail} ` +
585
+ `Ensure agentic-flow is installed and ONNX model is downloaded: ` +
586
+ `npx agentic-flow@alpha embeddings init`);
587
+ }
588
+ async embed(text) {
589
+ await this.initialize();
590
+ // Check our LRU cache first
591
+ const cached = this.cache.get(text);
592
+ if (cached) {
593
+ this.emitEvent({ type: 'cache_hit', text });
594
+ return {
595
+ embedding: cached,
596
+ latencyMs: 0,
597
+ cached: true,
598
+ };
599
+ }
600
+ this.emitEvent({ type: 'embed_start', text });
601
+ const startTime = performance.now();
602
+ try {
603
+ // Use agentic-flow's optimized embedder (has its own internal cache)
604
+ const embedding = await this.embedder.embed(text);
605
+ // Store in our cache as well
606
+ this.cache.set(text, embedding);
607
+ const latencyMs = performance.now() - startTime;
608
+ this.emitEvent({ type: 'embed_complete', text, latencyMs });
609
+ return {
610
+ embedding,
611
+ latencyMs,
612
+ };
613
+ }
614
+ catch (error) {
615
+ const message = error instanceof Error ? error.message : 'Unknown error';
616
+ this.emitEvent({ type: 'embed_error', text, error: message });
617
+ throw new Error(`Agentic-flow embedding failed: ${message}`);
618
+ }
619
+ }
620
+ async embedBatch(texts) {
621
+ await this.initialize();
622
+ this.emitEvent({ type: 'batch_start', count: texts.length });
623
+ const startTime = performance.now();
624
+ // Check cache for each text
625
+ const cached = [];
626
+ const uncached = [];
627
+ texts.forEach((text, index) => {
628
+ const cachedEmbedding = this.cache.get(text);
629
+ if (cachedEmbedding) {
630
+ cached.push({ index, embedding: cachedEmbedding });
631
+ this.emitEvent({ type: 'cache_hit', text });
632
+ }
633
+ else {
634
+ uncached.push({ index, text });
635
+ }
636
+ });
637
+ // Use optimized batch embedding for uncached texts
638
+ let batchEmbeddings = [];
639
+ if (uncached.length > 0) {
640
+ const uncachedTexts = uncached.map(u => u.text);
641
+ batchEmbeddings = await this.embedder.embedBatch(uncachedTexts);
642
+ // Cache results
643
+ uncached.forEach((item, i) => {
644
+ this.cache.set(item.text, batchEmbeddings[i]);
645
+ });
646
+ }
647
+ // Reconstruct result array in original order
648
+ const embeddings = new Array(texts.length);
649
+ cached.forEach(c => {
650
+ embeddings[c.index] = c.embedding;
651
+ });
652
+ uncached.forEach((u, i) => {
653
+ embeddings[u.index] = batchEmbeddings[i];
654
+ });
655
+ const totalLatencyMs = performance.now() - startTime;
656
+ this.emitEvent({ type: 'batch_complete', count: texts.length, latencyMs: totalLatencyMs });
657
+ return {
658
+ embeddings,
659
+ totalLatencyMs,
660
+ avgLatencyMs: totalLatencyMs / texts.length,
661
+ cacheStats: {
662
+ hits: cached.length,
663
+ misses: uncached.length,
664
+ },
665
+ };
666
+ }
667
+ /**
668
+ * Get combined cache statistics from both our LRU cache and embedder's internal cache
669
+ */
670
+ getCacheStats() {
671
+ const baseStats = super.getCacheStats();
672
+ if (this.embedder && this.embedder.getCacheStats) {
673
+ const embedderStats = this.embedder.getCacheStats();
674
+ return {
675
+ size: baseStats.size + embedderStats.size,
676
+ maxSize: baseStats.maxSize + embedderStats.maxSize,
677
+ hitRate: baseStats.hitRate,
678
+ embedderCache: embedderStats,
679
+ };
680
+ }
681
+ return baseStats;
682
+ }
683
+ async shutdown() {
684
+ if (this.embedder && this.embedder.clearCache) {
685
+ this.embedder.clearCache();
686
+ }
687
+ await super.shutdown();
688
+ }
689
+ }
690
+ // ============================================================================
691
+ // Factory Functions
692
+ // ============================================================================
693
+ /**
694
+ * Check if agentic-flow is available
695
+ */
696
+ async function isAgenticFlowAvailable() {
697
+ try {
698
+ await import('agentic-flow/embeddings');
699
+ return true;
700
+ }
701
+ catch {
702
+ return false;
703
+ }
704
+ }
705
+ /**
706
+ * Auto-install agentic-flow and initialize model
707
+ */
708
+ async function autoInstallAgenticFlow() {
709
+ const { exec } = await import('child_process');
710
+ const { promisify } = await import('util');
711
+ const execAsync = promisify(exec);
712
+ try {
713
+ // Check if already available
714
+ if (await isAgenticFlowAvailable()) {
715
+ return true;
716
+ }
717
+ console.log('[embeddings] Installing agentic-flow@alpha...');
718
+ await execAsync('npm install agentic-flow@alpha --save', { timeout: 120000 });
719
+ // Initialize the model
720
+ console.log('[embeddings] Downloading embedding model...');
721
+ await execAsync('npx agentic-flow@alpha embeddings init', { timeout: 300000 });
722
+ // Verify installation
723
+ return await isAgenticFlowAvailable();
724
+ }
725
+ catch (error) {
726
+ console.warn('[embeddings] Auto-install failed:', error instanceof Error ? error.message : error);
727
+ return false;
728
+ }
729
+ }
730
+ /**
731
+ * Create embedding service based on configuration (sync version)
732
+ * Note: For 'auto' provider or smart fallback, use createEmbeddingServiceAsync
733
+ */
734
+ export function createEmbeddingService(config) {
735
+ switch (config.provider) {
736
+ case 'openai':
737
+ return new OpenAIEmbeddingService(config);
738
+ case 'transformers':
739
+ return new TransformersEmbeddingService(config);
740
+ case 'mock':
741
+ return new MockEmbeddingService(config);
742
+ case 'agentic-flow':
743
+ return new AgenticFlowEmbeddingService(config);
744
+ default:
745
+ console.warn(`Unknown provider, using mock`);
746
+ return new MockEmbeddingService({ provider: 'mock', dimensions: 384 });
747
+ }
748
+ }
749
+ /**
750
+ * Create embedding service with automatic provider detection and fallback
751
+ *
752
+ * Features:
753
+ * - 'auto' provider picks best available: agentic-flow > transformers > mock
754
+ * - Automatic fallback if primary provider fails to initialize
755
+ * - Pre-validates provider availability before returning
756
+ *
757
+ * @example
758
+ * // Auto-select best provider
759
+ * const service = await createEmbeddingServiceAsync({ provider: 'auto' });
760
+ *
761
+ * // Try agentic-flow, fallback to transformers
762
+ * const service = await createEmbeddingServiceAsync({
763
+ * provider: 'agentic-flow',
764
+ * fallback: 'transformers'
765
+ * });
766
+ */
767
+ export async function createEmbeddingServiceAsync(config) {
768
+ const { provider, fallback, autoInstall = true, ...rest } = config;
769
+ // Auto provider selection
770
+ if (provider === 'auto') {
771
+ // Try agentic-flow first (fastest, ONNX-based)
772
+ let agenticFlowAvailable = await isAgenticFlowAvailable();
773
+ // Auto-install if not available and autoInstall is enabled
774
+ if (!agenticFlowAvailable && autoInstall) {
775
+ agenticFlowAvailable = await autoInstallAgenticFlow();
776
+ }
777
+ if (agenticFlowAvailable) {
778
+ try {
779
+ const service = new AgenticFlowEmbeddingService({
780
+ provider: 'agentic-flow',
781
+ modelId: rest.modelId ?? 'all-MiniLM-L6-v2',
782
+ dimensions: rest.dimensions ?? 384,
783
+ cacheSize: rest.cacheSize,
784
+ });
785
+ // Validate it can initialize
786
+ await service.embed('test');
787
+ return service;
788
+ }
789
+ catch {
790
+ // Fall through to next option
791
+ }
792
+ }
793
+ // Try transformers (good quality, built-in)
794
+ try {
795
+ const service = new TransformersEmbeddingService({
796
+ provider: 'transformers',
797
+ model: rest.model ?? 'Xenova/all-MiniLM-L6-v2',
798
+ cacheSize: rest.cacheSize,
799
+ });
800
+ // Validate it can initialize
801
+ await service.embed('test');
802
+ return service;
803
+ }
804
+ catch {
805
+ // Fall through to mock
806
+ }
807
+ // Fallback to mock (always works)
808
+ console.warn('[embeddings] Using mock provider - install agentic-flow or @xenova/transformers for real embeddings');
809
+ return new MockEmbeddingService({
810
+ dimensions: rest.dimensions ?? 384,
811
+ cacheSize: rest.cacheSize,
812
+ });
813
+ }
814
+ // Specific provider with optional fallback
815
+ const createPrimary = () => {
816
+ switch (provider) {
817
+ case 'agentic-flow':
818
+ return new AgenticFlowEmbeddingService({
819
+ provider: 'agentic-flow',
820
+ modelId: rest.modelId ?? 'all-MiniLM-L6-v2',
821
+ dimensions: rest.dimensions ?? 384,
822
+ cacheSize: rest.cacheSize,
823
+ });
824
+ case 'transformers':
825
+ return new TransformersEmbeddingService({
826
+ provider: 'transformers',
827
+ model: rest.model ?? 'Xenova/all-MiniLM-L6-v2',
828
+ cacheSize: rest.cacheSize,
829
+ });
830
+ case 'openai':
831
+ if (!rest.apiKey)
832
+ throw new Error('OpenAI provider requires apiKey');
833
+ return new OpenAIEmbeddingService({
834
+ provider: 'openai',
835
+ apiKey: rest.apiKey,
836
+ dimensions: rest.dimensions,
837
+ cacheSize: rest.cacheSize,
838
+ });
839
+ case 'mock':
840
+ return new MockEmbeddingService({
841
+ dimensions: rest.dimensions ?? 384,
842
+ cacheSize: rest.cacheSize,
843
+ });
844
+ default:
845
+ throw new Error(`Unknown provider: ${provider}`);
846
+ }
847
+ };
848
+ const primary = createPrimary();
849
+ // Try to validate primary provider
850
+ try {
851
+ await primary.embed('test');
852
+ return primary;
853
+ }
854
+ catch (error) {
855
+ if (!fallback) {
856
+ throw error;
857
+ }
858
+ // Try fallback
859
+ console.warn(`[embeddings] Primary provider '${provider}' failed, using fallback '${fallback}'`);
860
+ const fallbackConfig = { ...rest, provider: fallback };
861
+ return createEmbeddingServiceAsync(fallbackConfig);
862
+ }
863
+ }
864
+ /**
865
+ * Convenience function for quick embeddings
866
+ */
867
+ export async function getEmbedding(text, config) {
868
+ const service = createEmbeddingService({
869
+ provider: 'mock',
870
+ dimensions: 384,
871
+ ...config,
872
+ });
873
+ try {
874
+ const result = await service.embed(text);
875
+ return result.embedding;
876
+ }
877
+ finally {
878
+ await service.shutdown();
879
+ }
880
+ }
881
+ // ============================================================================
882
+ // Similarity Functions
883
+ // ============================================================================
884
+ /**
885
+ * Compute cosine similarity between two embeddings
886
+ */
887
+ export function cosineSimilarity(a, b) {
888
+ if (a.length !== b.length) {
889
+ throw new Error('Embedding dimensions must match');
890
+ }
891
+ let dot = 0;
892
+ let normA = 0;
893
+ let normB = 0;
894
+ for (let i = 0; i < a.length; i++) {
895
+ dot += a[i] * b[i];
896
+ normA += a[i] * a[i];
897
+ normB += b[i] * b[i];
898
+ }
899
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
900
+ return denom > 0 ? dot / denom : 0;
901
+ }
902
+ /**
903
+ * Compute Euclidean distance between two embeddings
904
+ */
905
+ export function euclideanDistance(a, b) {
906
+ if (a.length !== b.length) {
907
+ throw new Error('Embedding dimensions must match');
908
+ }
909
+ let sum = 0;
910
+ for (let i = 0; i < a.length; i++) {
911
+ const diff = a[i] - b[i];
912
+ sum += diff * diff;
913
+ }
914
+ return Math.sqrt(sum);
915
+ }
916
+ /**
917
+ * Compute dot product between two embeddings
918
+ */
919
+ export function dotProduct(a, b) {
920
+ if (a.length !== b.length) {
921
+ throw new Error('Embedding dimensions must match');
922
+ }
923
+ let dot = 0;
924
+ for (let i = 0; i < a.length; i++) {
925
+ dot += a[i] * b[i];
926
+ }
927
+ return dot;
928
+ }
929
+ /**
930
+ * Compute similarity using specified metric
931
+ */
932
+ export function computeSimilarity(a, b, metric = 'cosine') {
933
+ switch (metric) {
934
+ case 'cosine':
935
+ return { score: cosineSimilarity(a, b), metric };
936
+ case 'euclidean':
937
+ // Convert distance to similarity (closer = higher score)
938
+ return { score: 1 / (1 + euclideanDistance(a, b)), metric };
939
+ case 'dot':
940
+ return { score: dotProduct(a, b), metric };
941
+ default:
942
+ return { score: cosineSimilarity(a, b), metric: 'cosine' };
943
+ }
944
+ }
945
+ //# sourceMappingURL=embedding-service.js.map