UNPKG

@claude-flow/embeddings

Version:

V3 Embedding Service - OpenAI, Transformers.js, Agentic-Flow (ONNX), Mock providers with hyperbolic embeddings, normalization, and chunking

177 lines (152 loc) • 5.42 kB
/** * Base classes for the embedding-service module — extracted from * embedding-service.ts so sibling provider files (e.g. * ruvector-onnx-embedding-service.ts, future plugin providers) can * extend the base without causing a circular-import TDZ. * * The split happened during ADR-121 Phase 1. Before the split, every * concrete provider lived in embedding-service.ts; introducing a new * provider in a separate file that also imported `BaseEmbeddingService` * from embedding-service.ts created a cycle that crashed at module * load with "Class extends undefined". * * This file has no concrete provider classes — only the abstract base * + the LRU cache it uses. That keeps the dependency graph from any * provider back to base, and never the reverse. */ import { EventEmitter } from 'events'; import type { EmbeddingProvider, EmbeddingConfig, EmbeddingResult, BatchEmbeddingResult, IEmbeddingService, EmbeddingEvent, EmbeddingEventListener, NormalizationType, PersistentCacheConfig, } from './types.js'; import { normalize } from './normalization.js'; import { PersistentEmbeddingCache } from './persistent-cache.js'; // ============================================================================ // LRU Cache Implementation // ============================================================================ export 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) { 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 // ============================================================================ export 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'; 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(); } }