232 lines
6.6 KiB
JavaScript
232 lines
6.6 KiB
JavaScript
/**
|
||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||
* All rights reserved.
|
||
*
|
||
* This software is proprietary and confidential.
|
||
* Unauthorized copying, modification, or distribution is prohibited.
|
||
*
|
||
* For licensing inquiries: info@hb3-accelerator.com
|
||
* Website: https://hb3-accelerator.com
|
||
* GitHub: https://github.com/VC-HB3-Accelerator
|
||
*/
|
||
|
||
/**
|
||
* Кэширование AI ответов для ускорения работы
|
||
* Использует настройки из aiConfigService
|
||
*/
|
||
|
||
const crypto = require('crypto');
|
||
const logger = require('../utils/logger');
|
||
const ollamaConfig = require('./ollamaConfig');
|
||
const aiConfigService = require('./aiConfigService');
|
||
|
||
class AICache {
|
||
constructor() {
|
||
// Загружаем настройки из aiConfigService
|
||
this.cache = new Map();
|
||
this._loadSettings();
|
||
}
|
||
|
||
/**
|
||
* Загружает настройки кэша из aiConfigService
|
||
* @private
|
||
*/
|
||
async _loadSettings() {
|
||
try {
|
||
const cacheConfig = await aiConfigService.getCacheConfig();
|
||
this.maxSize = cacheConfig.maxSize || 1000;
|
||
this.ttl = cacheConfig.llmTTL || 86400000; // 24 часа
|
||
this.ragTtl = cacheConfig.ragTTL || 300000; // 5 минут
|
||
} catch (error) {
|
||
logger.warn('[AICache] Ошибка загрузки настроек, используем дефолты:', error.message);
|
||
// Дефолтные значения
|
||
const timeouts = ollamaConfig.getTimeouts();
|
||
this.maxSize = timeouts.cacheMax || 1000;
|
||
this.ttl = timeouts.cacheLLM || 86400000;
|
||
this.ragTtl = timeouts.cacheRAG || 300000;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Получает актуальные настройки (перезагружает из БД)
|
||
*/
|
||
async _getSettings() {
|
||
await this._loadSettings();
|
||
return {
|
||
maxSize: this.maxSize,
|
||
ttl: this.ttl,
|
||
ragTtl: this.ragTtl
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Генерация ключа кэша на основе запроса
|
||
* Использует параметры LLM из настроек для генерации ключа
|
||
*/
|
||
async generateKey(messages, options = {}) {
|
||
// Загружаем актуальные параметры LLM для ключа
|
||
const llmParams = await aiConfigService.getLLMParameters();
|
||
|
||
const content = JSON.stringify({
|
||
messages: messages.map(m => ({ role: m.role, content: m.content })),
|
||
temperature: options.temperature || llmParams.temperature,
|
||
maxTokens: options.num_predict || llmParams.maxTokens
|
||
});
|
||
return crypto.createHash('md5').update(content).digest('hex');
|
||
}
|
||
|
||
/**
|
||
* Генерация ключа для RAG результатов
|
||
* Включает tagIds для учета фильтрации по тегам
|
||
*/
|
||
generateKeyForRAG(tableId, userQuestion, product = null, userId = null, tagIds = null) {
|
||
// Сортируем tagIds для стабильности ключа (одинаковый порядок = одинаковый ключ)
|
||
const sortedTagIds = tagIds ? [...tagIds].sort((a, b) => a - b) : null;
|
||
const content = JSON.stringify({ tableId, userQuestion, product, userId, tagIds: sortedTagIds });
|
||
return crypto.createHash('md5').update(content).digest('hex');
|
||
}
|
||
|
||
/**
|
||
* Получение ответа из кэша (LLM)
|
||
*/
|
||
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;
|
||
|
||
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, value, type = 'llm') {
|
||
// Проверяем размер кэша
|
||
if (this.cache.size >= this.maxSize) {
|
||
// Удаляем самую старую запись
|
||
const oldestKey = Array.from(this.cache.keys())[0];
|
||
this.cache.delete(oldestKey);
|
||
logger.warn(`[AICache] Кэш переполнен, удалена старая запись: ${oldestKey.substring(0, 8)}...`);
|
||
}
|
||
|
||
this.cache.set(key, {
|
||
response: value,
|
||
timestamp: Date.now(),
|
||
type
|
||
});
|
||
|
||
logger.debug(`[AICache] Сохранено в кэш (${type}): ${key.substring(0, 8)}...`);
|
||
}
|
||
|
||
/**
|
||
* Сохранение с указанием типа
|
||
*/
|
||
setWithType(key, value, type = 'llm') {
|
||
this.set(key, value, type);
|
||
}
|
||
|
||
/**
|
||
* Очистка кэша
|
||
*/
|
||
clear() {
|
||
const size = this.cache.size;
|
||
this.cache.clear();
|
||
logger.info(`[AICache] Кэш очищен. Удалено записей: ${size}`);
|
||
return size;
|
||
}
|
||
|
||
/**
|
||
* Получение статистики
|
||
*/
|
||
getStats() {
|
||
const stats = {
|
||
size: this.cache.size,
|
||
maxSize: this.maxSize,
|
||
ttl: this.ttl,
|
||
ragTtl: this.ragTtl
|
||
};
|
||
|
||
// Подсчитываем по типам
|
||
let llmCount = 0;
|
||
let ragCount = 0;
|
||
for (const [key, value] of this.cache.entries()) {
|
||
if (value.type === 'rag') {
|
||
ragCount++;
|
||
} else {
|
||
llmCount++;
|
||
}
|
||
}
|
||
|
||
stats.llmCount = llmCount;
|
||
stats.ragCount = ragCount;
|
||
|
||
return stats;
|
||
}
|
||
|
||
/**
|
||
* Получение статистики по типам
|
||
*/
|
||
getStatsByType() {
|
||
const stats = {
|
||
llm: { count: 0, size: 0 },
|
||
rag: { count: 0, size: 0 }
|
||
};
|
||
|
||
for (const [key, value] of this.cache.entries()) {
|
||
const type = value.type || 'llm';
|
||
stats[type].count++;
|
||
stats[type].size += JSON.stringify(value.response).length;
|
||
}
|
||
|
||
return stats;
|
||
}
|
||
|
||
/**
|
||
* Инвалидация кэша по префиксу
|
||
*/
|
||
invalidateByPrefix(prefix) {
|
||
let count = 0;
|
||
for (const key of this.cache.keys()) {
|
||
if (key.startsWith(prefix)) {
|
||
this.cache.delete(key);
|
||
count++;
|
||
}
|
||
}
|
||
if (count > 0) {
|
||
logger.info(`[AICache] Инвалидировано записей с префиксом ${prefix}: ${count}`);
|
||
}
|
||
return count;
|
||
}
|
||
}
|
||
|
||
// Экспортируем singleton экземпляр
|
||
const aiCache = new AICache();
|
||
|
||
module.exports = aiCache;
|
||
|