Files
DLE/backend/services/aiConfigService.js
2025-11-06 16:24:50 +03:00

400 lines
12 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.

/**
* 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
*
* Принципы:
* - Единый источник истины (таблица ai_config)
* - Кэширование в памяти (TTL: 1 минута)
* - Автоматическая инвалидация при изменении
* - Приоритет источников: БД > ENV > хардкод
*/
const db = require('../db');
const logger = require('../utils/logger');
class AIConfigService {
constructor() {
// Кэш для настроек
this.cache = null;
this.cacheTimestamp = 0;
this.CACHE_TTL = 60000; // 1 минута
// Дефолтные значения (fallback)
this.defaults = {
ollama_base_url: process.env.OLLAMA_BASE_URL || 'http://ollama:11434',
ollama_llm_model: process.env.OLLAMA_MODEL || 'qwen2.5:7b',
ollama_embedding_model: process.env.OLLAMA_EMBED_MODEL || 'mxbai-embed-large:latest',
vector_search_url: process.env.VECTOR_SEARCH_URL || 'http://vector-search:8001',
embedding_parameters: {
batch_size: 32,
normalize: true,
dimension: null,
pooling: 'mean'
},
llm_parameters: {
temperature: 0.3,
maxTokens: 150,
top_p: 0.9,
top_k: 40,
repeat_penalty: 1.1
},
qwen_specific_parameters: {
format: null
},
rag_settings: {
threshold: 300,
maxResults: 3,
searchMethod: 'hybrid',
relevanceThreshold: 0.1,
keywordExtraction: {
enabled: true,
minWordLength: 3,
maxKeywords: 10,
removeStopWords: true,
language: 'ru'
},
searchWeights: {
semantic: 70,
keyword: 30
},
advanced: {
enableFuzzySearch: true,
enableStemming: true,
enableSynonyms: false
}
},
cache_settings: {
enabled: true,
llmTTL: 86400000,
ragTTL: 300000,
maxSize: 1000
},
queue_settings: {
enabled: true,
timeout: 180000,
maxSize: 100,
interval: 100
},
timeouts: {
ollamaChat: 600000,
ollamaEmbedding: 90000,
vectorSearch: 90000,
vectorUpsert: 600000,
vectorHealth: 5000,
ollamaHealth: 5000,
ollamaTags: 10000
},
rag_behavior: {
upsertOnQuery: false,
autoIndexOnTableChange: true
},
deduplication_settings: {
enabled: true,
ttl: 300000
}
};
}
/**
* Проверка актуальности кэша
* @returns {boolean}
*/
_isCacheValid() {
if (!this.cache) return false;
const now = Date.now();
return (now - this.cacheTimestamp) < this.CACHE_TTL;
}
/**
* Загрузить все настройки из БД
* @returns {Promise<Object>} Полный объект настроек
*/
async loadConfig() {
try {
const query = db.getQuery();
const result = await query(
'SELECT * FROM ai_config WHERE id = 1 LIMIT 1'
);
if (result.rows.length === 0) {
logger.warn('[aiConfigService] Таблица ai_config пуста, используем дефолтные значения');
// Создаем дефолтную запись
await this._createDefaultConfig();
return this.defaults;
}
const config = result.rows[0];
// Парсим JSONB поля
const parsedConfig = {
...config,
embedding_parameters: config.embedding_parameters || this.defaults.embedding_parameters,
llm_parameters: config.llm_parameters || this.defaults.llm_parameters,
qwen_specific_parameters: config.qwen_specific_parameters || this.defaults.qwen_specific_parameters,
rag_settings: config.rag_settings || this.defaults.rag_settings,
cache_settings: config.cache_settings || this.defaults.cache_settings,
queue_settings: config.queue_settings || this.defaults.queue_settings,
timeouts: config.timeouts || this.defaults.timeouts,
rag_behavior: config.rag_behavior || this.defaults.rag_behavior,
deduplication_settings: config.deduplication_settings || this.defaults.deduplication_settings
};
// Объединяем таймауты с дефолтами и при необходимости обновляем БД
const existingTimeouts = parsedConfig.timeouts || {};
const mergedTimeouts = { ...existingTimeouts };
for (const [key, defaultValue] of Object.entries(this.defaults.timeouts)) {
const rawValue = existingTimeouts[key];
const numericValue = Number(rawValue);
if (!Number.isFinite(numericValue) || numericValue < defaultValue) {
mergedTimeouts[key] = defaultValue;
} else {
mergedTimeouts[key] = numericValue;
}
}
const shouldPersistTimeouts = JSON.stringify(existingTimeouts) !== JSON.stringify(mergedTimeouts);
parsedConfig.timeouts = mergedTimeouts;
// Обновляем кэш
this.cache = parsedConfig;
this.cacheTimestamp = Date.now();
if (shouldPersistTimeouts) {
try {
await query(
'UPDATE ai_config SET timeouts = $1::jsonb, updated_at = NOW() WHERE id = 1',
[JSON.stringify(mergedTimeouts)]
);
logger.info('[aiConfigService] Таймауты обновлены до актуальных значений по умолчанию');
} catch (updateError) {
logger.warn('[aiConfigService] Не удалось обновить таймауты в БД:', updateError.message);
}
}
logger.info('[aiConfigService] Настройки загружены из БД');
return parsedConfig;
} catch (error) {
logger.error('[aiConfigService] Ошибка загрузки настроек из БД:', error.message);
// Возвращаем дефолтные значения в случае ошибки
return this.defaults;
}
}
/**
* Создать дефолтную запись в БД
* @private
*/
async _createDefaultConfig() {
try {
const query = db.getQuery();
await query(
`INSERT INTO ai_config (id) VALUES (1) ON CONFLICT (id) DO NOTHING`
);
logger.info('[aiConfigService] Создана дефолтная запись в ai_config');
} catch (error) {
logger.error('[aiConfigService] Ошибка создания дефолтной записи:', error.message);
}
}
/**
* Получить все настройки (с кэшированием)
* @returns {Promise<Object>} Настройки
*/
async getConfig() {
if (this._isCacheValid()) {
return this.cache;
}
return await this.loadConfig();
}
/**
* Обновить настройки
* @param {Object} updates - Обновления
* @param {number} userId - ID пользователя (опционально)
* @returns {Promise<Object>} Обновленные настройки
*/
async updateConfig(updates, userId = null) {
try {
const query = db.getQuery();
const fields = [];
const values = [];
let paramIndex = 1;
// Строим SET часть запроса
for (const [key, value] of Object.entries(updates)) {
if (key === 'id' || key === 'updated_at' || key === 'updated_by') continue;
if (typeof value === 'object' && value !== null) {
// JSONB поля
fields.push(`${key} = $${paramIndex}::jsonb`);
values.push(JSON.stringify(value));
} else {
fields.push(`${key} = $${paramIndex}`);
values.push(value);
}
paramIndex++;
}
// Добавляем updated_at и updated_by
if (fields.length > 0) {
fields.push(`updated_at = NOW()`);
if (userId) {
fields.push(`updated_by = $${paramIndex}`);
values.push(userId);
}
const sql = `UPDATE ai_config SET ${fields.join(', ')} WHERE id = 1`;
await query(sql, values);
// Инвалидируем кэш
this.invalidateCache();
logger.info('[aiConfigService] Настройки обновлены');
return await this.loadConfig();
}
return await this.getConfig();
} catch (error) {
logger.error('[aiConfigService] Ошибка обновления настроек:', error.message);
throw error;
}
}
/**
* Инвалидация кэша (принудительная перезагрузка)
*/
invalidateCache() {
this.cache = null;
this.cacheTimestamp = 0;
logger.debug('[aiConfigService] Кэш инвалидирован');
}
// ============================================
// МЕТОДЫ ДЛЯ КОНКРЕТНЫХ КАТЕГОРИЙ
// ============================================
/**
* Получить настройки Ollama
* @returns {Promise<Object>}
*/
async getOllamaConfig() {
const config = await this.getConfig();
return {
baseUrl: config.ollama_base_url || this.defaults.ollama_base_url,
llmModel: config.ollama_llm_model || this.defaults.ollama_llm_model,
embeddingModel: config.ollama_embedding_model || this.defaults.ollama_embedding_model
};
}
/**
* Получить RAG настройки
* @returns {Promise<Object>}
*/
async getRAGConfig() {
const config = await this.getConfig();
return config.rag_settings || this.defaults.rag_settings;
}
/**
* Получить LLM параметры (общие)
* @returns {Promise<Object>}
*/
async getLLMParameters() {
const config = await this.getConfig();
return config.llm_parameters || this.defaults.llm_parameters;
}
/**
* Получить специфичные параметры qwen
* @returns {Promise<Object>}
*/
async getQwenSpecificParameters() {
const config = await this.getConfig();
return config.qwen_specific_parameters || this.defaults.qwen_specific_parameters;
}
/**
* Получить настройки кэша
* @returns {Promise<Object>}
*/
async getCacheConfig() {
const config = await this.getConfig();
return config.cache_settings || this.defaults.cache_settings;
}
/**
* Получить настройки очереди
* @returns {Promise<Object>}
*/
async getQueueConfig() {
const config = await this.getConfig();
return config.queue_settings || this.defaults.queue_settings;
}
/**
* Получить таймауты
* @returns {Promise<Object>}
*/
async getTimeouts() {
const config = await this.getConfig();
return config.timeouts || this.defaults.timeouts;
}
/**
* Получить настройки дедупликации
* @returns {Promise<Object>}
*/
async getDeduplicationConfig() {
const config = await this.getConfig();
return config.deduplication_settings || this.defaults.deduplication_settings;
}
/**
* Получить настройки embedding модели
* @returns {Promise<Object>}
*/
async getEmbeddingParameters() {
const config = await this.getConfig();
return config.embedding_parameters || this.defaults.embedding_parameters;
}
/**
* Получить настройки Vector Search
* @returns {Promise<Object>}
*/
async getVectorSearchConfig() {
const config = await this.getConfig();
return {
url: config.vector_search_url || this.defaults.vector_search_url
};
}
/**
* Получить настройки RAG поведения
* @returns {Promise<Object>}
*/
async getRAGBehavior() {
const config = await this.getConfig();
return config.rag_behavior || this.defaults.rag_behavior;
}
}
// Экспортируем singleton экземпляр
const aiConfigService = new AIConfigService();
module.exports = aiConfigService;