Files
DLE/backend/services/ai-cache.js
2025-10-09 16:48:20 +03:00

165 lines
5.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Кэширование 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();