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
text/typescript
/**
* 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' };
}
}