UNPKG

@vfarcic/dot-ai

Version:

AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance

396 lines (395 loc) 15.8 kB
"use strict"; /** * Embedding Service * * Optional semantic search enhancement for pattern matching. * Gracefully falls back to keyword-only search when embedding providers are not available. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.EmbeddingService = exports.VercelEmbeddingProvider = exports.EMBEDDING_PROVIDERS = void 0; const amazon_bedrock_1 = require("@ai-sdk/amazon-bedrock"); const google_1 = require("@ai-sdk/google"); const openai_1 = require("@ai-sdk/openai"); const ai_1 = require("ai"); const ai_retry_config_1 = require("./ai-retry-config"); const circuit_breaker_1 = require("./circuit-breaker"); const tracing_1 = require("./tracing"); /** * Module-level circuit breaker for embedding API calls. * Shared across all EmbeddingService instances since they hit the same API. * Opens after 3 consecutive failures, blocks for 30s before testing recovery. */ const embeddingCircuitBreaker = new circuit_breaker_1.CircuitBreaker('embedding-api', { failureThreshold: 3, // Open after 3 consecutive failures cooldownPeriodMs: 30000, // 30s cooldown before half-open halfOpenMaxAttempts: 1, // Allow 1 test request in half-open }); /** * Supported embedding providers - single source of truth */ exports.EMBEDDING_PROVIDERS = [ 'openai', 'google', 'amazon_bedrock', ]; /** * Unified Vercel AI SDK Embedding Provider * Supports OpenAI, Google, and Amazon Bedrock through Vercel AI SDK */ class VercelEmbeddingProvider { providerType; apiKey; model; dimensions; available; modelInstance; constructor(config) { this.providerType = config.provider; this.available = false; // Get API key based on provider switch (this.providerType) { case 'openai': { this.apiKey = config.apiKey || process.env.CUSTOM_EMBEDDINGS_API_KEY || process.env.OPENAI_API_KEY || ''; this.model = config.model || process.env.EMBEDDINGS_MODEL || 'text-embedding-3-small'; const envDimensions = process.env.EMBEDDINGS_DIMENSIONS ? parseInt(process.env.EMBEDDINGS_DIMENSIONS, 10) : undefined; this.dimensions = config.dimensions || (Number.isFinite(envDimensions) ? envDimensions : 1536); break; } case 'google': this.apiKey = config.apiKey || process.env.GOOGLE_API_KEY || ''; this.model = config.model || process.env.EMBEDDINGS_MODEL || 'gemini-embedding-001'; this.dimensions = config.dimensions || 768; break; case 'amazon_bedrock': // AWS SDK handles credentials automatically - no API key needed this.apiKey = 'bedrock-uses-aws-credentials'; this.model = config.model || process.env.EMBEDDINGS_MODEL || 'amazon.titan-embed-text-v2:0'; this.dimensions = config.dimensions || 1024; // Titan v2 default break; } if (!this.apiKey) { this.available = false; return; } try { // Initialize model instance based on provider switch (this.providerType) { case 'openai': { const baseURL = process.env.CUSTOM_EMBEDDINGS_BASE_URL; const openai = (0, openai_1.createOpenAI)({ apiKey: this.apiKey, ...(baseURL && { baseURL }), }); this.modelInstance = openai.textEmbedding(this.model); break; } case 'google': // Set environment variable that Google SDK expects process.env.GOOGLE_GENERATIVE_AI_API_KEY = this.apiKey; this.modelInstance = google_1.google.textEmbedding(this.model); break; case 'amazon_bedrock': { // AWS SDK automatically uses credential chain: // 1. Environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION) // 2. ~/.aws/credentials file // 3. IAM roles (EC2 instance profiles, ECS roles, EKS service accounts) const bedrock = (0, amazon_bedrock_1.createAmazonBedrock)({ region: process.env.AWS_REGION || 'us-east-1', }); this.modelInstance = bedrock.textEmbeddingModel(this.model); break; } } this.available = true; } catch { this.available = false; } } async generateEmbedding(text) { if (!this.isAvailable()) { throw new Error(`${this.providerType} embedding provider not available`); } if (!text || text.trim().length === 0) { throw new Error('Text cannot be empty for embedding generation'); } // Wrap embed() call with OpenTelemetry tracing return await (0, tracing_1.withAITracing)({ provider: this.providerType, model: this.model, operation: 'embeddings', }, async () => { try { // Execute through circuit breaker to prevent cascading failures return await embeddingCircuitBreaker.execute(async () => { const embedOptions = { model: this.modelInstance, value: text.trim(), // Configurable retry budget; embeddings default to higher resilience. maxRetries: (0, ai_retry_config_1.getMaxRetries)('embeddings'), }; // Add Google-specific options if (this.providerType === 'google') { embedOptions.providerOptions = { google: { outputDimensionality: this.dimensions, taskType: 'SEMANTIC_SIMILARITY', }, }; } const result = await (0, ai_1.embed)(embedOptions); return result.embedding; }); } catch (error) { // Convert CircuitOpenError to descriptive message if (error instanceof circuit_breaker_1.CircuitOpenError) { throw new Error(`Embedding API circuit open: ${error.message}`, { cause: error, }); } if (error instanceof Error) { throw new Error(`${this.providerType} embedding failed: ${error.message}`, { cause: error }); } throw new Error(`${this.providerType} embedding failed: ${String(error)}`, { cause: error }); } }, embedding => ({ embeddingCount: 1, embeddingDimensions: embedding.length, })); } async generateEmbeddings(texts) { if (!this.isAvailable()) { throw new Error(`${this.providerType} embedding provider not available`); } if (!texts || texts.length === 0) { return []; } const validTexts = texts.map(t => t?.trim()).filter(t => t && t.length > 0); if (validTexts.length === 0) { return []; } // Wrap batch embed calls with OpenTelemetry tracing return await (0, tracing_1.withAITracing)({ provider: this.providerType, model: this.model, operation: 'embeddings', }, async () => { try { // Execute through circuit breaker to prevent cascading failures return await embeddingCircuitBreaker.execute(async () => { // Single batch request via SDK; handles chunking internally. const embedManyOptions = { model: this.modelInstance, values: validTexts, // Configurable retry budget; embeddings default to higher resilience. maxRetries: (0, ai_retry_config_1.getMaxRetries)('embeddings'), }; // Apply Google-specific options once for the whole batch. if (this.providerType === 'google') { embedManyOptions.providerOptions = { google: { outputDimensionality: this.dimensions, taskType: 'SEMANTIC_SIMILARITY', }, }; } const result = await (0, ai_1.embedMany)(embedManyOptions); return result.embeddings; }); } catch (error) { // Convert CircuitOpenError to descriptive message if (error instanceof circuit_breaker_1.CircuitOpenError) { throw new Error(`Embedding API circuit open: ${error.message}`, { cause: error, }); } if (error instanceof Error) { throw new Error(`${this.providerType} batch embedding failed: ${error.message}`, { cause: error }); } throw new Error(`${this.providerType} batch embedding failed: ${String(error)}`, { cause: error }); } }, embeddings => ({ embeddingCount: embeddings.length, embeddingDimensions: embeddings[0]?.length || this.dimensions, })); } isAvailable() { return this.available; } getDimensions() { return this.dimensions; } getModel() { return this.model; } getProviderType() { return this.providerType; } } exports.VercelEmbeddingProvider = VercelEmbeddingProvider; /** * Factory function to create embedding provider based on configuration */ function createEmbeddingProvider(config = {}) { const providerType = (config.provider || process.env.EMBEDDINGS_PROVIDER || 'openai').toLowerCase(); // Validate provider type using centralized list if (!exports.EMBEDDING_PROVIDERS.includes(providerType)) { console.warn(`Unknown embedding provider: ${providerType}, falling back to openai`); return createEmbeddingProvider({ ...config, provider: 'openai' }); } try { const provider = new VercelEmbeddingProvider({ ...config, provider: providerType, }); return provider.isAvailable() ? provider : null; } catch (error) { console.error(`Failed to create ${providerType} embedding provider:`, error); return null; } } /** * Main Embedding Service * Provides optional semantic search capabilities with graceful degradation */ class EmbeddingService { provider; lastBatchFailureLogTime; suppressedBatchFailureCount = 0; static BATCH_FAILURE_LOG_INTERVAL_MS = 30000; constructor(config = {}) { // Use factory to initialize appropriate provider this.provider = createEmbeddingProvider(config); } /** * Generate embedding for text * Throws error if embeddings not available or generation fails */ async generateEmbedding(text) { if (!this.isAvailable()) { throw new Error('Embedding service not available'); } try { return await this.provider.generateEmbedding(text); } catch (error) { // Throw error immediately - no silent fallback throw new Error(`Embedding generation failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error }); } } /** * Generate embeddings for multiple texts (optional enhancement) * Returns empty array if embeddings not available */ async generateEmbeddings(texts) { if (!this.isAvailable()) { return []; } try { return await this.provider.generateEmbeddings(texts); } catch (error) { // Rate-limit fallback warnings to avoid log spam during sustained outages const now = Date.now(); if (!this.lastBatchFailureLogTime || now - this.lastBatchFailureLogTime >= EmbeddingService.BATCH_FAILURE_LOG_INTERVAL_MS) { const suppressed = this.suppressedBatchFailureCount; this.suppressedBatchFailureCount = 0; this.lastBatchFailureLogTime = now; console.warn('Batch embedding generation failed, falling back to keyword search:', error instanceof Error ? error.message : String(error), suppressed > 0 ? `(${suppressed} similar warnings suppressed)` : ''); } else { this.suppressedBatchFailureCount++; } return []; } } /** * Check if semantic search is available */ isAvailable() { return this.provider !== null && this.provider.isAvailable(); } /** * Get embedding dimensions (if available) */ getDimensions() { return this.provider?.getDimensions() || 1536; } /** * Get status information for debugging/logging */ getStatus() { if (this.isAvailable()) { // Get provider type from VercelEmbeddingProvider const providerName = this.provider.getProviderType?.() || 'unknown'; const isCustomEndpoint = providerName === 'openai' && !!process.env.CUSTOM_EMBEDDINGS_BASE_URL; return { available: true, provider: isCustomEndpoint ? 'custom' : providerName, model: isCustomEndpoint && !process.env.EMBEDDINGS_MODEL ? 'custom' : this.provider.getModel(), dimensions: this.provider.getDimensions(), }; } const requestedProvider = process.env.EMBEDDINGS_PROVIDER || 'openai'; const keyMap = { openai: 'OPENAI_API_KEY', google: 'GOOGLE_API_KEY', amazon_bedrock: 'AWS credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION)', }; const requiredKey = keyMap[requestedProvider] || 'OPENAI_API_KEY'; return { available: false, provider: null, reason: `${requiredKey} not set - vector operations will fail`, }; } /** * Create searchable text from pattern data */ createPatternSearchText(pattern) { return [ pattern.description, ...pattern.triggers, pattern.rationale, // Include resource types for better semantic matching ...pattern.suggestedResources.map(r => `kubernetes ${r.toLowerCase()}`), ] .join(' ') .trim(); } /** * Get circuit breaker statistics for monitoring * Returns null if embedding service is not available */ getCircuitBreakerStats() { return embeddingCircuitBreaker.getStats(); } } exports.EmbeddingService = EmbeddingService;