@sparkleideas/embeddings 3.0.0-alpha.17 → 3.0.0-alpha.27

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.
@@ -19,6 +19,7 @@ import type {
19
19
  OpenAIEmbeddingConfig,
20
20
  TransformersEmbeddingConfig,
21
21
  MockEmbeddingConfig,
22
+ AgenticFlowEmbeddingConfig,
22
23
  EmbeddingResult,
23
24
  BatchEmbeddingResult,
24
25
  IEmbeddingService,
@@ -26,7 +27,11 @@ import type {
26
27
  EmbeddingEventListener,
27
28
  SimilarityMetric,
28
29
  SimilarityResult,
30
+ NormalizationType,
31
+ PersistentCacheConfig,
29
32
  } from './types.js';
33
+ import { normalize } from './normalization.js';
34
+ import { PersistentEmbeddingCache } from './persistent-cache.js';
30
35
 
31
36
  // ============================================================================
32
37
  // LRU Cache Implementation
@@ -98,16 +103,55 @@ class LRUCache<K, V> {
98
103
  abstract class BaseEmbeddingService extends EventEmitter implements IEmbeddingService {
99
104
  abstract readonly provider: EmbeddingProvider;
100
105
  protected cache: LRUCache<string, Float32Array>;
106
+ protected persistentCache: PersistentEmbeddingCache | null = null;
101
107
  protected embeddingListeners: Set<EmbeddingEventListener> = new Set();
108
+ protected normalizationType: NormalizationType;
102
109
 
103
110
  constructor(protected readonly config: EmbeddingConfig) {
104
111
  super();
105
112
  this.cache = new LRUCache(config.cacheSize ?? 1000);
113
+ this.normalizationType = config.normalization ?? 'none';
114
+
115
+ // Initialize persistent cache if configured
116
+ if (config.persistentCache?.enabled) {
117
+ const pcConfig: PersistentCacheConfig = config.persistentCache;
118
+ this.persistentCache = new PersistentEmbeddingCache({
119
+ dbPath: pcConfig.dbPath ?? '.cache/embeddings.db',
120
+ maxSize: pcConfig.maxSize ?? 10000,
121
+ ttlMs: pcConfig.ttlMs,
122
+ });
123
+ }
106
124
  }
107
125
 
108
126
  abstract embed(text: string): Promise<EmbeddingResult>;
109
127
  abstract embedBatch(texts: string[]): Promise<BatchEmbeddingResult>;
110
128
 
129
+ /**
130
+ * Apply normalization to embedding if configured
131
+ */
132
+ protected applyNormalization(embedding: Float32Array): Float32Array {
133
+ if (this.normalizationType === 'none') {
134
+ return embedding;
135
+ }
136
+ return normalize(embedding, { type: this.normalizationType });
137
+ }
138
+
139
+ /**
140
+ * Check persistent cache for embedding
141
+ */
142
+ protected async checkPersistentCache(text: string): Promise<Float32Array | null> {
143
+ if (!this.persistentCache) return null;
144
+ return this.persistentCache.get(text);
145
+ }
146
+
147
+ /**
148
+ * Store embedding in persistent cache
149
+ */
150
+ protected async storePersistentCache(text: string, embedding: Float32Array): Promise<void> {
151
+ if (!this.persistentCache) return;
152
+ await this.persistentCache.set(text, embedding);
153
+ }
154
+
111
155
  protected emitEvent(event: EmbeddingEvent): void {
112
156
  for (const listener of this.embeddingListeners) {
113
157
  try {
@@ -431,10 +475,17 @@ export class MockEmbeddingService extends BaseEmbeddingService {
431
475
  private readonly dimensions: number;
432
476
  private readonly simulatedLatency: number;
433
477
 
434
- constructor(config: MockEmbeddingConfig) {
435
- super(config);
436
- this.dimensions = config.dimensions ?? 384;
437
- this.simulatedLatency = config.simulatedLatency ?? 0;
478
+ constructor(config: Partial<MockEmbeddingConfig> = {}) {
479
+ const fullConfig: MockEmbeddingConfig = {
480
+ provider: 'mock',
481
+ dimensions: config.dimensions ?? 384,
482
+ cacheSize: config.cacheSize ?? 1000,
483
+ simulatedLatency: config.simulatedLatency ?? 0,
484
+ enableCache: config.enableCache ?? true,
485
+ };
486
+ super(fullConfig);
487
+ this.dimensions = fullConfig.dimensions!;
488
+ this.simulatedLatency = fullConfig.simulatedLatency!;
438
489
  }
439
490
 
440
491
  async embed(text: string): Promise<EmbeddingResult> {
@@ -532,12 +583,286 @@ export class MockEmbeddingService extends BaseEmbeddingService {
532
583
  }
533
584
  }
534
585
 
586
+ // ============================================================================
587
+ // Agentic-Flow Embedding Service
588
+ // ============================================================================
589
+
590
+ /**
591
+ * Agentic-Flow embedding service using OptimizedEmbedder
592
+ *
593
+ * Features:
594
+ * - ONNX-based embeddings with SIMD acceleration
595
+ * - 256-entry LRU cache with FNV-1a hash
596
+ * - 8x loop unrolling for cosine similarity
597
+ * - Pre-allocated buffers (no GC pressure)
598
+ * - 3-4x faster batch processing
599
+ */
600
+ export class AgenticFlowEmbeddingService extends BaseEmbeddingService {
601
+ readonly provider: EmbeddingProvider = 'agentic-flow';
602
+ private embedder: any = null;
603
+ private initialized = false;
604
+ private readonly modelId: string;
605
+ private readonly dimensions: number;
606
+ private readonly embedderCacheSize: number;
607
+ private readonly modelDir: string | undefined;
608
+ private readonly autoDownload: boolean;
609
+
610
+ constructor(config: AgenticFlowEmbeddingConfig) {
611
+ super(config);
612
+ this.modelId = config.modelId ?? 'all-MiniLM-L6-v2';
613
+ this.dimensions = config.dimensions ?? 384;
614
+ this.embedderCacheSize = config.embedderCacheSize ?? 256;
615
+ this.modelDir = config.modelDir;
616
+ this.autoDownload = config.autoDownload ?? false;
617
+ }
618
+
619
+ private async initialize(): Promise<void> {
620
+ if (this.initialized) return;
621
+
622
+ let lastError: Error | undefined;
623
+
624
+ const createEmbedder = async (modulePath: string): Promise<boolean> => {
625
+ try {
626
+ // Use file:// protocol for absolute paths
627
+ const importPath = modulePath.startsWith('/') ? `file://${modulePath}` : modulePath;
628
+ const module = await import(/* webpackIgnore: true */ importPath);
629
+ const getOptimizedEmbedder = module.getOptimizedEmbedder || module.default?.getOptimizedEmbedder;
630
+ if (!getOptimizedEmbedder) {
631
+ lastError = new Error(`Module loaded but getOptimizedEmbedder not found`);
632
+ return false;
633
+ }
634
+
635
+ // Only include defined values to not override defaults
636
+ const embedderConfig: Record<string, unknown> = {
637
+ modelId: this.modelId,
638
+ dimension: this.dimensions,
639
+ cacheSize: this.embedderCacheSize,
640
+ autoDownload: this.autoDownload,
641
+ };
642
+ if (this.modelDir !== undefined) {
643
+ embedderConfig.modelDir = this.modelDir;
644
+ }
645
+ this.embedder = getOptimizedEmbedder(embedderConfig);
646
+ await this.embedder.init();
647
+ this.initialized = true;
648
+ return true;
649
+ } catch (error) {
650
+ lastError = error instanceof Error ? error : new Error(String(error));
651
+ return false;
652
+ }
653
+ };
654
+
655
+ // Build list of possible module paths to try
656
+ const possiblePaths: string[] = [];
657
+
658
+ // Try proper package exports first (preferred)
659
+ possiblePaths.push('agentic-flow/embeddings');
660
+
661
+ // Try node_modules resolution from different locations (for file:// imports)
662
+ try {
663
+ const path = await import('path');
664
+ const { existsSync } = await import('fs');
665
+ const cwd = process.cwd();
666
+
667
+ // Prioritize absolute paths that exist (for file:// import fallback)
668
+ const absolutePaths = [
669
+ path.join(cwd, 'node_modules/agentic-flow/dist/embeddings/optimized-embedder.js'),
670
+ path.join(cwd, '../node_modules/agentic-flow/dist/embeddings/optimized-embedder.js'),
671
+ '/workspaces/claude-flow/node_modules/agentic-flow/dist/embeddings/optimized-embedder.js',
672
+ ];
673
+
674
+ for (const p of absolutePaths) {
675
+ if (existsSync(p)) {
676
+ possiblePaths.push(p);
677
+ }
678
+ }
679
+ } catch {
680
+ // fs/path module not available
681
+ }
682
+
683
+ // Try each path
684
+ for (const modulePath of possiblePaths) {
685
+ if (await createEmbedder(modulePath)) {
686
+ return;
687
+ }
688
+ }
689
+
690
+ const errorDetail = lastError?.message ? ` Last error: ${lastError.message}` : '';
691
+ throw new Error(
692
+ `Failed to initialize agentic-flow embeddings.${errorDetail} ` +
693
+ `Ensure agentic-flow is installed and ONNX model is downloaded: ` +
694
+ `npx agentic-flow@alpha embeddings init`
695
+ );
696
+ }
697
+
698
+ async embed(text: string): Promise<EmbeddingResult> {
699
+ await this.initialize();
700
+
701
+ // Check our LRU cache first
702
+ const cached = this.cache.get(text);
703
+ if (cached) {
704
+ this.emitEvent({ type: 'cache_hit', text });
705
+ return {
706
+ embedding: cached,
707
+ latencyMs: 0,
708
+ cached: true,
709
+ };
710
+ }
711
+
712
+ this.emitEvent({ type: 'embed_start', text });
713
+ const startTime = performance.now();
714
+
715
+ try {
716
+ // Use agentic-flow's optimized embedder (has its own internal cache)
717
+ const embedding = await this.embedder.embed(text);
718
+
719
+ // Store in our cache as well
720
+ this.cache.set(text, embedding);
721
+
722
+ const latencyMs = performance.now() - startTime;
723
+ this.emitEvent({ type: 'embed_complete', text, latencyMs });
724
+
725
+ return {
726
+ embedding,
727
+ latencyMs,
728
+ };
729
+ } catch (error) {
730
+ const message = error instanceof Error ? error.message : 'Unknown error';
731
+ this.emitEvent({ type: 'embed_error', text, error: message });
732
+ throw new Error(`Agentic-flow embedding failed: ${message}`);
733
+ }
734
+ }
735
+
736
+ async embedBatch(texts: string[]): Promise<BatchEmbeddingResult> {
737
+ await this.initialize();
738
+
739
+ this.emitEvent({ type: 'batch_start', count: texts.length });
740
+ const startTime = performance.now();
741
+
742
+ // Check cache for each text
743
+ const cached: Array<{ index: number; embedding: Float32Array }> = [];
744
+ const uncached: Array<{ index: number; text: string }> = [];
745
+
746
+ texts.forEach((text, index) => {
747
+ const cachedEmbedding = this.cache.get(text);
748
+ if (cachedEmbedding) {
749
+ cached.push({ index, embedding: cachedEmbedding });
750
+ this.emitEvent({ type: 'cache_hit', text });
751
+ } else {
752
+ uncached.push({ index, text });
753
+ }
754
+ });
755
+
756
+ // Use optimized batch embedding for uncached texts
757
+ let batchEmbeddings: Float32Array[] = [];
758
+ if (uncached.length > 0) {
759
+ const uncachedTexts = uncached.map(u => u.text);
760
+ batchEmbeddings = await this.embedder.embedBatch(uncachedTexts);
761
+
762
+ // Cache results
763
+ uncached.forEach((item, i) => {
764
+ this.cache.set(item.text, batchEmbeddings[i]);
765
+ });
766
+ }
767
+
768
+ // Reconstruct result array in original order
769
+ const embeddings: Float32Array[] = new Array(texts.length);
770
+ cached.forEach(c => {
771
+ embeddings[c.index] = c.embedding;
772
+ });
773
+ uncached.forEach((u, i) => {
774
+ embeddings[u.index] = batchEmbeddings[i];
775
+ });
776
+
777
+ const totalLatencyMs = performance.now() - startTime;
778
+ this.emitEvent({ type: 'batch_complete', count: texts.length, latencyMs: totalLatencyMs });
779
+
780
+ return {
781
+ embeddings,
782
+ totalLatencyMs,
783
+ avgLatencyMs: totalLatencyMs / texts.length,
784
+ cacheStats: {
785
+ hits: cached.length,
786
+ misses: uncached.length,
787
+ },
788
+ };
789
+ }
790
+
791
+ /**
792
+ * Get combined cache statistics from both our LRU cache and embedder's internal cache
793
+ */
794
+ override getCacheStats() {
795
+ const baseStats = super.getCacheStats();
796
+
797
+ if (this.embedder && this.embedder.getCacheStats) {
798
+ const embedderStats = this.embedder.getCacheStats();
799
+ return {
800
+ size: baseStats.size + embedderStats.size,
801
+ maxSize: baseStats.maxSize + embedderStats.maxSize,
802
+ hitRate: baseStats.hitRate,
803
+ embedderCache: embedderStats,
804
+ };
805
+ }
806
+
807
+ return baseStats;
808
+ }
809
+
810
+ override async shutdown(): Promise<void> {
811
+ if (this.embedder && this.embedder.clearCache) {
812
+ this.embedder.clearCache();
813
+ }
814
+ await super.shutdown();
815
+ }
816
+ }
817
+
535
818
  // ============================================================================
536
819
  // Factory Functions
537
820
  // ============================================================================
538
821
 
539
822
  /**
540
- * Create embedding service based on configuration
823
+ * Check if agentic-flow is available
824
+ */
825
+ async function isAgenticFlowAvailable(): Promise<boolean> {
826
+ try {
827
+ await import('agentic-flow/embeddings');
828
+ return true;
829
+ } catch {
830
+ return false;
831
+ }
832
+ }
833
+
834
+ /**
835
+ * Auto-install agentic-flow and initialize model
836
+ */
837
+ async function autoInstallAgenticFlow(): Promise<boolean> {
838
+ const { exec } = await import('child_process');
839
+ const { promisify } = await import('util');
840
+ const execAsync = promisify(exec);
841
+
842
+ try {
843
+ // Check if already available
844
+ if (await isAgenticFlowAvailable()) {
845
+ return true;
846
+ }
847
+
848
+ console.log('[embeddings] Installing agentic-flow@alpha...');
849
+ await execAsync('npm install agentic-flow@alpha --save', { timeout: 120000 });
850
+
851
+ // Initialize the model
852
+ console.log('[embeddings] Downloading embedding model...');
853
+ await execAsync('npx agentic-flow@alpha embeddings init', { timeout: 300000 });
854
+
855
+ // Verify installation
856
+ return await isAgenticFlowAvailable();
857
+ } catch (error) {
858
+ console.warn('[embeddings] Auto-install failed:', error instanceof Error ? error.message : error);
859
+ return false;
860
+ }
861
+ }
862
+
863
+ /**
864
+ * Create embedding service based on configuration (sync version)
865
+ * Note: For 'auto' provider or smart fallback, use createEmbeddingServiceAsync
541
866
  */
542
867
  export function createEmbeddingService(config: EmbeddingConfig): IEmbeddingService {
543
868
  switch (config.provider) {
@@ -547,12 +872,159 @@ export function createEmbeddingService(config: EmbeddingConfig): IEmbeddingServi
547
872
  return new TransformersEmbeddingService(config as TransformersEmbeddingConfig);
548
873
  case 'mock':
549
874
  return new MockEmbeddingService(config as MockEmbeddingConfig);
875
+ case 'agentic-flow':
876
+ return new AgenticFlowEmbeddingService(config as AgenticFlowEmbeddingConfig);
550
877
  default:
551
878
  console.warn(`Unknown provider, using mock`);
552
879
  return new MockEmbeddingService({ provider: 'mock', dimensions: 384 });
553
880
  }
554
881
  }
555
882
 
883
+ /**
884
+ * Extended config with auto provider option
885
+ */
886
+ export interface AutoEmbeddingConfig {
887
+ /** Provider: 'auto' will pick best available (agentic-flow > transformers > mock) */
888
+ provider: EmbeddingProvider | 'auto';
889
+ /** Fallback provider if primary fails */
890
+ fallback?: EmbeddingProvider;
891
+ /** Auto-install agentic-flow if not available (default: true for 'auto' provider) */
892
+ autoInstall?: boolean;
893
+ /** Model ID for agentic-flow */
894
+ modelId?: string;
895
+ /** Model name for transformers */
896
+ model?: string;
897
+ /** Dimensions */
898
+ dimensions?: number;
899
+ /** Cache size */
900
+ cacheSize?: number;
901
+ /** OpenAI API key (required for openai provider) */
902
+ apiKey?: string;
903
+ }
904
+
905
+ /**
906
+ * Create embedding service with automatic provider detection and fallback
907
+ *
908
+ * Features:
909
+ * - 'auto' provider picks best available: agentic-flow > transformers > mock
910
+ * - Automatic fallback if primary provider fails to initialize
911
+ * - Pre-validates provider availability before returning
912
+ *
913
+ * @example
914
+ * // Auto-select best provider
915
+ * const service = await createEmbeddingServiceAsync({ provider: 'auto' });
916
+ *
917
+ * // Try agentic-flow, fallback to transformers
918
+ * const service = await createEmbeddingServiceAsync({
919
+ * provider: 'agentic-flow',
920
+ * fallback: 'transformers'
921
+ * });
922
+ */
923
+ export async function createEmbeddingServiceAsync(
924
+ config: AutoEmbeddingConfig
925
+ ): Promise<IEmbeddingService> {
926
+ const { provider, fallback, autoInstall = true, ...rest } = config;
927
+
928
+ // Auto provider selection
929
+ if (provider === 'auto') {
930
+ // Try agentic-flow first (fastest, ONNX-based)
931
+ let agenticFlowAvailable = await isAgenticFlowAvailable();
932
+
933
+ // Auto-install if not available and autoInstall is enabled
934
+ if (!agenticFlowAvailable && autoInstall) {
935
+ agenticFlowAvailable = await autoInstallAgenticFlow();
936
+ }
937
+
938
+ if (agenticFlowAvailable) {
939
+ try {
940
+ const service = new AgenticFlowEmbeddingService({
941
+ provider: 'agentic-flow',
942
+ modelId: rest.modelId ?? 'all-MiniLM-L6-v2',
943
+ dimensions: rest.dimensions ?? 384,
944
+ cacheSize: rest.cacheSize,
945
+ });
946
+ // Validate it can initialize
947
+ await service.embed('test');
948
+ return service;
949
+ } catch {
950
+ // Fall through to next option
951
+ }
952
+ }
953
+
954
+ // Try transformers (good quality, built-in)
955
+ try {
956
+ const service = new TransformersEmbeddingService({
957
+ provider: 'transformers',
958
+ model: rest.model ?? 'Xenova/all-MiniLM-L6-v2',
959
+ cacheSize: rest.cacheSize,
960
+ });
961
+ // Validate it can initialize
962
+ await service.embed('test');
963
+ return service;
964
+ } catch {
965
+ // Fall through to mock
966
+ }
967
+
968
+ // Fallback to mock (always works)
969
+ console.warn('[embeddings] Using mock provider - install agentic-flow or @xenova/transformers for real embeddings');
970
+ return new MockEmbeddingService({
971
+ dimensions: rest.dimensions ?? 384,
972
+ cacheSize: rest.cacheSize,
973
+ });
974
+ }
975
+
976
+ // Specific provider with optional fallback
977
+ const createPrimary = (): IEmbeddingService => {
978
+ switch (provider) {
979
+ case 'agentic-flow':
980
+ return new AgenticFlowEmbeddingService({
981
+ provider: 'agentic-flow',
982
+ modelId: rest.modelId ?? 'all-MiniLM-L6-v2',
983
+ dimensions: rest.dimensions ?? 384,
984
+ cacheSize: rest.cacheSize,
985
+ });
986
+ case 'transformers':
987
+ return new TransformersEmbeddingService({
988
+ provider: 'transformers',
989
+ model: rest.model ?? 'Xenova/all-MiniLM-L6-v2',
990
+ cacheSize: rest.cacheSize,
991
+ });
992
+ case 'openai':
993
+ if (!rest.apiKey) throw new Error('OpenAI provider requires apiKey');
994
+ return new OpenAIEmbeddingService({
995
+ provider: 'openai',
996
+ apiKey: rest.apiKey,
997
+ dimensions: rest.dimensions,
998
+ cacheSize: rest.cacheSize,
999
+ });
1000
+ case 'mock':
1001
+ return new MockEmbeddingService({
1002
+ dimensions: rest.dimensions ?? 384,
1003
+ cacheSize: rest.cacheSize,
1004
+ });
1005
+ default:
1006
+ throw new Error(`Unknown provider: ${provider}`);
1007
+ }
1008
+ };
1009
+
1010
+ const primary = createPrimary();
1011
+
1012
+ // Try to validate primary provider
1013
+ try {
1014
+ await primary.embed('test');
1015
+ return primary;
1016
+ } catch (error) {
1017
+ if (!fallback) {
1018
+ throw error;
1019
+ }
1020
+
1021
+ // Try fallback
1022
+ console.warn(`[embeddings] Primary provider '${provider}' failed, using fallback '${fallback}'`);
1023
+ const fallbackConfig: AutoEmbeddingConfig = { ...rest, provider: fallback };
1024
+ return createEmbeddingServiceAsync(fallbackConfig);
1025
+ }
1026
+ }
1027
+
556
1028
  /**
557
1029
  * Convenience function for quick embeddings
558
1030
  */