UNPKG

pulse-ai-utils

Version:

Utility functions and helpers for AI-powered applications

402 lines 17 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SupabaseQueryCache = void 0; exports.searchQueryCache = searchQueryCache; const index_1 = require("./index"); const openai_helper_1 = __importDefault(require("../helpers/openai-helper")); /** * Calculate cosine similarity between two vectors */ function cosineSimilarity(a, b) { if (a.length !== b.length) return 0; 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]; } normA = Math.sqrt(normA); normB = Math.sqrt(normB); if (normA === 0 || normB === 0) return 0; return dotProduct / (normA * normB); } /** * Search query cache using vector similarity */ async function searchQueryCache({ query, area, region, country, timeline, lat, lng, radius, limit = 20 }) { try { const openai = new openai_helper_1.default(); const promptEmbedding = await openai.generateEmbedding(query); // Use geographic search if coordinates are provided if (lat !== undefined && lng !== undefined) { console.log('[SearchQueryCache] Using geographic search with coordinates:', lat, lng, radius); // First, get geographically filtered results const { data: geoData, error: geoError } = await index_1.supabase.rpc('search_query_cache_by_location', { center_lat: lat, center_lng: lng, radius: radius || 10, search_area: area, search_region: region || null, search_country: country || null, result_limit: limit * 2 // Get more for filtering }); if (geoError) { console.error('Error in geographic query cache search:', geoError); // Fall back to regular search } else if (geoData && geoData.length > 0) { // Filter by prompt similarity client-side const resultsWithSimilarity = await Promise.all(geoData.map(async (item) => { // Get the stored embedding if available if (item.prompt_embedding) { const storedEmbedding = JSON.parse(item.prompt_embedding); const similarity = cosineSimilarity(promptEmbedding, storedEmbedding); return { ...item, similarity }; } return { ...item, similarity: 0 }; })); // Filter by similarity threshold and sort const filteredResults = resultsWithSimilarity .filter(item => item.similarity >= 0.9) .sort((a, b) => b.similarity - a.similarity) .slice(0, limit); if (filteredResults.length > 0) { return filteredResults.map(item => item.result); } } } // Search both prompt embeddings (query similarity) and response embeddings (content similarity) console.log('[SearchQueryCache] Searching both prompt and response embeddings'); // 1. Search by prompt embedding (similar queries) with high threshold const { data: promptResults, error: promptError } = await index_1.supabase.rpc('search_query_cache_json', { prompt_embedding: JSON.stringify(promptEmbedding), area_filter: area, region_filter: region || null, country_filter: country || null, timeline_filter: timeline || null, similarity_threshold: 0.9 // High threshold for query matching }); if (promptError) { console.error('Error searching query cache by prompt:', promptError); } // 2. Search by result embedding (similar content) with lower threshold const { data: responseResults, error: responseError } = await index_1.supabase.rpc('search_query_cache_by_response', { result_embedding: JSON.stringify(promptEmbedding), // Use same embedding to find relevant content area_filter: area, region_filter: region || null, country_filter: country || null, timeline_filter: timeline || null, similarity_threshold: 0.7 // Lower threshold for content matching }); if (responseError) { console.error('Error searching query cache by response:', responseError); } // 3. Combine and deduplicate results const allResults = []; const seenIds = new Set(); // Add prompt results first (higher priority) if (promptResults) { for (const result of promptResults) { if (!seenIds.has(result.id)) { allResults.push({ ...result, search_type: 'query_match' }); seenIds.add(result.id); } } } // Add response results (content matches) if (responseResults) { for (const result of responseResults) { if (!seenIds.has(result.id)) { allResults.push({ ...result, search_type: 'content_match' }); seenIds.add(result.id); } } } console.log(`[SearchQueryCache] Found ${promptResults?.length || 0} query matches, ${responseResults?.length || 0} content matches, ${allResults.length} total unique results`); return allResults; } catch (error) { console.error('Error in searchQueryCache:', error); return []; } } class SupabaseQueryCache { constructor() { this.openai = new openai_helper_1.default(); } /** * Get cached result using vector similarity search */ async getCachedResult(prompt, location) { try { // Generate embedding for the prompt const promptEmbedding = await this.openai.generateEmbedding(prompt); // Use geographic search if coordinates are provided if (location.lat !== undefined && location.lng !== undefined) { console.log('[SupabaseQueryCache] Using geographic search for cache lookup'); // First, get geographically filtered results const { data: geoData, error: geoError } = await index_1.supabase.rpc('search_query_cache_geographic', { user_lat: location.lat, user_lng: location.lng, input_prompt_embedding: JSON.stringify(promptEmbedding), search_radius: location.radius || 10, similarity_threshold: 0.9 }); if (geoError) { console.error('Error in geographic query cache search:', geoError); // Fall back to regular search } else if (geoData) { // The geographic function already filters by similarity and returns the best match console.log('[SupabaseQueryCache] Geographic cache hit with similarity:', geoData.similarity); return geoData.result; } } // Regular search without geographic filtering const { data, error } = await index_1.supabase.rpc('search_query_cache_json', { prompt_embedding: JSON.stringify(promptEmbedding), // Convert to JSON string area_filter: location.area, region_filter: location.region || null, country_filter: location.country || null, timeline_filter: location.timeline || null, similarity_threshold: 0.9 // Match the 90% threshold from original LLMCache }); if (error) { console.error('Error searching query cache:', error); return null; } if (!data || data.length === 0) { return null; } // Return the most similar result return data[0].result; } catch (error) { console.error('Error in getCachedResult:', error); return null; } } /** * Set cached result with vector embedding */ async setCachedResult(prompt, location, result) { try { // Generate embedding for the prompt const promptEmbedding = await this.openai.generateEmbedding(prompt); // Calculate expiration (24 hours from now) const expireAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // Generate consistent ID: promptHash_area_region_country_date const dateStr = new Date().toISOString().split('T')[0]; const promptHash = this.hashString(prompt); const id = [ promptHash, location.area || 'unknown', location.region || 'noregion', location.country || 'nocountry', dateStr ].join('_'); // Generate geohash if coordinates provided let geohash = null; if (location.lat !== undefined && location.lng !== undefined) { const { data: geohashData } = await index_1.supabase.rpc('generate_geohash', { latitude: location.lat, longitude: location.lng, geohash_precision: 6 }); geohash = geohashData; } // Prepare data for insertion with optimized format const cacheData = { id, // Use consistent ID format prompt, area: location.area, region: location.region || null, country: location.country || null, timeline: location.timeline || null, count: location.count || null, // Handle additional fields lat: location.lat !== undefined ? location.lat : null, lng: location.lng !== undefined ? location.lng : null, radius: location.radius !== undefined ? location.radius : null, geohash: geohash, result, prompt_embedding: JSON.stringify(promptEmbedding), // Serialize for storage expire_at: expireAt.toISOString() // Fixed column name }; // Use upsert for better performance and avoid conflicts const { error } = await index_1.supabase.from('query_cache').upsert(cacheData, { onConflict: 'id' }); if (error) { console.error('Error inserting into query cache:', error); throw error; } } catch (error) { console.error('Error in setCachedResult:', error); throw error; } } /** * Batch insert multiple cache entries for better performance */ async batchSetCachedResults(entries) { try { // Prepare batch data with embeddings const batchData = await Promise.all(entries.map(async (entry) => { const promptEmbedding = await this.openai.generateEmbedding(entry.prompt); const expireAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // Generate consistent ID const dateStr = new Date().toISOString().split('T')[0]; const promptHash = this.hashString(entry.prompt); const id = [ promptHash, entry.location.area || 'unknown', entry.location.region || 'noregion', entry.location.country || 'nocountry', dateStr ].join('_'); return { id, // Use consistent ID format prompt: entry.prompt, area: entry.location.area, region: entry.location.region || null, country: entry.location.country || null, timeline: entry.location.timeline || null, result: entry.result, prompt_embedding: JSON.stringify(promptEmbedding), expire_at: expireAt.toISOString() }; })); // Use batch upsert for better performance const { error } = await index_1.supabase.from('query_cache').upsert(batchData); if (error) { console.error('Error batch inserting cache entries:', error); throw error; } console.log(`Batch inserted ${batchData.length} cache entries`); } catch (error) { console.error('Error in batchSetCachedResults:', error); throw error; } } /** * Search cache by location filters without prompt similarity */ async searchByLocation(location, limit = 10) { try { let query = index_1.supabase .from('query_cache') .select('*') .eq('area', location.area) .gte('expire_at', new Date().toISOString()) .order('timestamp', { ascending: false }) .limit(limit); // Add optional location filters if (location.region) query = query.eq('region', location.region); if (location.country) query = query.eq('country', location.country); if (location.timeline) query = query.eq('timeline', location.timeline); const { data, error } = await query; if (error) { console.error('Error searching cache by location:', error); return []; } return data || []; } catch (error) { console.error('Error in searchByLocation:', error); return []; } } /** * Clean up expired cache entries */ async cleanupExpired() { try { const { data, error } = await index_1.supabase .from('query_cache') .delete() .lt('expire_at', new Date().toISOString()) .select('id'); if (error) { console.error('Error cleaning up expired cache:', error); return 0; } const deletedCount = data?.length || 0; console.log(`Cleaned up ${deletedCount} expired cache entries`); return deletedCount; } catch (error) { console.error('Error in cleanupExpired:', error); return 0; } } /** * Get cache statistics */ async getStats() { try { // Get total count const { count: totalCount, error: totalError } = await index_1.supabase .from('query_cache') .select('*', { count: 'exact', head: true }); if (totalError) throw totalError; // Get expired count const { count: expiredCount, error: expiredError } = await index_1.supabase .from('query_cache') .select('*', { count: 'exact', head: true }) .lt('expire_at', new Date().toISOString()); if (expiredError) throw expiredError; // Get counts by area const { data: areaData, error: areaError } = await index_1.supabase .from('query_cache') .select('area') .gte('expire_at', new Date().toISOString()); if (areaError) throw areaError; const byArea = {}; areaData?.forEach(row => { byArea[row.area] = (byArea[row.area] || 0) + 1; }); return { totalEntries: totalCount || 0, expiredEntries: expiredCount || 0, byArea }; } catch (error) { console.error('Error getting cache stats:', error); return { totalEntries: 0, expiredEntries: 0, byArea: {} }; } } /** * Hash function for generating consistent IDs */ hashString(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } return Math.abs(hash).toString(36); } } exports.SupabaseQueryCache = SupabaseQueryCache; //# sourceMappingURL=query-cache-supabase.js.map