165 lines
5.1 KiB
JavaScript
165 lines
5.1 KiB
JavaScript
/**
|
||
* Кэширование AI ответов для ускорения работы
|
||
*/
|
||
|
||
const crypto = require('crypto');
|
||
const logger = require('../utils/logger');
|
||
const ollamaConfig = require('./ollamaConfig');
|
||
|
||
class AICache {
|
||
constructor() {
|
||
const timeouts = ollamaConfig.getTimeouts();
|
||
|
||
this.cache = new Map();
|
||
this.maxSize = timeouts.cacheMax; // Из централизованных настроек
|
||
this.ttl = timeouts.cacheLLM; // 24 часа (для LLM)
|
||
this.ragTtl = timeouts.cacheRAG; // 5 минут (для RAG результатов)
|
||
}
|
||
|
||
// Генерация ключа кэша на основе запроса
|
||
generateKey(messages, options = {}) {
|
||
const content = JSON.stringify({
|
||
messages: messages.map(m => ({ role: m.role, content: m.content })),
|
||
temperature: options.temperature || 0.3,
|
||
maxTokens: options.num_predict || 150
|
||
});
|
||
return crypto.createHash('md5').update(content).digest('hex');
|
||
}
|
||
|
||
// ✨ НОВОЕ: Генерация ключа для RAG результатов
|
||
generateKeyForRAG(tableId, userQuestion, product = null) {
|
||
const content = JSON.stringify({ tableId, userQuestion, product });
|
||
return crypto.createHash('md5').update(content).digest('hex');
|
||
}
|
||
|
||
// Получение ответа из кэша
|
||
get(key) {
|
||
const cached = this.cache.get(key);
|
||
if (!cached) return null;
|
||
|
||
// Проверяем TTL
|
||
if (Date.now() - cached.timestamp > this.ttl) {
|
||
this.cache.delete(key);
|
||
return null;
|
||
}
|
||
|
||
logger.info(`[AICache] Cache hit for key: ${key.substring(0, 8)}...`);
|
||
return cached.response;
|
||
}
|
||
|
||
// ✨ НОВОЕ: Получение с учетом типа кэша (RAG или LLM)
|
||
getWithTTL(key, type = 'llm') {
|
||
const cached = this.cache.get(key);
|
||
if (!cached) return null;
|
||
|
||
// Выбираем TTL в зависимости от типа
|
||
const ttl = type === 'rag' ? this.ragTtl : this.ttl;
|
||
|
||
// Проверяем TTL
|
||
if (Date.now() - cached.timestamp > ttl) {
|
||
this.cache.delete(key);
|
||
return null;
|
||
}
|
||
|
||
logger.info(`[AICache] Cache hit (${type}) for key: ${key.substring(0, 8)}...`);
|
||
return cached.response;
|
||
}
|
||
|
||
// Сохранение ответа в кэш
|
||
set(key, response) {
|
||
// Очищаем старые записи если кэш переполнен
|
||
if (this.cache.size >= this.maxSize) {
|
||
const oldestKey = this.cache.keys().next().value;
|
||
this.cache.delete(oldestKey);
|
||
}
|
||
|
||
this.cache.set(key, {
|
||
response,
|
||
timestamp: Date.now()
|
||
});
|
||
|
||
logger.info(`[AICache] Cached response for key: ${key.substring(0, 8)}...`);
|
||
}
|
||
|
||
// ✨ НОВОЕ: Сохранение с указанием типа (rag или llm)
|
||
setWithType(key, response, type = 'llm') {
|
||
// Очищаем старые записи если кэш переполнен
|
||
if (this.cache.size >= this.maxSize) {
|
||
const oldestKey = this.cache.keys().next().value;
|
||
this.cache.delete(oldestKey);
|
||
}
|
||
|
||
this.cache.set(key, {
|
||
response,
|
||
timestamp: Date.now(),
|
||
type: type // Сохраняем тип для статистики
|
||
});
|
||
|
||
logger.info(`[AICache] Cached ${type} response for key: ${key.substring(0, 8)}...`);
|
||
}
|
||
|
||
// Очистка кэша
|
||
clear() {
|
||
this.cache.clear();
|
||
logger.info('[AICache] Cache cleared');
|
||
}
|
||
|
||
// Очистка старых записей по времени
|
||
cleanup(maxAge = 3600000) { // По умолчанию 1 час
|
||
const now = Date.now();
|
||
let deletedCount = 0;
|
||
|
||
for (const [key, value] of this.cache.entries()) {
|
||
if (now - value.timestamp > maxAge) {
|
||
this.cache.delete(key);
|
||
deletedCount++;
|
||
}
|
||
}
|
||
|
||
if (deletedCount > 0) {
|
||
logger.info(`[AICache] Cleaned up ${deletedCount} old entries`);
|
||
}
|
||
}
|
||
|
||
// Статистика кэша
|
||
getStats() {
|
||
return {
|
||
size: this.cache.size,
|
||
maxSize: this.maxSize,
|
||
hitRate: this.calculateHitRate()
|
||
};
|
||
}
|
||
|
||
calculateHitRate() {
|
||
// Простая реализация - в реальности нужно отслеживать hits/misses
|
||
if (this.maxSize === 0) return 0;
|
||
return this.cache.size / this.maxSize;
|
||
}
|
||
|
||
// ✨ НОВОЕ: Статистика по типу кэша
|
||
getStatsByType() {
|
||
const stats = { rag: 0, llm: 0, other: 0 };
|
||
for (const [key, value] of this.cache.entries()) {
|
||
const type = value.type || 'other';
|
||
stats[type] = (stats[type] || 0) + 1;
|
||
}
|
||
return stats;
|
||
}
|
||
|
||
// ✨ НОВОЕ: Инвалидация по префиксу (для очистки RAG кэша при обновлении таблиц)
|
||
invalidateByPrefix(prefix) {
|
||
let deletedCount = 0;
|
||
for (const [key, value] of this.cache.entries()) {
|
||
if (key.startsWith(prefix)) {
|
||
this.cache.delete(key);
|
||
deletedCount++;
|
||
}
|
||
}
|
||
if (deletedCount > 0) {
|
||
logger.info(`[AICache] Инвалидировано ${deletedCount} записей с префиксом: ${prefix}`);
|
||
}
|
||
return deletedCount;
|
||
}
|
||
}
|
||
|
||
module.exports = new AICache();
|