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

378 lines (322 loc) 11.4 kB
/** * RVF Embedding Service - Lightweight Hash-Based Embeddings * * Provides deterministic, sub-millisecond embedding generation using * FNV-1a hash-based vectors. No neural model or external API required. * * Features: * - Deterministic: same input always produces the same embedding * - FNV-1a hash seeding with multi-round mixing * - L2-normalized output vectors * - Sub-millisecond generation (<0.1ms typical) * - RvfEmbeddingCache for binary file persistence * - Zero external dependencies * * Use cases: * - Fast similarity search where relative distances matter more than semantics * - Development and testing without API keys * - Offline environments without neural model access * - Bootstrapping before heavier providers are available * * @module @claude-flow/embeddings */ import { EventEmitter } from 'events'; import type { EmbeddingProvider, EmbeddingResult, BatchEmbeddingResult, IEmbeddingService, EmbeddingEvent, EmbeddingEventListener, NormalizationType, RvfEmbeddingConfig, } from './types.js'; import { normalize } from './normalization.js'; import { RvfEmbeddingCache } from './rvf-embedding-cache.js'; // ============================================================================ // Constants // ============================================================================ /** FNV-1a offset basis (32-bit) */ const FNV_OFFSET_BASIS = 0x811c9dc5; /** FNV-1a prime (32-bit) */ const FNV_PRIME = 0x01000193; /** Default embedding dimensions */ const DEFAULT_DIMENSIONS = 384; /** Default in-memory LRU cache size */ const DEFAULT_CACHE_SIZE = 1000; // ============================================================================ // LRU Cache (lightweight in-memory) // ============================================================================ 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, }; } } // ============================================================================ // RVF Embedding Service // ============================================================================ /** * Lightweight hash-based embedding service. * * Generates deterministic embeddings from text using FNV-1a hashing * with multi-round mixing and L2 normalization. The output is a unit * vector in R^n where n = configured dimensions (default 384). * * Extends EventEmitter and implements IEmbeddingService for drop-in * compatibility with other providers. */ export class RvfEmbeddingService extends EventEmitter implements IEmbeddingService { readonly provider: EmbeddingProvider = 'rvf'; private readonly dimensions: number; private readonly cache: LRUCache<string, Float32Array>; private readonly normalizationType: NormalizationType; private readonly embeddingListeners: Set<EmbeddingEventListener> = new Set(); private persistentCache: RvfEmbeddingCache | null = null; constructor(config: RvfEmbeddingConfig) { super(); this.dimensions = config.dimensions ?? DEFAULT_DIMENSIONS; if (this.dimensions <= 0 || !Number.isInteger(this.dimensions)) { throw new Error(`Invalid dimensions: ${this.dimensions}. Must be a positive integer.`); } this.cache = new LRUCache(config.cacheSize ?? DEFAULT_CACHE_SIZE); this.normalizationType = config.normalization ?? 'none'; // Initialize persistent RVF cache if a path is provided if (config.cachePath) { this.persistentCache = new RvfEmbeddingCache({ cachePath: config.cachePath, maxSize: config.cacheSize ?? 10000, dimensions: this.dimensions, }); } } // -------------------------------------------------------------------------- // IEmbeddingService Implementation // -------------------------------------------------------------------------- /** * Generate an embedding for a single text string. */ async embed(text: string): Promise<EmbeddingResult> { if (typeof text !== 'string') { throw new Error('embed() expects a string argument'); } // Check in-memory cache const cached = this.cache.get(text); if (cached) { this.emitEvent({ type: 'cache_hit', text }); return { embedding: cached, latencyMs: 0, cached: true }; } // Check persistent cache if (this.persistentCache) { const persisted = await this.persistentCache.get(text); if (persisted) { this.cache.set(text, persisted); this.emitEvent({ type: 'cache_hit', text }); return { embedding: persisted, latencyMs: 0, cached: true, persistentCached: true }; } } this.emitEvent({ type: 'embed_start', text }); const startTime = performance.now(); // Generate deterministic embedding const embedding = this.generateHashEmbedding(text); // Apply optional normalization (hash embeddings are already L2-normalized, // but the user may want a different normalization) const normalized = this.applyNormalization(embedding); // Store in caches this.cache.set(text, normalized); if (this.persistentCache) { await this.persistentCache.set(text, normalized); } const latencyMs = performance.now() - startTime; this.emitEvent({ type: 'embed_complete', text, latencyMs }); return { embedding: normalized, latencyMs }; } /** * Generate embeddings for multiple text strings. */ async embedBatch(texts: string[]): Promise<BatchEmbeddingResult> { if (!Array.isArray(texts)) { throw new Error('embedBatch() expects an array of strings'); } 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 }); continue; } // Check persistent cache if (this.persistentCache) { const persisted = await this.persistentCache.get(text); if (persisted) { this.cache.set(text, persisted); embeddings.push(persisted); cacheHits++; this.emitEvent({ type: 'cache_hit', text }); continue; } } const embedding = this.generateHashEmbedding(text); const normalized = this.applyNormalization(embedding); this.cache.set(text, normalized); if (this.persistentCache) { await this.persistentCache.set(text, normalized); } embeddings.push(normalized); } 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, }, }; } 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(); if (this.persistentCache) { await this.persistentCache.close(); } } // -------------------------------------------------------------------------- // Event System // -------------------------------------------------------------------------- addEventListener(listener: EmbeddingEventListener): void { this.embeddingListeners.add(listener); } removeEventListener(listener: EmbeddingEventListener): void { this.embeddingListeners.delete(listener); } private 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); } // -------------------------------------------------------------------------- // Hash Embedding Generation // -------------------------------------------------------------------------- /** * Generate a deterministic embedding from text using FNV-1a hashing. * * Algorithm: * 1. Compute a base FNV-1a hash of the full text. * 2. For each dimension, derive a unique seed by mixing the base hash * with the dimension index using the golden ratio constant. * 3. Apply a sine-based pseudo-random transform to spread values. * 4. L2-normalize the result to produce a unit vector. * * This is deterministic: the same text always yields the same vector. */ private generateHashEmbedding(text: string): Float32Array { const embedding = new Float32Array(this.dimensions); // Compute base FNV-1a hash of the full text let baseHash = FNV_OFFSET_BASIS; for (let i = 0; i < text.length; i++) { baseHash ^= text.charCodeAt(i); baseHash = Math.imul(baseHash, FNV_PRIME) >>> 0; } // Generate each dimension from a mixed seed for (let i = 0; i < this.dimensions; i++) { // Mix dimension index with the base hash using golden ratio constant const seed = (baseHash + Math.imul(i, 0x9E3779B9)) >>> 0; // Use sine for pseudo-random distribution in [-1, 1] range const x = Math.sin(seed) * 43758.5453; embedding[i] = x - Math.floor(x); // fractional part in [0, 1) // Shift to [-0.5, 0.5) for zero-centered distribution embedding[i] -= 0.5; } // L2 normalize to unit vector let norm = 0; for (let i = 0; i < this.dimensions; i++) { norm += embedding[i] * embedding[i]; } norm = Math.sqrt(norm); if (norm > 0) { for (let i = 0; i < this.dimensions; i++) { embedding[i] /= norm; } } return embedding; } /** * Apply user-configured normalization if set. */ private applyNormalization(embedding: Float32Array): Float32Array { if (this.normalizationType === 'none') { return embedding; } return normalize(embedding, { type: this.normalizationType }); } }