/** * Embedding generation using Gemini API * * Converts text into vector embeddings for semantic search. */ import { GoogleGenerativeAI } from '@google/generative-ai'; const GEMINI_API_KEY = process.env.GEMINI_API_KEY; if (!GEMINI_API_KEY) { console.warn('[Embeddings] GEMINI_API_KEY not set - embedding functions will fail'); } const genAI = GEMINI_API_KEY ? new GoogleGenerativeAI(GEMINI_API_KEY) : null; // Gemini embedding model - text-embedding-004 produces 768-dim embeddings // Adjust EMBEDDING_DIMENSION in knowledge-chunks-schema.sql if using different model const EMBEDDING_MODEL = 'text-embedding-004'; const EMBEDDING_DIMENSION = 768; /** * Generate embedding for a single text string * * @param text - Input text to embed * @returns Vector embedding as array of numbers * * @throws Error if Gemini API is not configured or request fails */ export async function embedText(text: string): Promise { if (!genAI) { throw new Error('GEMINI_API_KEY not configured - cannot generate embeddings'); } if (!text || text.trim().length === 0) { throw new Error('Cannot embed empty text'); } try { const model = genAI.getGenerativeModel({ model: EMBEDDING_MODEL }); const result = await model.embedContent(text); const embedding = result.embedding; if (!embedding || !embedding.values || embedding.values.length === 0) { throw new Error('Gemini returned empty embedding'); } // Verify dimension matches expectation if (embedding.values.length !== EMBEDDING_DIMENSION) { console.warn( `[Embeddings] Unexpected dimension: got ${embedding.values.length}, expected ${EMBEDDING_DIMENSION}` ); } return embedding.values; } catch (error) { console.error('[Embeddings] Failed to embed text:', error); throw new Error( `Embedding generation failed: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Generate embeddings for multiple texts in batch * * More efficient than calling embedText() repeatedly. * Processes texts sequentially to avoid rate limiting. * * @param texts - Array of texts to embed * @param options - Batch processing options * @returns Array of embeddings (same order as input texts) * * @example * ```typescript * const chunks = ["First chunk...", "Second chunk...", "Third chunk..."]; * const embeddings = await embedTextBatch(chunks); * // embeddings[0] corresponds to chunks[0], etc. * ``` */ export async function embedTextBatch( texts: string[], options: { delayMs?: number; skipEmpty?: boolean } = {} ): Promise { const { delayMs = 100, skipEmpty = true } = options; if (texts.length === 0) { return []; } const embeddings: number[][] = []; for (let i = 0; i < texts.length; i++) { const text = texts[i]; // Skip empty texts if requested if (skipEmpty && (!text || text.trim().length === 0)) { console.warn(`[Embeddings] Skipping empty text at index ${i}`); embeddings.push(new Array(EMBEDDING_DIMENSION).fill(0)); // Zero vector for empty continue; } try { const embedding = await embedText(text); embeddings.push(embedding); // Add delay between requests to avoid rate limiting (except for last item) if (i < texts.length - 1 && delayMs > 0) { await new Promise((resolve) => setTimeout(resolve, delayMs)); } } catch (error) { console.error(`[Embeddings] Failed to embed text at index ${i}:`, error); // Push zero vector as fallback embeddings.push(new Array(EMBEDDING_DIMENSION).fill(0)); } } console.log(`[Embeddings] Generated ${embeddings.length} embeddings`); return embeddings; } /** * Compute cosine similarity between two embeddings * * @param a - First embedding vector * @param b - Second embedding vector * @returns Cosine similarity score (0-1, higher = more similar) */ export function cosineSimilarity(a: number[], b: number[]): number { if (a.length !== b.length) { throw new Error('Embedding dimensions do not match'); } 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 magnitude = Math.sqrt(normA) * Math.sqrt(normB); if (magnitude === 0) { return 0; } return dotProduct / magnitude; } /** * Get the expected embedding dimension for the current model */ export function getEmbeddingDimension(): number { return EMBEDDING_DIMENSION; } /** * Check if embeddings API is configured and working */ export async function checkEmbeddingsHealth(): Promise { try { const testEmbedding = await embedText('health check'); return testEmbedding.length === EMBEDDING_DIMENSION; } catch (error) { console.error('[Embeddings Health Check] Failed:', error); return false; } }