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