feat: новая функция
This commit is contained in:
399
backend/services/aiConfigService.js
Normal file
399
backend/services/aiConfigService.js
Normal file
@@ -0,0 +1,399 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
Reference in New Issue
Block a user