/** * Copyright (c) 2024-2026 Тарабанов Александр Викторович * 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 || process.env.OLLAMA_EMBEDDINGS_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} Полный объект настроек */ 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} Настройки */ async getConfig() { if (this._isCacheValid()) { return this.cache; } return await this.loadConfig(); } /** * Обновить настройки * @param {Object} updates - Обновления * @param {number} userId - ID пользователя (опционально) * @returns {Promise} Обновленные настройки */ 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} */ 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} */ async getRAGConfig() { const config = await this.getConfig(); return config.rag_settings || this.defaults.rag_settings; } /** * Получить LLM параметры (общие) * @returns {Promise} */ async getLLMParameters() { const config = await this.getConfig(); return config.llm_parameters || this.defaults.llm_parameters; } /** * Получить специфичные параметры qwen * @returns {Promise} */ async getQwenSpecificParameters() { const config = await this.getConfig(); return config.qwen_specific_parameters || this.defaults.qwen_specific_parameters; } /** * Получить настройки кэша * @returns {Promise} */ async getCacheConfig() { const config = await this.getConfig(); return config.cache_settings || this.defaults.cache_settings; } /** * Получить настройки очереди * @returns {Promise} */ async getQueueConfig() { const config = await this.getConfig(); return config.queue_settings || this.defaults.queue_settings; } /** * Получить таймауты * @returns {Promise} */ async getTimeouts() { const config = await this.getConfig(); return config.timeouts || this.defaults.timeouts; } /** * Получить настройки дедупликации * @returns {Promise} */ async getDeduplicationConfig() { const config = await this.getConfig(); return config.deduplication_settings || this.defaults.deduplication_settings; } /** * Получить настройки embedding модели * @returns {Promise} */ async getEmbeddingParameters() { const config = await this.getConfig(); return config.embedding_parameters || this.defaults.embedding_parameters; } /** * Получить настройки Vector Search * @returns {Promise} */ async getVectorSearchConfig() { const config = await this.getConfig(); return { url: config.vector_search_url || this.defaults.vector_search_url }; } /** * Получить настройки RAG поведения * @returns {Promise} */ async getRAGBehavior() { const config = await this.getConfig(); return config.rag_behavior || this.defaults.rag_behavior; } } // Экспортируем singleton экземпляр const aiConfigService = new AIConfigService(); module.exports = aiConfigService;