pulse-ai-utils
Version:
Utility functions and helpers for AI-powered applications
402 lines • 17 kB
JavaScript
;
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