UNPKG

claude-gemini-multimodal-bridge

Version:

Enterprise-grade AI integration bridge connecting Claude Code, Gemini CLI, and Google AI Studio with intelligent routing and advanced multimodal processing capabilities

271 lines (270 loc) 9.51 kB
import { logger } from './logger.js'; import crypto from 'crypto'; export class SearchCache { options; cache = new Map(); stats = { totalEntries: 0, hitCount: 0, missCount: 0, hitRate: 0, storageSize: 0, expiredEntries: 0 }; defaultTTL = 30 * 60 * 1000; maxEntries = 1000; similarityThreshold = 0.8; enableMetrics = true; constructor(options = {}) { this.options = options; } async get(query, searchEngine = 'gemini') { const key = this.generateCacheKey(query, searchEngine); const entry = this.cache.get(key); if (!entry) { this.stats.missCount++; this.updateHitRate(); logger.debug('Cache miss', { query: query.substring(0, 50), searchEngine }); return null; } if (Date.now() > entry.expiresAt) { this.cache.delete(key); this.stats.expiredEntries++; this.stats.missCount++; this.updateHitRate(); logger.debug('Cache expired', { query: query.substring(0, 50), searchEngine, expiredAt: new Date(entry.expiresAt).toISOString() }); return null; } if (!entry && this.options.similarityThreshold) { const similarEntry = this.findSimilarEntry(query, searchEngine); if (similarEntry) { this.stats.hitCount++; this.updateHitRate(); logger.debug('Cache hit (similar)', { originalQuery: query.substring(0, 50), similarQuery: similarEntry.query.substring(0, 50), searchEngine }); return similarEntry.result; } } this.stats.hitCount++; this.updateHitRate(); logger.debug('Cache hit', { query: query.substring(0, 50), searchEngine, age: Date.now() - entry.timestamp }); return entry.result; } async set(query, result, searchEngine = 'gemini', processingTime = 0) { const key = this.generateCacheKey(query, searchEngine); const timestamp = Date.now(); const ttl = this.options.ttl || this.defaultTTL; const expiresAt = timestamp + ttl; if (this.cache.size >= (this.options.maxEntries || this.maxEntries)) { this.evictOldestEntries(); } const entry = { key, query, result, timestamp, expiresAt, metadata: { promptHash: this.hashString(query), searchEngine, resultCount: this.countResults(result), processingTime } }; this.cache.set(key, entry); this.stats.totalEntries = this.cache.size; this.updateStorageSize(); logger.debug('Cache stored', { query: query.substring(0, 50), searchEngine, ttl, resultCount: entry.metadata.resultCount }); } generateCacheKey(query, searchEngine) { const normalizedQuery = this.normalizeQuery(query); const dataToHash = `${normalizedQuery}:${searchEngine}`; return this.hashString(dataToHash); } normalizeQuery(query) { return query .toLowerCase() .trim() .replace(/[。、!?]/g, '') .replace(/\s+/g, ' ') .replace(/202[4-9]年?/g, '2024-2025') .replace(/について教えて|を説明して|について知りたい/g, 'について') .replace(/最新の|最近の|新しい/g, '最新') .replace(/具体的に|詳しく|詳細に/g, '詳細'); } findSimilarEntry(query, searchEngine) { const normalizedQuery = this.normalizeQuery(query); const threshold = this.options.similarityThreshold || this.similarityThreshold; for (const entry of this.cache.values()) { if (entry.metadata.searchEngine !== searchEngine) { continue; } if (Date.now() > entry.expiresAt) { continue; } const similarity = this.calculateSimilarity(normalizedQuery, this.normalizeQuery(entry.query)); if (similarity >= threshold) { return entry; } } return null; } calculateSimilarity(str1, str2) { const tokens1 = new Set(str1.split(/\s+/)); const tokens2 = new Set(str2.split(/\s+/)); const intersection = new Set([...tokens1].filter(x => tokens2.has(x))); const union = new Set([...tokens1, ...tokens2]); return union.size === 0 ? 0 : intersection.size / union.size; } evictOldestEntries() { const maxEntries = this.options.maxEntries || this.maxEntries; const evictCount = Math.floor(maxEntries * 0.2); const sortedEntries = Array.from(this.cache.entries()) .sort(([, a], [, b]) => a.timestamp - b.timestamp); for (let i = 0; i < evictCount && i < sortedEntries.length; i++) { const entry = sortedEntries[i]; if (entry?.[0]) { this.cache.delete(entry[0]); } } logger.debug('Cache eviction completed', { evictedCount: evictCount, remainingEntries: this.cache.size }); } async cleanup() { const now = Date.now(); let cleanedCount = 0; for (const [key, entry] of this.cache.entries()) { if (now > entry.expiresAt) { this.cache.delete(key); cleanedCount++; } } this.stats.totalEntries = this.cache.size; this.stats.expiredEntries += cleanedCount; this.updateStorageSize(); if (cleanedCount > 0) { logger.debug('Cache cleanup completed', { cleanedCount, remainingEntries: this.cache.size }); } return cleanedCount; } async clear() { const entryCount = this.cache.size; this.cache.clear(); this.resetStats(); logger.info('Cache cleared', { clearedEntries: entryCount }); } getStats() { this.updateStorageSize(); return { ...this.stats }; } async exportToFile(filePath) { try { const exportData = { timestamp: Date.now(), stats: this.stats, entries: Array.from(this.cache.entries()).map(([cacheKey, entry]) => ({ cacheKey, ...entry })) }; const fs = await import('fs/promises'); await fs.writeFile(filePath, JSON.stringify(exportData, null, 2)); logger.info('Cache exported successfully', { filePath, entryCount: this.cache.size }); } catch (error) { logger.error('Cache export failed', { error, filePath }); throw error; } } async importFromFile(filePath) { try { const fs = await import('fs/promises'); const data = await fs.readFile(filePath, 'utf-8'); const importData = JSON.parse(data); let importedCount = 0; const now = Date.now(); for (const entryData of importData.entries) { if (entryData.expiresAt > now) { const key = entryData.cacheKey || entryData.key; this.cache.set(key, { key: entryData.key, query: entryData.query, result: entryData.result, timestamp: entryData.timestamp, expiresAt: entryData.expiresAt, metadata: entryData.metadata }); importedCount++; } } this.stats.totalEntries = this.cache.size; this.updateStorageSize(); logger.info('Cache imported successfully', { filePath, importedCount, skippedExpired: importData.entries.length - importedCount }); } catch (error) { logger.error('Cache import failed', { error, filePath }); throw error; } } hashString(str) { return crypto.createHash('sha256').update(str).digest('hex').substring(0, 16); } countResults(result) { if (Array.isArray(result)) { return result.length; } if (result && typeof result === 'object') { return Object.keys(result).length; } return 1; } updateHitRate() { const total = this.stats.hitCount + this.stats.missCount; this.stats.hitRate = total > 0 ? (this.stats.hitCount / total) * 100 : 0; } updateStorageSize() { let size = 0; for (const entry of this.cache.values()) { size += JSON.stringify(entry).length * 2; } this.stats.storageSize = size; } resetStats() { this.stats = { totalEntries: 0, hitCount: 0, missCount: 0, hitRate: 0, storageSize: 0, expiredEntries: 0 }; } }