/** * Local Offline Embeddings Service * * Provides 100% offline text embeddings using Transformers.js (WASM-based). * No external API calls. Model is downloaded once and cached locally. * * @module @aitytech/agentkits-memory/embeddings */ import type { EmbeddingGenerator } from '../types.js'; /** * Embedding provider type */ export type EmbeddingProvider = 'transformers' | 'mock'; /** * Local embeddings configuration */ export interface LocalEmbeddingsConfig { /** Provider to use (default: 'transformers') */ provider?: EmbeddingProvider; /** * Model ID for Transformers.js * Default: 'Xenova/multilingual-e5-small' (100+ languages, optimized for retrieval) * Alternative: 'Xenova/paraphrase-multilingual-MiniLM-L12-v2' (50+ languages) * Alternative: 'Xenova/all-MiniLM-L6-v2' (English only, faster) */ modelId?: string; /** Vector dimensions (default: 384) */ dimensions?: number; /** Enable in-memory cache for repeated texts */ cacheEnabled?: boolean; /** Maximum cache size (default: 1000) */ maxCacheSize?: number; /** Show progress during model download */ showProgress?: boolean; /** Custom cache directory for models */ cacheDir?: string; } /** * Embedding result with metadata */ export interface EmbeddingResult { /** The embedding vector */ embedding: Float32Array; /** Time taken in milliseconds */ timeMs: number; /** Whether result was from cache */ cached: boolean; /** Token count (approximate) */ tokenCount?: number; } /** * Local embeddings service statistics */ export interface EmbeddingsStats { /** Total embeddings generated */ totalEmbeddings: number; /** Cache hits */ cacheHits: number; /** Cache misses */ cacheMisses: number; /** Average time per embedding (ms) */ avgTimeMs: number; /** Total time spent (ms) */ totalTimeMs: number; /** Model loaded */ modelLoaded: boolean; /** Provider being used */ provider: EmbeddingProvider; } /** * In-memory LRU Cache for embeddings */ class InMemoryEmbeddingCache { private cache = new Map(); private accessOrder: string[] = []; private maxSize: number; constructor(maxSize: number) { this.maxSize = maxSize; } get(key: string): Float32Array | undefined { const value = this.cache.get(key); if (value) { // Move to end (most recently used) const index = this.accessOrder.indexOf(key); if (index > -1) { this.accessOrder.splice(index, 1); } this.accessOrder.push(key); } return value; } set(key: string, value: Float32Array): void { if (this.cache.has(key)) { this.cache.set(key, value); return; } // Evict if at capacity while (this.cache.size >= this.maxSize && this.accessOrder.length > 0) { const oldest = this.accessOrder.shift(); if (oldest) { this.cache.delete(oldest); } } this.cache.set(key, value); this.accessOrder.push(key); } clear(): void { this.cache.clear(); this.accessOrder = []; } get size(): number { return this.cache.size; } } /** * Mock embedding provider for testing */ function createMockEmbedding(text: string, dimensions: number): Float32Array { const embedding = new Float32Array(dimensions); // Generate deterministic pseudo-random values based on text hash let hash = 0; for (let i = 0; i < text.length; i++) { hash = ((hash << 5) - hash) + text.charCodeAt(i); hash = hash & hash; } for (let i = 0; i < dimensions; i++) { // Use hash to seed pseudo-random generation hash = ((hash << 5) - hash) + i; hash = hash & hash; embedding[i] = (hash % 1000) / 1000 - 0.5; } // Normalize let norm = 0; for (let i = 0; i < dimensions; i++) { norm += embedding[i] * embedding[i]; } norm = Math.sqrt(norm); if (norm > 0) { for (let i = 0; i < dimensions; i++) { embedding[i] /= norm; } } return embedding; } /** * Local Embeddings Service * * Provides offline text embeddings using Transformers.js. * Models are downloaded once and cached locally in ~/.cache/huggingface. */ export class LocalEmbeddingsService { private config: Required; private cache: InMemoryEmbeddingCache | null = null; private pipeline: any = null; private modelLoading: Promise | null = null; private stats = { totalEmbeddings: 0, cacheHits: 0, cacheMisses: 0, totalTimeMs: 0, }; constructor(config: LocalEmbeddingsConfig = {}) { this.config = { provider: config.provider || 'transformers', // Use multilingual-e5-small for best CJK support and retrieval accuracy modelId: config.modelId || 'Xenova/multilingual-e5-small', dimensions: config.dimensions || 384, cacheEnabled: config.cacheEnabled ?? true, maxCacheSize: config.maxCacheSize || 1000, showProgress: config.showProgress ?? false, cacheDir: config.cacheDir || '', }; if (this.config.cacheEnabled) { this.cache = new InMemoryEmbeddingCache(this.config.maxCacheSize); } } /** * Initialize the embeddings service (loads model) */ async initialize(): Promise { if (this.config.provider === 'mock') { return; } if (this.pipeline) { return; } if (this.modelLoading) { await this.modelLoading; return; } this.modelLoading = this.loadModel(); await this.modelLoading; } private async loadModel(): Promise { try { // Dynamic import for Transformers.js const { pipeline } = await import('@xenova/transformers'); const progressCallback = this.config.showProgress ? (progress: { status: string; progress?: number }) => { if (progress.status === 'progress' && progress.progress !== undefined) { process.stderr.write( `\rLoading model: ${Math.round(progress.progress)}%` ); } else if (progress.status === 'done') { process.stderr.write('\rModel loaded successfully. \n'); } } : undefined; this.pipeline = await pipeline('feature-extraction', this.config.modelId, { progress_callback: progressCallback, }); } catch (error) { // If Transformers.js is not available, fall back to mock console.warn( 'Transformers.js not available, falling back to mock embeddings.', 'Install with: npm install @xenova/transformers' ); this.config.provider = 'mock'; } } /** * Generate embedding for text */ async embed(text: string): Promise { const startTime = performance.now(); // Check cache if (this.cache) { const cached = this.cache.get(text); if (cached) { this.stats.cacheHits++; return { embedding: cached, timeMs: performance.now() - startTime, cached: true, }; } this.stats.cacheMisses++; } // Generate embedding let embedding: Float32Array; if (this.config.provider === 'mock') { embedding = createMockEmbedding(text, this.config.dimensions); } else { await this.initialize(); if (!this.pipeline) { // Fall back to mock if model failed to load embedding = createMockEmbedding(text, this.config.dimensions); } else { const output = await this.pipeline(text, { pooling: 'mean', normalize: true, }); embedding = new Float32Array(output.data); } } const timeMs = performance.now() - startTime; // Update stats this.stats.totalEmbeddings++; this.stats.totalTimeMs += timeMs; // Cache result if (this.cache) { this.cache.set(text, embedding); } return { embedding, timeMs, cached: false, }; } /** * Generate embeddings for multiple texts (batch) */ async embedBatch(texts: string[]): Promise { // Process in parallel for better performance return Promise.all(texts.map((text) => this.embed(text))); } /** * Get embedding generator function compatible with ProjectMemoryService */ getGenerator(): EmbeddingGenerator { return async (content: string): Promise => { const result = await this.embed(content); return result.embedding; }; } /** * Get service statistics */ getStats(): EmbeddingsStats { return { totalEmbeddings: this.stats.totalEmbeddings, cacheHits: this.stats.cacheHits, cacheMisses: this.stats.cacheMisses, avgTimeMs: this.stats.totalEmbeddings > 0 ? this.stats.totalTimeMs / this.stats.totalEmbeddings : 0, totalTimeMs: this.stats.totalTimeMs, modelLoaded: this.pipeline !== null || this.config.provider === 'mock', provider: this.config.provider, }; } /** * Clear the embedding cache */ clearCache(): void { if (this.cache) { this.cache.clear(); } } /** * Get vector dimensions */ getDimensions(): number { return this.config.dimensions; } /** * Shutdown and cleanup */ async shutdown(): Promise { this.clearCache(); this.pipeline = null; this.modelLoading = null; } } /** * Create a local embeddings service with default configuration */ export function createLocalEmbeddings( config?: LocalEmbeddingsConfig ): LocalEmbeddingsService { return new LocalEmbeddingsService(config); } /** * Create an embedding generator function for use with ProjectMemoryService * * @example * ```typescript * import { createEmbeddingGenerator } from '@aitytech/agentkits-memory/embeddings'; * import { ProjectMemoryService } from '@aitytech/agentkits-memory'; * * const embeddingGenerator = await createEmbeddingGenerator(); * * const memory = new ProjectMemoryService({ * projectPath: '/path/to/project', * enableVectorIndex: true, * embeddingGenerator, * }); * ``` */ export async function createEmbeddingGenerator( config?: LocalEmbeddingsConfig ): Promise { const service = new LocalEmbeddingsService(config); await service.initialize(); return service.getGenerator(); } // Default export export default LocalEmbeddingsService;