UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

1,170 lines (1,014 loc) 35.2 kB
/** * V3 Embedding Service Implementation * * Production embedding service aligned with agentic-flow@alpha: * - OpenAI provider (text-embedding-3-small/large) * - Transformers.js provider (local ONNX models) * - Mock provider (development/testing) * * Performance Targets: * - Single embedding: <100ms (API), <50ms (local) * - Batch embedding: <500ms for 10 items * - Cache hit: <1ms */ import { EventEmitter } from 'events'; import type { EmbeddingProvider, EmbeddingConfig, OpenAIEmbeddingConfig, TransformersEmbeddingConfig, MockEmbeddingConfig, AgenticFlowEmbeddingConfig, RvfEmbeddingConfig, EmbeddingResult, BatchEmbeddingResult, IEmbeddingService, EmbeddingEvent, EmbeddingEventListener, SimilarityMetric, SimilarityResult, NormalizationType, PersistentCacheConfig, } from './types.js'; import { normalize } from './normalization.js'; import { PersistentEmbeddingCache } from './persistent-cache.js'; import { RvfEmbeddingService } from './rvf-embedding-service.js'; // ============================================================================ // LRU Cache Implementation // ============================================================================ class LRUCache<K, V> { private cache: Map<K, V> = new Map(); private hits = 0; private misses = 0; constructor(private readonly maxSize: number) {} get(key: K): V | undefined { const value = this.cache.get(key); if (value !== undefined) { // Move to end (most recently used) this.cache.delete(key); this.cache.set(key, value); this.hits++; return value; } this.misses++; return undefined; } set(key: K, value: V): void { if (this.cache.has(key)) { this.cache.delete(key); } else if (this.cache.size >= this.maxSize) { // Remove oldest (first) entry const firstKey = this.cache.keys().next().value; if (firstKey !== undefined) { this.cache.delete(firstKey); } } this.cache.set(key, value); } clear(): void { this.cache.clear(); this.hits = 0; this.misses = 0; } get size(): number { return this.cache.size; } get hitRate(): number { const total = this.hits + this.misses; return total > 0 ? this.hits / total : 0; } getStats() { return { size: this.cache.size, maxSize: this.maxSize, hits: this.hits, misses: this.misses, hitRate: this.hitRate, }; } } // ============================================================================ // Base Embedding Service // ============================================================================ abstract class BaseEmbeddingService extends EventEmitter implements IEmbeddingService { abstract readonly provider: EmbeddingProvider; protected cache: LRUCache<string, Float32Array>; protected persistentCache: PersistentEmbeddingCache | null = null; protected embeddingListeners: Set<EmbeddingEventListener> = new Set(); protected normalizationType: NormalizationType; constructor(protected readonly config: EmbeddingConfig) { super(); this.cache = new LRUCache(config.cacheSize ?? 1000); this.normalizationType = config.normalization ?? 'none'; // Initialize persistent cache if configured if (config.persistentCache?.enabled) { const pcConfig: PersistentCacheConfig = config.persistentCache; this.persistentCache = new PersistentEmbeddingCache({ dbPath: pcConfig.dbPath ?? '.cache/embeddings.db', maxSize: pcConfig.maxSize ?? 10000, ttlMs: pcConfig.ttlMs, }); } } abstract embed(text: string): Promise<EmbeddingResult>; abstract embedBatch(texts: string[]): Promise<BatchEmbeddingResult>; /** * Apply normalization to embedding if configured */ protected applyNormalization(embedding: Float32Array): Float32Array { if (this.normalizationType === 'none') { return embedding; } return normalize(embedding, { type: this.normalizationType }); } /** * Check persistent cache for embedding */ protected async checkPersistentCache(text: string): Promise<Float32Array | null> { if (!this.persistentCache) return null; return this.persistentCache.get(text); } /** * Store embedding in persistent cache */ protected async storePersistentCache(text: string, embedding: Float32Array): Promise<void> { if (!this.persistentCache) return; await this.persistentCache.set(text, embedding); } protected emitEvent(event: EmbeddingEvent): void { for (const listener of this.embeddingListeners) { try { listener(event); } catch (error) { console.error('Error in embedding event listener:', error); } } this.emit(event.type, event); } addEventListener(listener: EmbeddingEventListener): void { this.embeddingListeners.add(listener); } removeEventListener(listener: EmbeddingEventListener): void { this.embeddingListeners.delete(listener); } clearCache(): void { const size = this.cache.size; this.cache.clear(); this.emitEvent({ type: 'cache_eviction', size }); } getCacheStats() { const stats = this.cache.getStats(); return { size: stats.size, maxSize: stats.maxSize, hitRate: stats.hitRate, }; } async shutdown(): Promise<void> { this.clearCache(); this.embeddingListeners.clear(); } } // ============================================================================ // OpenAI Embedding Service // ============================================================================ export class OpenAIEmbeddingService extends BaseEmbeddingService { readonly provider: EmbeddingProvider = 'openai'; private readonly apiKey: string; private readonly model: string; private readonly baseURL: string; private readonly timeout: number; private readonly maxRetries: number; constructor(config: OpenAIEmbeddingConfig) { super(config); this.apiKey = config.apiKey; this.model = config.model ?? 'text-embedding-3-small'; this.baseURL = config.baseURL ?? 'https://api.openai.com/v1/embeddings'; this.timeout = config.timeout ?? 30000; this.maxRetries = config.maxRetries ?? 3; } async embed(text: string): Promise<EmbeddingResult> { // Check cache const cached = this.cache.get(text); if (cached) { this.emitEvent({ type: 'cache_hit', text }); return { embedding: cached, latencyMs: 0, cached: true, }; } this.emitEvent({ type: 'embed_start', text }); const startTime = performance.now(); try { const response = await this.callOpenAI([text]); const embedding = new Float32Array(response.data[0].embedding); // Cache result this.cache.set(text, embedding); const latencyMs = performance.now() - startTime; this.emitEvent({ type: 'embed_complete', text, latencyMs }); return { embedding, latencyMs, usage: { promptTokens: response.usage?.prompt_tokens ?? 0, totalTokens: response.usage?.total_tokens ?? 0, }, }; } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; this.emitEvent({ type: 'embed_error', text, error: message }); throw new Error(`OpenAI embedding failed: ${message}`); } } async embedBatch(texts: string[]): Promise<BatchEmbeddingResult> { this.emitEvent({ type: 'batch_start', count: texts.length }); const startTime = performance.now(); // Check cache for each text const cached: Array<{ index: number; embedding: Float32Array }> = []; const uncached: Array<{ index: number; text: string }> = []; texts.forEach((text, index) => { const cachedEmbedding = this.cache.get(text); if (cachedEmbedding) { cached.push({ index, embedding: cachedEmbedding }); this.emitEvent({ type: 'cache_hit', text }); } else { uncached.push({ index, text }); } }); // Fetch uncached embeddings let apiEmbeddings: Float32Array[] = []; let usage = { promptTokens: 0, totalTokens: 0 }; if (uncached.length > 0) { const response = await this.callOpenAI(uncached.map(u => u.text)); apiEmbeddings = response.data.map(d => new Float32Array(d.embedding)); // Cache results uncached.forEach((item, i) => { this.cache.set(item.text, apiEmbeddings[i]); }); usage = { promptTokens: response.usage?.prompt_tokens ?? 0, totalTokens: response.usage?.total_tokens ?? 0, }; } // Reconstruct result array in original order const embeddings: Array<Float32Array> = new Array(texts.length); cached.forEach(c => { embeddings[c.index] = c.embedding; }); uncached.forEach((u, i) => { embeddings[u.index] = apiEmbeddings[i]; }); const totalLatencyMs = performance.now() - startTime; this.emitEvent({ type: 'batch_complete', count: texts.length, latencyMs: totalLatencyMs }); return { embeddings, totalLatencyMs, avgLatencyMs: totalLatencyMs / texts.length, usage, cacheStats: { hits: cached.length, misses: uncached.length, }, }; } private async callOpenAI(texts: string[]): Promise<{ data: Array<{ embedding: number[] }>; usage?: { prompt_tokens: number; total_tokens: number }; }> { const config = this.config as OpenAIEmbeddingConfig; for (let attempt = 0; attempt < this.maxRetries; attempt++) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); const response = await fetch(this.baseURL, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.apiKey}`, }, body: JSON.stringify({ model: this.model, input: texts, dimensions: config.dimensions, }), signal: controller.signal, }); clearTimeout(timeoutId); if (!response.ok) { const error = await response.text(); throw new Error(`OpenAI API error: ${response.status} - ${error}`); } return await response.json() as { data: Array<{ embedding: number[] }>; usage?: { prompt_tokens: number; total_tokens: number }; }; } catch (error) { if (attempt === this.maxRetries - 1) { throw error; } // Exponential backoff await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 100)); } } throw new Error('Max retries exceeded'); } } // ============================================================================ // Transformers.js Embedding Service // ============================================================================ export class TransformersEmbeddingService extends BaseEmbeddingService { readonly provider: EmbeddingProvider = 'transformers'; private pipeline: any = null; private readonly modelName: string; private initialized = false; constructor(config: TransformersEmbeddingConfig) { super(config); this.modelName = config.model ?? 'Xenova/all-MiniLM-L6-v2'; } private async initialize(): Promise<void> { if (this.initialized) return; try { // ADR-094: try @huggingface/transformers first (clears the // protobufjs <7.5.5 critical RCE chain), fall back to legacy // @xenova/transformers for backwards compatibility. const { loadTransformersPipeline } = await import('./transformers-loader.js'); const handle = await loadTransformersPipeline(); if (!handle) { throw new Error( 'No transformers package available. Install @huggingface/transformers (preferred) ' + 'or @xenova/transformers to enable ONNX embeddings.', ); } this.pipeline = await handle.pipeline('feature-extraction', this.modelName); this.initialized = true; } catch (error) { throw new Error(`Failed to initialize transformers pipeline: ${error}`); } } async embed(text: string): Promise<EmbeddingResult> { await this.initialize(); // Check cache const cached = this.cache.get(text); if (cached) { this.emitEvent({ type: 'cache_hit', text }); return { embedding: cached, latencyMs: 0, cached: true, }; } this.emitEvent({ type: 'embed_start', text }); const startTime = performance.now(); try { const output = await this.pipeline(text, { pooling: 'mean', normalize: true }); const embedding = new Float32Array(output.data); // Cache result this.cache.set(text, embedding); const latencyMs = performance.now() - startTime; this.emitEvent({ type: 'embed_complete', text, latencyMs }); return { embedding, latencyMs, }; } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; this.emitEvent({ type: 'embed_error', text, error: message }); throw new Error(`Transformers.js embedding failed: ${message}`); } } async embedBatch(texts: string[]): Promise<BatchEmbeddingResult> { await this.initialize(); this.emitEvent({ type: 'batch_start', count: texts.length }); const startTime = performance.now(); const embeddings: Float32Array[] = []; let cacheHits = 0; for (const text of texts) { const cached = this.cache.get(text); if (cached) { embeddings.push(cached); cacheHits++; this.emitEvent({ type: 'cache_hit', text }); } else { const output = await this.pipeline(text, { pooling: 'mean', normalize: true }); const embedding = new Float32Array(output.data); this.cache.set(text, embedding); embeddings.push(embedding); } } const totalLatencyMs = performance.now() - startTime; this.emitEvent({ type: 'batch_complete', count: texts.length, latencyMs: totalLatencyMs }); return { embeddings, totalLatencyMs, avgLatencyMs: totalLatencyMs / texts.length, cacheStats: { hits: cacheHits, misses: texts.length - cacheHits, }, }; } } // ============================================================================ // Mock Embedding Service // ============================================================================ export class MockEmbeddingService extends BaseEmbeddingService { readonly provider: EmbeddingProvider = 'mock'; private readonly dimensions: number; private readonly simulatedLatency: number; constructor(config: Partial<MockEmbeddingConfig> = {}) { const fullConfig: MockEmbeddingConfig = { provider: 'mock', dimensions: config.dimensions ?? 384, cacheSize: config.cacheSize ?? 1000, simulatedLatency: config.simulatedLatency ?? 0, enableCache: config.enableCache ?? true, }; super(fullConfig); this.dimensions = fullConfig.dimensions!; this.simulatedLatency = fullConfig.simulatedLatency!; } async embed(text: string): Promise<EmbeddingResult> { // Check cache const cached = this.cache.get(text); if (cached) { this.emitEvent({ type: 'cache_hit', text }); return { embedding: cached, latencyMs: 0, cached: true, }; } this.emitEvent({ type: 'embed_start', text }); const startTime = performance.now(); // Simulate latency if (this.simulatedLatency > 0) { await new Promise(resolve => setTimeout(resolve, this.simulatedLatency)); } const embedding = this.hashEmbedding(text); this.cache.set(text, embedding); const latencyMs = performance.now() - startTime; this.emitEvent({ type: 'embed_complete', text, latencyMs }); return { embedding, latencyMs, }; } async embedBatch(texts: string[]): Promise<BatchEmbeddingResult> { this.emitEvent({ type: 'batch_start', count: texts.length }); const startTime = performance.now(); const embeddings: Float32Array[] = []; let cacheHits = 0; for (const text of texts) { const cached = this.cache.get(text); if (cached) { embeddings.push(cached); cacheHits++; } else { const embedding = this.hashEmbedding(text); this.cache.set(text, embedding); embeddings.push(embedding); } } const totalLatencyMs = performance.now() - startTime; this.emitEvent({ type: 'batch_complete', count: texts.length, latencyMs: totalLatencyMs }); return { embeddings, totalLatencyMs, avgLatencyMs: totalLatencyMs / texts.length, cacheStats: { hits: cacheHits, misses: texts.length - cacheHits, }, }; } /** * Generate deterministic hash-based embedding */ private hashEmbedding(text: string): Float32Array { const embedding = new Float32Array(this.dimensions); // Seed with text hash let hash = 0; for (let i = 0; i < text.length; i++) { hash = (hash << 5) - hash + text.charCodeAt(i); hash = hash & hash; } // Generate pseudo-random embedding for (let i = 0; i < this.dimensions; i++) { const seed = hash + i * 2654435761; const x = Math.sin(seed) * 10000; embedding[i] = x - Math.floor(x); } // Normalize to unit vector const norm = Math.sqrt(embedding.reduce((sum, v) => sum + v * v, 0)); for (let i = 0; i < this.dimensions; i++) { embedding[i] /= norm; } return embedding; } } // ============================================================================ // Agentic-Flow Embedding Service // ============================================================================ /** * Agentic-Flow embedding service using OptimizedEmbedder * * Features: * - ONNX-based embeddings with SIMD acceleration * - 256-entry LRU cache with FNV-1a hash * - 8x loop unrolling for cosine similarity * - Pre-allocated buffers (no GC pressure) * - 3-4x faster batch processing */ export class AgenticFlowEmbeddingService extends BaseEmbeddingService { readonly provider: EmbeddingProvider = 'agentic-flow'; private embedder: any = null; private initialized = false; private readonly modelId: string; private readonly dimensions: number; private readonly embedderCacheSize: number; private readonly modelDir: string | undefined; private readonly autoDownload: boolean; constructor(config: AgenticFlowEmbeddingConfig) { super(config); this.modelId = config.modelId ?? 'all-MiniLM-L6-v2'; this.dimensions = config.dimensions ?? 384; this.embedderCacheSize = config.embedderCacheSize ?? 256; this.modelDir = config.modelDir; this.autoDownload = config.autoDownload ?? false; } private async initialize(): Promise<void> { if (this.initialized) return; let lastError: Error | undefined; const createEmbedder = async (modulePath: string): Promise<boolean> => { try { // Use file:// protocol for absolute paths const importPath = modulePath.startsWith('/') ? `file://${modulePath}` : modulePath; const module = await import(/* webpackIgnore: true */ importPath); const getOptimizedEmbedder = module.getOptimizedEmbedder || module.default?.getOptimizedEmbedder; if (!getOptimizedEmbedder) { lastError = new Error(`Module loaded but getOptimizedEmbedder not found`); return false; } // Only include defined values to not override defaults const embedderConfig: Record<string, unknown> = { modelId: this.modelId, dimension: this.dimensions, cacheSize: this.embedderCacheSize, autoDownload: this.autoDownload, }; if (this.modelDir !== undefined) { embedderConfig.modelDir = this.modelDir; } this.embedder = getOptimizedEmbedder(embedderConfig); await this.embedder.init(); this.initialized = true; return true; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); return false; } }; // Build list of possible module paths to try const possiblePaths: string[] = []; // Try proper package exports first (preferred) possiblePaths.push('agentic-flow/embeddings'); // Try node_modules resolution from different locations (for file:// imports) try { const path = await import('path'); const { existsSync } = await import('fs'); const cwd = process.cwd(); // Prioritize absolute paths that exist (for file:// import fallback) const absolutePaths = [ path.join(cwd, 'node_modules/agentic-flow/dist/embeddings/optimized-embedder.js'), path.join(cwd, '../node_modules/agentic-flow/dist/embeddings/optimized-embedder.js'), '/workspaces/claude-flow/node_modules/agentic-flow/dist/embeddings/optimized-embedder.js', ]; for (const p of absolutePaths) { if (existsSync(p)) { possiblePaths.push(p); } } } catch { // fs/path module not available } // Try each path for (const modulePath of possiblePaths) { if (await createEmbedder(modulePath)) { return; } } const errorDetail = lastError?.message ? ` Last error: ${lastError.message}` : ''; throw new Error( `Failed to initialize agentic-flow embeddings.${errorDetail} ` + `Ensure agentic-flow is installed and ONNX model is downloaded: ` + `npx agentic-flow@alpha embeddings init` ); } async embed(text: string): Promise<EmbeddingResult> { await this.initialize(); // Check our LRU cache first const cached = this.cache.get(text); if (cached) { this.emitEvent({ type: 'cache_hit', text }); return { embedding: cached, latencyMs: 0, cached: true, }; } this.emitEvent({ type: 'embed_start', text }); const startTime = performance.now(); try { // Use agentic-flow's optimized embedder (has its own internal cache) const embedding = await this.embedder.embed(text); // Store in our cache as well this.cache.set(text, embedding); const latencyMs = performance.now() - startTime; this.emitEvent({ type: 'embed_complete', text, latencyMs }); return { embedding, latencyMs, }; } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; this.emitEvent({ type: 'embed_error', text, error: message }); throw new Error(`Agentic-flow embedding failed: ${message}`); } } async embedBatch(texts: string[]): Promise<BatchEmbeddingResult> { await this.initialize(); this.emitEvent({ type: 'batch_start', count: texts.length }); const startTime = performance.now(); // Check cache for each text const cached: Array<{ index: number; embedding: Float32Array }> = []; const uncached: Array<{ index: number; text: string }> = []; texts.forEach((text, index) => { const cachedEmbedding = this.cache.get(text); if (cachedEmbedding) { cached.push({ index, embedding: cachedEmbedding }); this.emitEvent({ type: 'cache_hit', text }); } else { uncached.push({ index, text }); } }); // Use optimized batch embedding for uncached texts let batchEmbeddings: Float32Array[] = []; if (uncached.length > 0) { const uncachedTexts = uncached.map(u => u.text); batchEmbeddings = await this.embedder.embedBatch(uncachedTexts); // Cache results uncached.forEach((item, i) => { this.cache.set(item.text, batchEmbeddings[i]); }); } // Reconstruct result array in original order const embeddings: Float32Array[] = new Array(texts.length); cached.forEach(c => { embeddings[c.index] = c.embedding; }); uncached.forEach((u, i) => { embeddings[u.index] = batchEmbeddings[i]; }); const totalLatencyMs = performance.now() - startTime; this.emitEvent({ type: 'batch_complete', count: texts.length, latencyMs: totalLatencyMs }); return { embeddings, totalLatencyMs, avgLatencyMs: totalLatencyMs / texts.length, cacheStats: { hits: cached.length, misses: uncached.length, }, }; } /** * Get combined cache statistics from both our LRU cache and embedder's internal cache */ override getCacheStats() { const baseStats = super.getCacheStats(); if (this.embedder && this.embedder.getCacheStats) { const embedderStats = this.embedder.getCacheStats(); return { size: baseStats.size + embedderStats.size, maxSize: baseStats.maxSize + embedderStats.maxSize, hitRate: baseStats.hitRate, embedderCache: embedderStats, }; } return baseStats; } override async shutdown(): Promise<void> { if (this.embedder && this.embedder.clearCache) { this.embedder.clearCache(); } await super.shutdown(); } } // ============================================================================ // Factory Functions // ============================================================================ /** * Check if agentic-flow is available */ async function isAgenticFlowAvailable(): Promise<boolean> { try { await import('agentic-flow/embeddings'); return true; } catch { return false; } } /** * Auto-install agentic-flow and initialize model */ async function autoInstallAgenticFlow(): Promise<boolean> { const { exec } = await import('child_process'); const { promisify } = await import('util'); const execAsync = promisify(exec); try { // Check if already available if (await isAgenticFlowAvailable()) { return true; } console.log('[embeddings] Installing agentic-flow@alpha...'); await execAsync('npm install agentic-flow@alpha --save', { timeout: 120000 }); // Initialize the model console.log('[embeddings] Downloading embedding model...'); await execAsync('npx agentic-flow@alpha embeddings init', { timeout: 300000 }); // Verify installation return await isAgenticFlowAvailable(); } catch (error) { console.warn('[embeddings] Auto-install failed:', error instanceof Error ? error.message : error); return false; } } /** * Create embedding service based on configuration (sync version) * Note: For 'auto' provider or smart fallback, use createEmbeddingServiceAsync */ export function createEmbeddingService(config: EmbeddingConfig): IEmbeddingService { switch (config.provider) { case 'openai': return new OpenAIEmbeddingService(config as OpenAIEmbeddingConfig); case 'transformers': return new TransformersEmbeddingService(config as TransformersEmbeddingConfig); case 'mock': return new MockEmbeddingService(config as MockEmbeddingConfig); case 'agentic-flow': return new AgenticFlowEmbeddingService(config as AgenticFlowEmbeddingConfig); case 'rvf': return new RvfEmbeddingService(config as RvfEmbeddingConfig); default: throw new Error( `Unknown embedding provider: '${(config as EmbeddingConfig).provider}'. ` + `Use 'agentic-flow' (recommended), 'transformers', 'openai', 'rvf', or 'mock' (tests only).` ); } } /** * Extended config with auto provider option */ export interface AutoEmbeddingConfig { /** Provider: 'auto' will pick best available (agentic-flow > transformers > mock) */ provider: EmbeddingProvider | 'auto'; /** Fallback provider if primary fails */ fallback?: EmbeddingProvider; /** Auto-install agentic-flow if not available (default: true for 'auto' provider) */ autoInstall?: boolean; /** Model ID for agentic-flow */ modelId?: string; /** Model name for transformers */ model?: string; /** Dimensions */ dimensions?: number; /** Cache size */ cacheSize?: number; /** OpenAI API key (required for openai provider) */ apiKey?: string; } /** * Create embedding service with automatic provider detection and fallback * * Features: * - 'auto' provider picks best available: agentic-flow > transformers > mock * - Automatic fallback if primary provider fails to initialize * - Pre-validates provider availability before returning * * @example * // Auto-select best provider * const service = await createEmbeddingServiceAsync({ provider: 'auto' }); * * // Try agentic-flow, fallback to transformers * const service = await createEmbeddingServiceAsync({ * provider: 'agentic-flow', * fallback: 'transformers' * }); */ export async function createEmbeddingServiceAsync( config: AutoEmbeddingConfig ): Promise<IEmbeddingService> { const { provider, fallback, autoInstall = true, ...rest } = config; // Auto provider selection if (provider === 'auto') { // Try RVF first (52KB, always available, fast hash embeddings) try { const service = new RvfEmbeddingService({ provider: 'rvf', dimensions: rest.dimensions ?? 384, cacheSize: rest.cacheSize, }); await service.embed('test'); return service; } catch { /* fall through */ } // Try agentic-flow (fastest neural, ONNX-based) let agenticFlowAvailable = await isAgenticFlowAvailable(); // Auto-install if not available and autoInstall is enabled if (!agenticFlowAvailable && autoInstall) { agenticFlowAvailable = await autoInstallAgenticFlow(); } if (agenticFlowAvailable) { try { const service = new AgenticFlowEmbeddingService({ provider: 'agentic-flow', modelId: rest.modelId ?? 'all-MiniLM-L6-v2', dimensions: rest.dimensions ?? 384, cacheSize: rest.cacheSize, }); // Validate it can initialize await service.embed('test'); return service; } catch { // Fall through to next option } } // Try transformers (good quality, built-in) try { const service = new TransformersEmbeddingService({ provider: 'transformers', model: rest.model ?? 'Xenova/all-MiniLM-L6-v2', cacheSize: rest.cacheSize, }); // Validate it can initialize await service.embed('test'); return service; } catch { // Fall through to mock } // No real provider available — refuse to silently fall back to mock embeddings. throw new Error( "[embeddings] No real embedding provider available for 'auto'. " + 'Install agentic-flow OR @xenova/transformers, OR pass provider:"openai" with apiKey, ' + 'OR explicitly request provider:"mock" if mock embeddings are intentional (tests only).' ); } // Specific provider with optional fallback const createPrimary = (): IEmbeddingService => { switch (provider) { case 'agentic-flow': return new AgenticFlowEmbeddingService({ provider: 'agentic-flow', modelId: rest.modelId ?? 'all-MiniLM-L6-v2', dimensions: rest.dimensions ?? 384, cacheSize: rest.cacheSize, }); case 'transformers': return new TransformersEmbeddingService({ provider: 'transformers', model: rest.model ?? 'Xenova/all-MiniLM-L6-v2', cacheSize: rest.cacheSize, }); case 'openai': if (!rest.apiKey) throw new Error('OpenAI provider requires apiKey'); return new OpenAIEmbeddingService({ provider: 'openai', apiKey: rest.apiKey, dimensions: rest.dimensions, cacheSize: rest.cacheSize, }); case 'rvf': return new RvfEmbeddingService({ provider: 'rvf', dimensions: rest.dimensions ?? 384, cacheSize: rest.cacheSize, }); case 'mock': return new MockEmbeddingService({ dimensions: rest.dimensions ?? 384, cacheSize: rest.cacheSize, }); default: throw new Error(`Unknown provider: ${provider}`); } }; const primary = createPrimary(); // Try to validate primary provider try { await primary.embed('test'); return primary; } catch (error) { if (!fallback) { throw error; } // Try fallback console.warn(`[embeddings] Primary provider '${provider}' failed, using fallback '${fallback}'`); const fallbackConfig: AutoEmbeddingConfig = { ...rest, provider: fallback }; return createEmbeddingServiceAsync(fallbackConfig); } } /** * Convenience function for quick embeddings */ export async function getEmbedding( text: string, config?: Partial<EmbeddingConfig> ): Promise<Float32Array | number[]> { const service = createEmbeddingService({ provider: 'mock', dimensions: 384, ...config, } as EmbeddingConfig); try { const result = await service.embed(text); return result.embedding; } finally { await service.shutdown(); } } // ============================================================================ // Similarity Functions // ============================================================================ /** * Compute cosine similarity between two embeddings */ export function cosineSimilarity( a: Float32Array | number[], b: Float32Array | number[] ): number { if (a.length !== b.length) { throw new Error('Embedding dimensions must match'); } let dot = 0; let normA = 0; let normB = 0; for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; normA += a[i] * a[i]; normB += b[i] * b[i]; } const denom = Math.sqrt(normA) * Math.sqrt(normB); return denom > 0 ? dot / denom : 0; } /** * Compute Euclidean distance between two embeddings */ export function euclideanDistance( a: Float32Array | number[], b: Float32Array | number[] ): number { if (a.length !== b.length) { throw new Error('Embedding dimensions must match'); } let sum = 0; for (let i = 0; i < a.length; i++) { const diff = a[i] - b[i]; sum += diff * diff; } return Math.sqrt(sum); } /** * Compute dot product between two embeddings */ export function dotProduct( a: Float32Array | number[], b: Float32Array | number[] ): number { if (a.length !== b.length) { throw new Error('Embedding dimensions must match'); } let dot = 0; for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; } return dot; } /** * Compute similarity using specified metric */ export function computeSimilarity( a: Float32Array | number[], b: Float32Array | number[], metric: SimilarityMetric = 'cosine' ): SimilarityResult { switch (metric) { case 'cosine': return { score: cosineSimilarity(a, b), metric }; case 'euclidean': // Convert distance to similarity (closer = higher score) return { score: 1 / (1 + euclideanDistance(a, b)), metric }; case 'dot': return { score: dotProduct(a, b), metric }; default: return { score: cosineSimilarity(a, b), metric: 'cosine' }; } }