supa-seed
Version:
A constraint-aware, framework-agnostic database seeding framework with deep PostgreSQL business logic discovery and MakerKit integration support
407 lines • 15.1 kB
JavaScript
;
/**
* AI Response Cache System
* Phase 5, Checkpoint E1 - Intelligent caching to avoid regeneration and improve performance
*/
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 });
exports.aiCache = exports.AIResponseCache = void 0;
const logger_1 = require("../core/utils/logger");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const crypto = __importStar(require("crypto"));
class AIResponseCache {
constructor(options) {
this.cache = new Map();
this.stats = {
hits: 0,
misses: 0,
totalRequests: 0
};
this.options = {
ttl: 24 * 60 * 60, // 24 hours default
maxSize: 1000,
persistToDisk: true,
cacheDirectory: path.join(process.cwd(), '.supa-seed', 'ai-cache'),
compressionEnabled: true,
qualityThreshold: 70,
...options
};
this.initializeCache();
this.startCleanupInterval();
logger_1.Logger.info(`🧠 AI Cache initialized: ${this.options.maxSize} entries, ${this.options.ttl}s TTL`);
}
/**
* Get cached response for a prompt
*/
async get(prompt, model, tags) {
this.stats.totalRequests++;
const key = this.generateKey(prompt, model, tags);
const entry = this.cache.get(key);
if (!entry) {
this.stats.misses++;
logger_1.Logger.debug(`🧠 Cache miss for key: ${key.substring(0, 16)}...`);
return null;
}
// Check if entry has expired
if (this.isExpired(entry)) {
this.cache.delete(key);
this.stats.misses++;
logger_1.Logger.debug(`🧠 Cache expired for key: ${key.substring(0, 16)}...`);
return null;
}
// Update access statistics
entry.metadata.usage_count++;
entry.metadata.last_accessed = new Date();
this.stats.hits++;
logger_1.Logger.debug(`🧠 Cache hit for key: ${key.substring(0, 16)}... (used ${entry.metadata.usage_count} times)`);
return entry.response;
}
/**
* Store response in cache
*/
async set(prompt, response, model, responseTime, options) {
const quality = options?.quality || 100;
// Don't cache low-quality responses
if (quality < this.options.qualityThreshold) {
logger_1.Logger.debug(`🧠 Skipping cache: quality ${quality} below threshold ${this.options.qualityThreshold}`);
return false;
}
const key = this.generateKey(prompt, model, options?.tags);
const ttl = options?.ttl || this.options.ttl;
const entry = {
key,
prompt,
response,
model,
timestamp: new Date(),
ttl,
metadata: {
responseTime,
tokenCount: options?.tokenCount,
quality,
usage_count: 0,
last_accessed: new Date()
},
tags: options?.tags || []
};
// Ensure cache size limit
if (this.cache.size >= this.options.maxSize) {
await this.evictLeastUsed();
}
this.cache.set(key, entry);
// Persist to disk if enabled
if (this.options.persistToDisk) {
await this.persistEntry(entry);
}
logger_1.Logger.debug(`🧠 Cached response: ${key.substring(0, 16)}... (quality: ${quality})`);
return true;
}
/**
* Search cached entries
*/
search(query, options) {
const results = [];
const queryLower = query.toLowerCase();
for (const entry of this.cache.values()) {
// Skip expired entries
if (this.isExpired(entry))
continue;
// Apply filters
if (options?.tags && !options.tags.some(tag => entry.tags.includes(tag))) {
continue;
}
if (options?.model && entry.model !== options.model) {
continue;
}
if (options?.minQuality && (entry.metadata.quality || 0) < options.minQuality) {
continue;
}
if (options?.dateRange) {
const entryDate = entry.timestamp;
if (entryDate < options.dateRange.from || entryDate > options.dateRange.to) {
continue;
}
}
// Check if query matches prompt or response content
if (entry.prompt.toLowerCase().includes(queryLower) ||
JSON.stringify(entry.response).toLowerCase().includes(queryLower)) {
results.push(entry);
}
}
// Sort results
if (options?.sortBy) {
results.sort((a, b) => {
switch (options.sortBy) {
case 'timestamp':
return b.timestamp.getTime() - a.timestamp.getTime();
case 'quality':
return (b.metadata.quality || 0) - (a.metadata.quality || 0);
case 'usage':
return b.metadata.usage_count - a.metadata.usage_count;
default:
return 0;
}
});
}
// Apply limit
if (options?.limit) {
return results.slice(0, options.limit);
}
return results;
}
/**
* Get similar cached responses using basic text similarity
*/
findSimilar(prompt, threshold = 0.6, limit = 5) {
const results = [];
for (const entry of this.cache.values()) {
if (this.isExpired(entry))
continue;
const similarity = this.calculateSimilarity(prompt, entry.prompt);
if (similarity >= threshold) {
results.push({ entry, similarity });
}
}
// Sort by similarity and return top results
return results
.sort((a, b) => b.similarity - a.similarity)
.slice(0, limit)
.map(r => r.entry);
}
/**
* Clear cache entries by criteria
*/
clear(criteria) {
let removedCount = 0;
for (const [key, entry] of this.cache.entries()) {
let shouldRemove = false;
if (!criteria) {
shouldRemove = true;
}
else {
if (criteria.tags && criteria.tags.some(tag => entry.tags.includes(tag))) {
shouldRemove = true;
}
if (criteria.model && entry.model === criteria.model) {
shouldRemove = true;
}
if (criteria.olderThan && entry.timestamp < criteria.olderThan) {
shouldRemove = true;
}
if (criteria.qualityBelow && (entry.metadata.quality || 100) < criteria.qualityBelow) {
shouldRemove = true;
}
}
if (shouldRemove) {
this.cache.delete(key);
removedCount++;
}
}
logger_1.Logger.info(`🧠 Cleared ${removedCount} cache entries`);
return removedCount;
}
/**
* Get cache statistics
*/
getStats() {
const entries = Array.from(this.cache.values()).filter(e => !this.isExpired(e));
const hitRate = this.stats.totalRequests > 0 ? this.stats.hits / this.stats.totalRequests : 0;
// Calculate total size (approximate)
const totalSize = entries.reduce((size, entry) => {
return size + JSON.stringify(entry).length;
}, 0);
// Get top tags
const tagCounts = new Map();
entries.forEach(entry => {
entry.tags.forEach(tag => {
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
});
});
const topTags = Array.from(tagCounts.entries())
.map(([tag, count]) => ({ tag, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 10);
// Calculate average response time
const avgResponseTime = entries.length > 0
? entries.reduce((sum, e) => sum + e.metadata.responseTime, 0) / entries.length
: 0;
const timestamps = entries.map(e => e.timestamp);
return {
totalEntries: entries.length,
hitRate,
missRate: 1 - hitRate,
totalSize,
oldestEntry: timestamps.length > 0 ? new Date(Math.min(...timestamps.map(t => t.getTime()))) : undefined,
newestEntry: timestamps.length > 0 ? new Date(Math.max(...timestamps.map(t => t.getTime()))) : undefined, averageResponseTime: avgResponseTime,
topTags
};
}
/**
* Export cache to file
*/
async exportCache(filePath) {
const entries = Array.from(this.cache.values());
const data = {
exported_at: new Date().toISOString(),
entries,
stats: this.getStats()
};
await fs.promises.writeFile(filePath, JSON.stringify(data, null, 2));
logger_1.Logger.info(`🧠 Exported ${entries.length} cache entries to ${filePath}`);
}
/**
* Import cache from file
*/
async importCache(filePath, merge = true) {
if (!fs.existsSync(filePath)) {
throw new Error(`Cache file not found: ${filePath}`);
}
const data = JSON.parse(await fs.promises.readFile(filePath, 'utf8'));
if (!merge) {
this.cache.clear();
}
let imported = 0;
for (const entry of data.entries) {
// Convert timestamp strings back to Date objects
entry.timestamp = new Date(entry.timestamp);
entry.metadata.last_accessed = new Date(entry.metadata.last_accessed);
if (!this.isExpired(entry)) {
this.cache.set(entry.key, entry);
imported++;
}
}
logger_1.Logger.info(`🧠 Imported ${imported} cache entries from ${filePath}`);
}
/**
* Private: Generate cache key
*/
generateKey(prompt, model, tags) {
const content = `${prompt}|${model || ''}|${tags?.sort().join(',') || ''}`;
return crypto.createHash('sha256').update(content).digest('hex');
}
/**
* Private: Check if entry has expired
*/
isExpired(entry) {
const expiryTime = new Date(entry.timestamp.getTime() + entry.ttl * 1000);
return new Date() > expiryTime;
}
/**
* Private: Initialize cache from disk
*/
async initializeCache() {
if (!this.options.persistToDisk)
return;
try {
await fs.promises.mkdir(this.options.cacheDirectory, { recursive: true });
const cacheFile = path.join(this.options.cacheDirectory, 'cache.json');
if (fs.existsSync(cacheFile)) {
await this.importCache(cacheFile, false);
}
}
catch (error) {
logger_1.Logger.warn(`🧠 Failed to initialize cache from disk: ${error.message}`);
}
}
/**
* Private: Persist entry to disk
*/
async persistEntry(entry) {
try {
const entryFile = path.join(this.options.cacheDirectory, `${entry.key}.json`);
await fs.promises.writeFile(entryFile, JSON.stringify(entry, null, 2));
}
catch (error) {
logger_1.Logger.debug(`🧠 Failed to persist cache entry: ${error.message}`);
}
}
/**
* Private: Evict least recently used entries
*/
async evictLeastUsed() {
const entries = Array.from(this.cache.entries());
// Sort by last accessed time (oldest first)
entries.sort((a, b) => {
return a[1].metadata.last_accessed.getTime() - b[1].metadata.last_accessed.getTime();
});
// Remove oldest 10% of entries
const toRemove = Math.max(1, Math.floor(entries.length * 0.1));
for (let i = 0; i < toRemove; i++) {
const [key] = entries[i];
this.cache.delete(key);
}
logger_1.Logger.debug(`🧠 Evicted ${toRemove} least used cache entries`);
}
/**
* Private: Calculate basic text similarity
*/
calculateSimilarity(text1, text2) {
// Simple word-based similarity (Jaccard similarity)
const words1 = new Set(text1.toLowerCase().split(/\s+/));
const words2 = new Set(text2.toLowerCase().split(/\s+/));
const intersection = new Set([...words1].filter(word => words2.has(word)));
const union = new Set([...words1, ...words2]);
return intersection.size / union.size;
}
/**
* Private: Start cleanup interval for expired entries
*/
startCleanupInterval() {
setInterval(() => {
let removedCount = 0;
for (const [key, entry] of this.cache.entries()) {
if (this.isExpired(entry)) {
this.cache.delete(key);
removedCount++;
}
}
if (removedCount > 0) {
logger_1.Logger.debug(`🧠 Cleaned up ${removedCount} expired cache entries`);
}
}, 5 * 60 * 1000); // Run every 5 minutes
}
/**
* Destroy cache and cleanup
*/
destroy() {
this.cache.clear();
logger_1.Logger.info('🧠 AI Cache destroyed');
}
}
exports.AIResponseCache = AIResponseCache;
// Export singleton instance
exports.aiCache = new AIResponseCache();
//# sourceMappingURL=response-cache.js.map