UNPKG

pulse-ai-utils

Version:

Utility functions and helpers for AI-powered applications

312 lines 18.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const MEMORY_CACHE = []; const COLLECTION = 'query_cache'; const DB_ID = (process.env.QUERY_CACHE_DB_ID || 'pulse').trim().replace(/[\r\n\t]/g, ''); const DAY_MS = 24 * 60 * 60 * 1000; function levenshtein(a, b) { const matrix = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0)); for (let i = 0; i <= a.length; i++) matrix[i][0] = i; for (let j = 0; j <= b.length; j++) matrix[0][j] = j; for (let i = 1; i <= a.length; i++) { for (let j = 1; j <= b.length; j++) { const cost = a[i - 1] === b[j - 1] ? 0 : 1; matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost); } } return matrix[a.length][b.length]; } function similarity(a, b) { const maxLen = Math.max(a.length, b.length); if (maxLen === 0) return 1; const dist = levenshtein(a, b); return 1 - dist / maxLen; } class QueryCache { async getCachedResult(prompt, location) { const now = Date.now(); console.log(`[Cache] Looking for cached result:`); console.log(`[Cache] Query prompt: "${prompt}"`); console.log(`[Cache] Query location:`, JSON.stringify(location)); const matches = (entry) => { const areaMatch = entry.location.area === location.area; const regionMatch = entry.location.region === location.region; const countryMatch = entry.location.country === location.country; const timelineMatch = entry.location.timeline === location.timeline; if (entry.prompt.includes('family')) { console.log(`[Cache] Found entry:`, entry); console.log(`[Cache] Entry prompt: "${entry.prompt}"`); console.log(`[Cache] Entry location:`, JSON.stringify(entry.location)); } // Check buttonClickCount - both in location and options for backward compatibility const entryButtonClick = entry.location.buttonClickCount || entry.options?.buttonClickCount; const queryButtonClick = location.buttonClickCount; const buttonClickMatch = entryButtonClick === queryButtonClick; const ageCheck = now - entry.timestamp < DAY_MS; const promptSimilarity = similarity(prompt, entry.prompt); const similarityMatch = promptSimilarity >= 0.9; const allMatch = areaMatch && regionMatch && countryMatch && timelineMatch && buttonClickMatch && ageCheck && similarityMatch; // Log detailed matching info for first few entries console.log(`[Cache] Checking entry: "${entry.prompt.substring(0, 50)}..."`); console.log(`[Cache] Entry location:`, JSON.stringify(entry.location)); console.log(`[Cache] Match checks: area=${areaMatch}, region=${regionMatch}, country=${countryMatch}, timeline=${timelineMatch}, buttonClick=${buttonClickMatch} (${queryButtonClick} vs ${entryButtonClick}), age=${ageCheck}, similarity=${promptSimilarity.toFixed(3)} (≥0.9?=${similarityMatch})`); console.log(`[Cache] Overall match: ${allMatch}`); return allMatch; }; if (process.env.NODE_ENV === 'test' || process.env.OPENAI_STUB_TEST === '1') { console.log(`[Cache] Using memory cache (${MEMORY_CACHE.length} entries)`); for (const entry of MEMORY_CACHE) { if (matches(entry)) return entry.result; } return null; } // Build Firestore query filters dynamically based on defined location fields const queryFilters = { 'location.area': location.area }; if (location.region !== undefined) queryFilters['location.region'] = location.region; if (location.country !== undefined) queryFilters['location.country'] = location.country; if (location.timeline !== undefined) queryFilters.timeline = location.timeline; if (location.buttonClickCount !== undefined) queryFilters.button_click_count = location.buttonClickCount; if (location.count !== undefined) queryFilters.count = location.count; // @ts-ignore const { default: firestoreService } = await Promise.resolve().then(() => __importStar(require('../firestore/firestore'))); console.log(`[Cache] Querying Firestore with filters:`, JSON.stringify(queryFilters)); const startTime = Date.now(); // Add timeout protection to catch hanging queries const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Firestore query timeout after 10s')), 10000)); try { const docs = await Promise.race([ firestoreService.getDocumentsByFields(COLLECTION, DB_ID, queryFilters), timeoutPromise ]); const queryTime = Date.now() - startTime; console.log(`[Cache] Firestore query completed in ${queryTime}ms, returned ${docs.length} documents`); if (docs.length === 0) { console.log(`[Cache] No documents found in Firestore for location filters`); return null; } console.log(`[Cache] Checking ${docs.length} cached entries for similarity match...`); // Log all entries with full details for debugging for (let i = 0; i < docs.length; i++) { try { const doc = docs[i]; // Handle Firestore Timestamp objects let ts; if (doc.timestamp && typeof doc.timestamp === 'object' && 'toDate' in doc.timestamp) { // Firestore Timestamp object ts = doc.timestamp.toDate().getTime(); } else if (doc.timestamp && typeof doc.timestamp === 'object' && '_seconds' in doc.timestamp) { // Firestore Timestamp in JSON format ts = doc.timestamp._seconds * 1000; } else if (typeof doc.timestamp === 'string') { // String timestamp ts = new Date(doc.timestamp).getTime(); } else if (typeof doc.timestamp === 'number') { // Already a number (milliseconds) ts = doc.timestamp; } else { console.log(`[Cache] Warning: Invalid timestamp format for doc ${i + 1}, skipping:`, doc.timestamp); continue; } // Only include defined properties in location const locationObj = { area: doc.location?.area || doc.area }; if (doc.location?.region !== undefined || doc.region !== undefined) locationObj.region = doc.location?.region || doc.region; if (doc.location?.country !== undefined || doc.country !== undefined) locationObj.country = doc.location?.country || doc.country; if (doc.timeline !== undefined) locationObj.timeline = doc.timeline; const entry = { prompt: doc.prompt, location: locationObj, result: doc.result || doc.response, // Check for both 'result' and 'response' fields timestamp: ts, options: doc.button_click_count !== undefined ? { buttonClickCount: doc.button_click_count } : undefined }; console.log(`[Cache] ===== Entry ${i + 1}/${docs.length} =====`); console.log(`[Cache] Cached prompt: "${entry.prompt}"`); console.log(`[Cache] Cached location:`, JSON.stringify(entry.location)); console.log(`[Cache] Cached timestamp: ${new Date(ts).toISOString()}`); console.log(`[Cache] Result exists: ${!!entry.result}, Result type: ${typeof entry.result}, Has keys: ${entry.result ? Object.keys(entry.result).join(', ') : 'N/A'}`); const areaMatch = entry.location.area === location.area; const regionMatch = entry.location.region === location.region; const countryMatch = entry.location.country === location.country; const timelineMatch = entry.location.timeline === location.timeline; // Handle button click count matching - treat null/undefined as equal const entryButtonClick = entry.options?.buttonClickCount; const queryButtonClick = location.buttonClickCount; const buttonClickMatch = (entryButtonClick === queryButtonClick) || (entryButtonClick == null && queryButtonClick == null); const ageCheck = now - entry.timestamp < DAY_MS; const promptSimilarity = similarity(prompt, entry.prompt); const similarityMatch = promptSimilarity >= 0.9; console.log(`[Cache] Match analysis:`); console.log(`[Cache] - Area match: ${areaMatch} (query: "${location.area}" vs cached: "${entry.location.area}")`); console.log(`[Cache] - Region match: ${regionMatch} (query: "${location.region}" vs cached: "${entry.location.region}")`); console.log(`[Cache] - Country match: ${countryMatch} (query: "${location.country}" vs cached: "${entry.location.country}")`); console.log(`[Cache] - Timeline match: ${timelineMatch} (query: "${location.timeline}" vs cached: "${entry.location.timeline}")`); console.log(`[Cache] - Button click match: ${buttonClickMatch} (query: "${queryButtonClick}" vs cached: "${entryButtonClick}")`); console.log(`[Cache] - Age check: ${ageCheck} (age: ${((now - entry.timestamp) / 1000 / 60).toFixed(1)} minutes, limit: ${(DAY_MS / 1000 / 60).toFixed(0)} minutes)`); console.log(`[Cache] - Prompt similarity: ${promptSimilarity.toFixed(4)} (≥0.9000? ${similarityMatch})`); console.log(`[Cache] - Query prompt: "${prompt}"`); console.log(`[Cache] - Cached prompt: "${entry.prompt}"`); const allMatch = areaMatch && regionMatch && countryMatch && timelineMatch && buttonClickMatch && ageCheck && similarityMatch; console.log(`[Cache] Overall match: ${allMatch ? '✅ YES' : '❌ NO'}`); if (allMatch) { console.log(`[Cache] 🎉 MATCH FOUND! Using cached entry from ${new Date(ts).toISOString()}`); return entry.result; } console.log(`[Cache] ────────────────────────────────────────`); } catch (entryError) { console.error(`[Cache] Error processing entry ${i + 1}/${docs.length}:`, entryError.message); continue; } } console.log(`[Cache] ❌ No matching entries found after checking all ${docs.length} documents`); return null; } catch (error) { const queryTime = Date.now() - startTime; console.error(`[Cache] Firestore query failed after ${queryTime}ms:`, error.message); return null; } } async setCachedResult(prompt, location, result, options) { console.log('[QueryCache] ===== setCachedResult called ====='); console.log('[QueryCache] Environment check:'); console.log('[QueryCache] - NODE_ENV:', process.env.NODE_ENV); console.log('[QueryCache] - OPENAI_STUB_TEST:', process.env.OPENAI_STUB_TEST); console.log('[QueryCache] - DISABLE_CACHE_WRITER:', process.env.DISABLE_CACHE_WRITER); console.log('[QueryCache] - GOOGLE_PROJECT_ID:', process.env.GOOGLE_PROJECT_ID || 'api-project-269146618053'); if (process.env.NODE_ENV === 'test' || process.env.OPENAI_STUB_TEST === '1') { console.log('[QueryCache] 🧪 Test mode - using memory cache'); MEMORY_CACHE.push({ prompt, location, result, timestamp: Date.now(), options }); return; } console.log('[QueryCache] 🚀 Production mode - sending to Cloud Function'); console.log('[QueryCache] Request details:'); console.log('[QueryCache] - Prompt length:', prompt?.length || 0); console.log('[QueryCache] - Prompt preview:', prompt?.substring(0, 50) + '...' || 'undefined'); console.log('[QueryCache] - Location:', JSON.stringify(location)); console.log('[QueryCache] - Result type:', typeof result); console.log('[QueryCache] - Result keys:', result ? Object.keys(result) : 'undefined'); console.log('[QueryCache] - Options:', JSON.stringify(options)); // Fire-and-forget: Send to Cloud Function for async processing try { const cloudFunctionUrl = process.env.QUERY_CACHE_WRITER_URL || `https://us-central1-${process.env.GOOGLE_PROJECT_ID || 'api-project-269146618053'}.cloudfunctions.net/write_query_cache`; console.log('[QueryCache] - Cloud Function Full URL:', JSON.stringify(cloudFunctionUrl)); // Use fetch to call the Cloud Function asynchronously const requestData = { prompt, location, result, options, timestamp: new Date().toISOString() }; console.log('[QueryCache] Request payload size:', JSON.stringify(requestData).length, 'bytes'); // Fire and forget - don't await the response // For local development, you might want to skip this if (process.env.DISABLE_CACHE_WRITER !== 'true') { console.log('[QueryCache] 📤 Sending fire-and-forget request...'); fetch(cloudFunctionUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(requestData), }).then(response => { console.log('[QueryCache] 📥 Response received - Status:', response.status); if (!response.ok) { console.error('[QueryCache] ❌ HTTP error:', response.status, response.statusText); return response.text().then(text => { console.error('[QueryCache] ❌ Response body:', text); }); } return response.json().then(data => { console.log('[QueryCache] ✅ Success response:', JSON.stringify(data)); }); }).catch(error => { // Log error but don't block the user console.error('[QueryCache] ❌ Fire-and-forget cache write failed:'); console.error('[QueryCache] - Error type:', error.constructor.name); console.error('[QueryCache] - Error message:', error.message); console.error('[QueryCache] - Error stack:', error.stack); }); } else { console.log('[QueryCache] ⏸️ Cache writer disabled (DISABLE_CACHE_WRITER=true)'); } console.log('[QueryCache] ✅ Cache write initiated successfully'); } catch (error) { // Even if the fire-and-forget fails, don't block the user console.error('[QueryCache] ❌ Failed to initiate cache write:'); console.error('[QueryCache] - Error type:', error.constructor.name); console.error('[QueryCache] - Error message:', error.message); console.error('[QueryCache] - Error stack:', error.stack); } } /** * Generate a short hash of a string for ID generation */ hashString(str) { let hash = 0; if (str.length === 0) return hash.toString(); 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).substring(0, 8); // Convert to base36 and take first 8 chars } } exports.default = QueryCache; //# sourceMappingURL=query-cache.js.map