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

299 lines 11.7 kB
/** * Vector Database Module * * Provides optional ruvector WASM-accelerated vector operations for: * - Semantic similarity search * - HNSW indexing (150x faster) * - Embedding generation * * Gracefully degrades when ruvector is not installed. * * Created with love by ruv.io */ import os from 'node:os'; import path from 'node:path'; import { randomUUID } from 'node:crypto'; // ============================================================================ // Fallback Implementation (when ruvector not available) // ============================================================================ class FallbackVectorDB { vectors = new Map(); dimensions; constructor(dimensions) { this.dimensions = dimensions; } insert(embedding, id, metadata) { this.vectors.set(id, { embedding, metadata }); } search(query, k = 10) { const results = []; for (const [id, { embedding, metadata }] of this.vectors) { const score = cosineSimilarity(query, embedding); results.push({ id, score, metadata }); } return results .sort((a, b) => b.score - a.score) .slice(0, k); } remove(id) { return this.vectors.delete(id); } size() { return this.vectors.size; } clear() { this.vectors.clear(); } } /** * Compute cosine similarity between two vectors */ function cosineSimilarity(a, b) { if (a.length !== b.length) { throw new Error(`Vector dimension mismatch: ${a.length} vs ${b.length}`); } let dotProduct = 0; let normA = 0; let normB = 0; for (let i = 0; i < a.length; i++) { dotProduct += 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 ? 0 : dotProduct / denom; } /** * Whether the hash-embedding one-time warning has been emitted */ let hashEmbeddingWarned = false; /** * Generate a simple hash-based embedding (fallback when ruvector not available) * WARNING: Produces deterministic vectors with NO semantic meaning. */ function generateHashEmbedding(text, dimensions = 768) { if (!hashEmbeddingWarned) { hashEmbeddingWarned = true; console.warn('[vector-db] Using hash-based pseudo-embeddings (no semantic similarity). ' + 'Install ruvector or @claude-flow/embeddings for real ML embeddings.'); } const embedding = new Float32Array(dimensions); const normalized = text.toLowerCase().trim(); // Simple hash function let hash = 0; for (let i = 0; i < normalized.length; i++) { hash = ((hash << 5) - hash) + normalized.charCodeAt(i); hash = hash & hash; // Convert to 32bit integer } // Generate pseudo-random embedding based on hash for (let i = 0; i < dimensions; i++) { embedding[i] = Math.sin(hash * (i + 1) * 0.001) * 0.5 + 0.5; } // Normalize let norm = 0; for (let i = 0; i < dimensions; i++) { norm += embedding[i] * embedding[i]; } norm = Math.sqrt(norm); for (let i = 0; i < dimensions; i++) { embedding[i] /= norm; } return embedding; } // ============================================================================ // Module State // ============================================================================ let ruvectorModule = null; let loadAttempted = false; let isAvailable = false; // ============================================================================ // Public API // ============================================================================ /** * Attempt to load the ruvector module * Returns true if successfully loaded, false otherwise */ export async function loadRuVector() { if (loadAttempted) { return isAvailable; } loadAttempted = true; try { // Dynamic import to handle missing dependency gracefully const ruvector = await import('ruvector').catch(() => null); // ruvector exports VectorDB class, not createVectorDB function if (ruvector && (typeof ruvector.VectorDB === 'function' || typeof ruvector.VectorDb === 'function')) { // Create adapter module that matches our expected interface const VectorDBClass = ruvector.VectorDB || ruvector.VectorDb; ruvectorModule = { createVectorDB: async (dimensions) => { // Build the HNSW graph with explicit, high-recall parameters instead // of relying on the native layer's undocumented defaults. Measured on // a 384-dim clustered corpus (scripts/benchmark-intelligence.mjs): // - sparser graphs (e.g. m=16/efConstruction=64) are faster but // drop recall@10 to ~0.68 at N=20k — unacceptable for a memory // store, so we use the standard high-recall HNSW setting // m=32/efConstruction=200, which keeps recall@10 ~0.99 while still // scaling sub-linearly (log-graph traversal) above the crossover. // These map onto ruvector's DbOptions.hnsw { m, efConstruction }. // // storagePath: the native ruvector DB takes an exclusive file lock on // its storage path. Without an explicit path it defaults to a shared // file in the CWD (e.g. agentdb.rvf), which is frequently already // held by a running daemon/MCP server. When the lock cannot be // acquired the constructor THROWS, and createVectorDB() below // silently degrades to the O(n) brute-force FallbackVectorDB — so the // "HNSW" index is never actually used. Giving each transient index a // unique path avoids that contention so the real HNSW graph is built. const db = new VectorDBClass({ dimensions, hnswConfig: { m: 32, efConstruction: 200 }, storagePath: path.join(os.tmpdir(), `ruvector-${process.pid}-${randomUUID()}.rvf`), }); // Wrap ruvector's VectorDB to match our interface return { insert: (embedding, id, metadata) => { db.insert({ id, vector: embedding, metadata }); }, search: async (query, k = 10) => { const results = await db.search({ vector: query, k }); return results.map((r) => ({ id: r.id, score: r.score, metadata: r.metadata, })); }, remove: (id) => { db.delete(id); return true; }, size: async () => { const len = await db.len(); return len; }, clear: () => { // Not directly supported - would need to recreate }, }; }, generateEmbedding: (text, dimensions = 768) => { // ruvector may not have this - use fallback return generateHashEmbedding(text, dimensions); }, cosineSimilarity: (a, b) => { return cosineSimilarity(a, b); }, isWASMAccelerated: () => { return ruvector.isWasm?.() ?? false; }, }; isAvailable = true; return true; } } catch { // Silently fail - ruvector is optional } isAvailable = false; return false; } /** * Check if ruvector is available */ export function isRuVectorAvailable() { return isAvailable; } /** * Check if WASM acceleration is enabled */ export function isWASMAccelerated() { if (ruvectorModule && typeof ruvectorModule.isWASMAccelerated === 'function') { return ruvectorModule.isWASMAccelerated(); } return false; } /** * Create a vector database * Uses ruvector HNSW if available, falls back to brute-force search */ export async function createVectorDB(dimensions = 768) { await loadRuVector(); if (ruvectorModule && typeof ruvectorModule.createVectorDB === 'function') { try { return await ruvectorModule.createVectorDB(dimensions); } catch (err) { // Fall back to simple implementation, but make the degradation VISIBLE. // A silent fall-through here turns HNSW into O(n) brute force without any // signal — exactly the failure mode that masked the lock-contention bug. console.warn(`[vector-db] ruvector HNSW init failed, falling back to brute-force search: ${err?.message ?? err}`); } } return new FallbackVectorDB(dimensions); } /** * Generate an embedding for text * Uses ruvector if available, falls back to hash-based embedding * * @returns The embedding vector. When using hash fallback, the returned * Float32Array will have a `_warning` property (non-enumerable) * indicating it lacks semantic meaning. */ export function generateEmbedding(text, dimensions = 768) { if (ruvectorModule && typeof ruvectorModule.generateEmbedding === 'function') { try { return ruvectorModule.generateEmbedding(text, dimensions); } catch { // Fall back to hash-based embedding } } const embedding = generateHashEmbedding(text, dimensions); // Tag the result so consumers can detect it came from hash fallback Object.defineProperty(embedding, '_warning', { value: 'hash-based pseudo-embedding — no semantic similarity', enumerable: false, configurable: true, }); return embedding; } /** * Compute cosine similarity between two vectors */ export function computeSimilarity(a, b) { if (ruvectorModule && typeof ruvectorModule.cosineSimilarity === 'function') { try { return ruvectorModule.cosineSimilarity(a, b); } catch { // Fall back to JS implementation } } return cosineSimilarity(a, b); } /** * Get status information about the ruvector module */ export function getStatus() { if (!isAvailable) { return { available: false, wasmAccelerated: false, backend: 'fallback', }; } // HONESTY (audit docs/reviews/intelligence-system-audit-2026-05-29.md): // ruvector's `isWasm()` does NOT mean "WASM-accelerated" — there is no WASM // HNSW build in this stack. `isWasm()===true` means the do-nothing STUB is // active (search() returns []), i.e. native NAPI failed to load. So // wasmAccelerated===false is the HEALTHY state (native NAPI is the fastest // backend available). We label the stub honestly so a regression into it is // visible instead of being mistaken for a faster mode. const stubActive = isWASMAccelerated(); return { available: true, wasmAccelerated: stubActive, backend: stubActive ? 'ruvector-stub-search-disabled' : 'ruvector-native', }; } //# sourceMappingURL=vector-db.js.map