605 lines
23 KiB
JavaScript
605 lines
23 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
|
||
*/
|
||
|
||
const encryptedDb = require('./encryptedDatabaseService');
|
||
const vectorSearch = require('./vectorSearchClient');
|
||
const { getProviderSettings } = require('./aiProviderSettingsService');
|
||
const axios = require('axios');
|
||
const ollamaConfig = require('./ollamaConfig');
|
||
const aiCache = require('./ai-cache');
|
||
const AIQueue = require('./ai-queue');
|
||
const logger = require('../utils/logger');
|
||
|
||
// console.log('[RAG] ragService.js loaded');
|
||
|
||
// Управляет поведением: выполнять ли upsert всех строк на каждый запрос поиска
|
||
const UPSERT_ON_QUERY = process.env.RAG_UPSERT_ON_QUERY === 'true';
|
||
|
||
// Флаги для включения/выключения Queue и Cache
|
||
const USE_AI_CACHE = process.env.USE_AI_CACHE !== 'false'; // default: true
|
||
const USE_AI_QUEUE = process.env.USE_AI_QUEUE !== 'false'; // default: true
|
||
|
||
// Создаем экземпляр очереди
|
||
const aiQueue = new AIQueue();
|
||
|
||
async function getTableData(tableId) {
|
||
// console.log(`[RAG] getTableData called for tableId: ${tableId}`);
|
||
|
||
const columns = await encryptedDb.getData('user_columns', { table_id: tableId });
|
||
// console.log(`[RAG] Found ${columns.length} columns:`, columns.map(col => ({ id: col.id, name: col.name, purpose: col.options?.purpose })));
|
||
|
||
const rows = await encryptedDb.getData('user_rows', { table_id: tableId });
|
||
// console.log(`[RAG] Found ${rows.length} rows:`, rows.map(row => ({ id: row.id, name: row.name })));
|
||
|
||
// Исправление: проверяем что есть строки перед запросом cell_values
|
||
const cellValues = rows.length > 0
|
||
? await encryptedDb.getData('user_cell_values', { row_id: { $in: rows.map(row => row.id) } })
|
||
: [];
|
||
// console.log(`[RAG] Found ${cellValues.length} cell values`);
|
||
|
||
const getColId = purpose => columns.find(col => col.options?.purpose === purpose)?.id;
|
||
const questionColId = getColId('question');
|
||
const answerColId = getColId('answer');
|
||
const contextColId = getColId('context');
|
||
const productColId = getColId('product');
|
||
const priorityColId = getColId('priority');
|
||
const dateColId = getColId('date');
|
||
|
||
// console.log(`[RAG] Column IDs:`, {
|
||
// question: questionColId,
|
||
// answer: answerColId,
|
||
// context: contextColId,
|
||
// product: productColId,
|
||
// priority: priorityColId,
|
||
// date: dateColId
|
||
// });
|
||
|
||
const data = rows.map(row => {
|
||
const cells = cellValues.filter(cell => cell.row_id === row.id);
|
||
const result = {
|
||
id: row.id,
|
||
question: cells.find(c => c.column_id === questionColId)?.value,
|
||
answer: cells.find(c => c.column_id === answerColId)?.value,
|
||
context: cells.find(c => c.column_id === contextColId)?.value,
|
||
product: parseIfArray(cells.find(c => c.column_id === productColId)?.value),
|
||
userTags: parseIfArray(cells.find(c => c.column_id === getColId('userTags'))?.value),
|
||
priority: cells.find(c => c.column_id === priorityColId)?.value,
|
||
date: cells.find(c => c.column_id === dateColId)?.value,
|
||
};
|
||
// console.log(`[RAG] Processed row ${row.id}:`, result);
|
||
return result;
|
||
});
|
||
return data;
|
||
}
|
||
|
||
async function ragAnswer({ tableId, userQuestion, product = null, threshold = 300, forceReindex = false }) {
|
||
// console.log(`[RAG] ragAnswer called: tableId=${tableId}, userQuestion="${userQuestion}"`);
|
||
|
||
// Проверяем кэш (используем ai-cache вместо ragCache)
|
||
if (USE_AI_CACHE) {
|
||
const cacheKey = aiCache.generateKeyForRAG(tableId, userQuestion, product);
|
||
const cached = aiCache.getWithTTL(cacheKey, 'rag');
|
||
if (cached) {
|
||
console.log(`[RAG] Возврат RAG результата из кэша`);
|
||
return cached;
|
||
}
|
||
}
|
||
|
||
const data = await getTableData(tableId);
|
||
// console.log(`[RAG] Got ${data.length} rows from database`);
|
||
|
||
// Подробное логирование данных
|
||
data.forEach((row, index) => {
|
||
// console.log(`[RAG] Row ${index}:`, {
|
||
// id: row.id,
|
||
// question: row.question,
|
||
// answer: row.answer,
|
||
// product: row.product
|
||
// });
|
||
});
|
||
|
||
const questions = data.map(row => row.question && typeof row.question === 'string' ? row.question.trim() : row.question);
|
||
// Фильтруем только строки с непустым вопросом (text)
|
||
const rowsForUpsert = data
|
||
.filter(row => row.id && row.question && String(row.question).trim().length > 0)
|
||
.map(row => ({
|
||
row_id: row.id,
|
||
text: row.question,
|
||
metadata: {
|
||
answer: row.answer || null,
|
||
context: row.context || null,
|
||
product: row.product || [],
|
||
userTags: row.userTags || [],
|
||
priority: row.priority || null,
|
||
date: row.date || null
|
||
}
|
||
}));
|
||
|
||
// console.log(`[RAG] Prepared ${rowsForUpsert.length} rows for upsert`);
|
||
// console.log(`[RAG] First row:`, rowsForUpsert[0]);
|
||
|
||
// Выполняем upsert ТОЛЬКО если явно разрешено флагом/параметром.
|
||
if ((UPSERT_ON_QUERY || forceReindex) && rowsForUpsert.length > 0) {
|
||
await vectorSearch.upsert(tableId, rowsForUpsert);
|
||
}
|
||
|
||
// Поиск
|
||
let results = [];
|
||
if (rowsForUpsert.length > 0 && userQuestion && userQuestion.trim()) {
|
||
results = await vectorSearch.search(tableId, userQuestion, 3); // Увеличиваем до 3 результатов для лучшего поиска
|
||
console.log(`[RAG] Search completed, got ${results.length} results`);
|
||
|
||
// Подробное логирование результатов поиска
|
||
results.forEach((result, index) => {
|
||
console.log(`[RAG] Search result ${index}:`, {
|
||
row_id: result.row_id,
|
||
score: result.score,
|
||
metadata: result.metadata
|
||
});
|
||
});
|
||
} else {
|
||
console.log(`[RAG] No data in table, skipping search`);
|
||
}
|
||
|
||
// Фильтрация по тегам/продукту
|
||
let filtered = results;
|
||
// console.log(`[RAG] Before filtering: ${filtered.length} results`);
|
||
|
||
if (product) {
|
||
// console.log(`[RAG] Filtering by product:`, product);
|
||
filtered = filtered.filter(row => Array.isArray(row.metadata.product) ? row.metadata.product.includes(product) : row.metadata.product === product);
|
||
// console.log(`[RAG] After product filtering: ${filtered.length} results`);
|
||
}
|
||
|
||
// Берём ближайший результат с учётом порога (по модулю)
|
||
console.log(`[RAG] Looking for best result with abs(threshold): ${threshold}`);
|
||
const best = filtered.reduce((acc, row) => {
|
||
if (Math.abs(row.score) <= threshold && (acc === null || Math.abs(row.score) < Math.abs(acc.score))) {
|
||
return row;
|
||
}
|
||
return acc;
|
||
}, null);
|
||
console.log(`[RAG] Best result:`, best);
|
||
|
||
// Логируем все результаты с их score для диагностики
|
||
if (filtered.length > 0) {
|
||
// console.log(`[RAG] All filtered results with scores:`);
|
||
// filtered.forEach((result, index) => {
|
||
// console.log(`[RAG] ${index}: score=${result.score}, meets_threshold=${Math.abs(result.score) <= threshold}`);
|
||
// });
|
||
}
|
||
|
||
const result = {
|
||
answer: best?.metadata?.answer,
|
||
context: best?.metadata?.context,
|
||
product: best?.metadata?.product,
|
||
priority: best?.metadata?.priority,
|
||
date: best?.metadata?.date,
|
||
score: best?.score !== undefined && best?.score !== null ? Number(best.score) : null,
|
||
};
|
||
|
||
console.log(`[RAG] Final result:`, result);
|
||
|
||
// Кэшируем результат (используем ai-cache вместо ragCache)
|
||
if (USE_AI_CACHE) {
|
||
const cacheKey = aiCache.generateKeyForRAG(tableId, userQuestion, product);
|
||
aiCache.setWithType(cacheKey, result, 'rag');
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Загрузка всех плейсхолдеров и их значений из пользовательских таблиц
|
||
* Возвращает объект: { placeholder1: value1, placeholder2: value2, ... }
|
||
* @param {Array} selectedRagTables - Массив ID выбранных RAG таблиц для фильтрации
|
||
*/
|
||
async function getAllPlaceholdersWithValues(selectedRagTables = []) {
|
||
try {
|
||
console.log('[RAG] Начинаем загрузку плейсхолдеров...');
|
||
|
||
// Получаем колонки с плейсхолдерами
|
||
let columns = await encryptedDb.getData('user_columns', {});
|
||
|
||
// Фильтруем по выбранным RAG таблицам, если они указаны
|
||
if (selectedRagTables && selectedRagTables.length > 0) {
|
||
columns = columns.filter(col => selectedRagTables.includes(col.table_id));
|
||
console.log(`[RAG] Фильтруем по RAG таблицам: ${selectedRagTables.join(', ')}`);
|
||
}
|
||
console.log(`[RAG] Получено колонок: ${columns.length}`);
|
||
|
||
const columnsWithPlaceholders = columns.filter(col => col.placeholder && col.placeholder.trim() !== '');
|
||
console.log(`[RAG] Колонок с плейсхолдерами: ${columnsWithPlaceholders.length}`);
|
||
|
||
if (columnsWithPlaceholders.length === 0) {
|
||
console.log('[RAG] Нет колонок с плейсхолдерами');
|
||
return {};
|
||
}
|
||
|
||
// Получаем значения для каждой колонки с плейсхолдером
|
||
const map = {};
|
||
for (const column of columnsWithPlaceholders) {
|
||
try {
|
||
console.log(`[RAG] Получаем значение для плейсхолдера: ${column.placeholder} (column_id: ${column.id})`);
|
||
|
||
// Получаем первое значение для этой колонки
|
||
const values = await encryptedDb.getData('user_cell_values', { column_id: column.id }, 1);
|
||
console.log(`[RAG] Найдено значений для ${column.placeholder}: ${values ? values.length : 0}`);
|
||
|
||
if (values && values.length > 0 && values[0].value) {
|
||
map[column.placeholder] = values[0].value;
|
||
console.log(`[RAG] Установлено значение для ${column.placeholder}: ${values[0].value.substring(0, 50)}...`);
|
||
} else {
|
||
console.log(`[RAG] Нет значений для плейсхолдера ${column.placeholder}`);
|
||
}
|
||
} catch (error) {
|
||
console.error(`[RAG] Ошибка получения значения для плейсхолдера ${column.placeholder}:`, error);
|
||
}
|
||
}
|
||
|
||
console.log(`[RAG] Итоговый объект плейсхолдеров:`, Object.keys(map));
|
||
return map;
|
||
} catch (error) {
|
||
console.error('[RAG] Ошибка получения плейсхолдеров:', error);
|
||
return {};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Подставляет значения плейсхолдеров в строку (например, systemPrompt)
|
||
* Пример: "Добро пожаловать, {client_name}!" => "Добро пожаловать, ООО Ромашка!"
|
||
*/
|
||
function replacePlaceholders(str, placeholders) {
|
||
if (!str || typeof str !== 'string') return str;
|
||
return str.replace(/\{([a-zA-Z0-9_]+)\}/g, (match, key) => {
|
||
return key in placeholders ? placeholders[key] : match;
|
||
});
|
||
}
|
||
|
||
function parseIfArray(val) {
|
||
if (typeof val === 'string') {
|
||
try {
|
||
const arr = JSON.parse(val);
|
||
if (Array.isArray(arr)) return arr;
|
||
} catch {}
|
||
}
|
||
return Array.isArray(val) ? val : (val ? [val] : []);
|
||
}
|
||
|
||
async function generateLLMResponse({
|
||
userQuestion,
|
||
context,
|
||
clarifyingAnswer,
|
||
objectionAnswer,
|
||
answer,
|
||
systemPrompt,
|
||
userTags,
|
||
product,
|
||
priority,
|
||
date,
|
||
rules,
|
||
history,
|
||
model,
|
||
selectedRagTables
|
||
}) {
|
||
console.log(`[RAG] generateLLMResponse called with:`, {
|
||
userQuestion,
|
||
context,
|
||
answer,
|
||
systemPrompt: systemPrompt ? systemPrompt.substring(0, 100) + '...' : 'null',
|
||
userTags,
|
||
product,
|
||
priority,
|
||
date,
|
||
model,
|
||
historyLength: history ? history.length : 0
|
||
});
|
||
|
||
try {
|
||
const aiAssistant = require('./ai-assistant');
|
||
|
||
// Создаем контекст беседы с RAG данными
|
||
const conversationContext = createConversationContext({
|
||
userQuestion,
|
||
ragAnswer: answer,
|
||
ragContext: context,
|
||
history,
|
||
product,
|
||
priority,
|
||
date
|
||
}, 'generateLLMResponse');
|
||
|
||
// Формируем улучшенный промпт для LLM с учетом найденной информации
|
||
let prompt = `Вопрос пользователя: ${userQuestion}`;
|
||
|
||
// Добавляем найденную информацию из RAG
|
||
if (answer) {
|
||
// Формат: делаем RAG ответ главным, вопрос - контекстом
|
||
prompt = `База знаний содержит ответ:\n"${answer}"\n\nВопрос пользователя: ${userQuestion}\n\nДай пользователю этот ответ из базы знаний.`;
|
||
}
|
||
|
||
if (context) {
|
||
prompt += `\n\nДополнительный контекст: ${context}`;
|
||
}
|
||
|
||
if (product) {
|
||
prompt += `\n\nПродукт: ${product}`;
|
||
}
|
||
|
||
if (priority) {
|
||
prompt += `\n\nПриоритет: ${priority}`;
|
||
}
|
||
|
||
if (date) {
|
||
prompt += `\n\nДата: ${date}`;
|
||
}
|
||
|
||
// --- ДОБАВЛЕНО: подстановка плейсхолдеров ---
|
||
let finalSystemPrompt = systemPrompt;
|
||
if (systemPrompt && systemPrompt.includes('{')) {
|
||
const placeholders = await getAllPlaceholdersWithValues(selectedRagTables);
|
||
finalSystemPrompt = replacePlaceholders(systemPrompt, placeholders);
|
||
console.log(`[RAG] Подставлены плейсхолдеры в системный промпт`);
|
||
}
|
||
// --- КОНЕЦ ДОБАВЛЕНИЯ ---
|
||
|
||
// Системный промпт полностью настраивается пользователем в /settings/ai/assistant
|
||
// RAG ответ уже добавлен в prompt выше
|
||
|
||
console.log(`[RAG] Сформированный промпт:`, prompt.substring(0, 200) + '...');
|
||
|
||
// Получаем ответ от AI с учетом истории беседы
|
||
let llmResponse;
|
||
|
||
// Формируем сообщения для LLM
|
||
const messages = [];
|
||
if (finalSystemPrompt) {
|
||
messages.push({ role: 'system', content: finalSystemPrompt });
|
||
}
|
||
for (const h of (history || [])) {
|
||
if (h && h.content) {
|
||
const role = h.role === 'assistant' ? 'assistant' : 'user';
|
||
messages.push({ role, content: h.content });
|
||
}
|
||
}
|
||
messages.push({ role: 'user', content: prompt });
|
||
|
||
try {
|
||
// ✨ НОВОЕ: Используем очередь (если включена)
|
||
if (USE_AI_QUEUE) {
|
||
try {
|
||
llmResponse = await aiQueue.addTask({
|
||
messages,
|
||
model
|
||
// Приоритет не используется - все запросы обрабатываются FIFO
|
||
});
|
||
|
||
console.log('[RAG] LLM response from queue:', llmResponse ? llmResponse.substring(0, 100) + '...' : 'null');
|
||
return llmResponse;
|
||
|
||
} catch (queueError) {
|
||
console.warn('[RAG] Queue error, fallback to direct call:', queueError.message);
|
||
|
||
// Fallback: если очередь переполнена и есть ответ из RAG - возвращаем его
|
||
if (queueError.message.includes('переполнена') && answer) {
|
||
console.log('[RAG] Возврат прямого ответа из RAG (очередь переполнена)');
|
||
return answer;
|
||
}
|
||
|
||
// Продолжаем к прямому вызову
|
||
}
|
||
}
|
||
|
||
// Прямой вызов Ollama (если очередь отключена или ошибка очереди)
|
||
const ollamaUrl = ollamaConfig.getBaseUrl();
|
||
const timeouts = ollamaConfig.getTimeouts();
|
||
|
||
// Логируем размер промпта для отладки
|
||
const promptSize = JSON.stringify(messages).length;
|
||
console.log(`[RAG] Отправка запроса в Ollama. Размер промпта: ${promptSize} символов, таймаут: ${timeouts.ollamaChat/1000}с`);
|
||
|
||
// Проверяем размер промпта и предупреждаем, если он большой
|
||
if (promptSize > 10000) {
|
||
console.warn(`[RAG] ⚠️ Большой промпт (${promptSize} символов). Возможны проблемы с производительностью.`);
|
||
}
|
||
|
||
const response = await axios.post(`${ollamaUrl}/api/chat`, {
|
||
model: model || ollamaConfig.getDefaultModel(),
|
||
messages: messages,
|
||
stream: false
|
||
}, {
|
||
timeout: timeouts.ollamaChat
|
||
});
|
||
|
||
llmResponse = response.data.message.content;
|
||
|
||
} catch (error) {
|
||
const isTimeout = error.message && (
|
||
error.message.includes('timeout') ||
|
||
error.message.includes('ETIMEDOUT') ||
|
||
error.message.includes('ECONNABORTED')
|
||
);
|
||
|
||
if (isTimeout) {
|
||
console.warn(`[RAG] Ollama timeout после ${timeouts.ollamaChat/1000}с. Возможно, модель перегружена или контекст слишком большой.`);
|
||
} else {
|
||
console.error(`[RAG] Error in Ollama call:`, error.message);
|
||
}
|
||
|
||
// Финальный fallback - возврат ответа из RAG
|
||
if (answer) {
|
||
console.log('[RAG] Возврат прямого ответа из RAG (ошибка Ollama)');
|
||
return answer;
|
||
}
|
||
|
||
// Если был таймаут и нет ответа из RAG - возвращаем более информативное сообщение
|
||
if (isTimeout) {
|
||
return 'Извините, обработка запроса заняла слишком много времени. Пожалуйста, попробуйте упростить ваш вопрос или повторите попытку позже.';
|
||
}
|
||
|
||
return 'Извините, произошла ошибка при генерации ответа.';
|
||
}
|
||
|
||
console.log(`[RAG] LLM response generated:`, llmResponse ? (typeof llmResponse === 'string' ? llmResponse.substring(0, 100) + '...' : JSON.stringify(llmResponse).substring(0, 100) + '...') : 'null');
|
||
return llmResponse;
|
||
} catch (error) {
|
||
console.error(`[RAG] Error generating LLM response:`, error);
|
||
return 'Извините, произошла ошибка при генерации ответа.';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Создает контекст беседы с RAG данными
|
||
*/
|
||
function createConversationContext({
|
||
userQuestion,
|
||
ragAnswer,
|
||
ragContext,
|
||
history,
|
||
product,
|
||
priority,
|
||
date
|
||
}, source = 'generic') {
|
||
const context = {
|
||
currentQuestion: userQuestion,
|
||
ragData: {
|
||
answer: ragAnswer,
|
||
context: ragContext,
|
||
product,
|
||
priority,
|
||
date
|
||
},
|
||
conversationHistory: history || [],
|
||
hasRagData: !!(ragAnswer || ragContext),
|
||
isFollowUpQuestion: history && history.length > 0
|
||
};
|
||
|
||
console.log(`[RAG] Создан контекст беседы (${source}):`, {
|
||
hasRagData: context.hasRagData,
|
||
historyLength: context.conversationHistory.length,
|
||
isFollowUp: context.isFollowUpQuestion
|
||
});
|
||
|
||
return context;
|
||
}
|
||
|
||
/**
|
||
* Улучшенная функция RAG с поддержкой беседы
|
||
*/
|
||
async function ragAnswerWithConversation({
|
||
tableId,
|
||
userQuestion,
|
||
product = null,
|
||
threshold = 300,
|
||
history = [],
|
||
conversationId = null,
|
||
forceReindex = false
|
||
}) {
|
||
console.log(`[RAG] ragAnswerWithConversation: tableId=${tableId}, question="${userQuestion}", historyLength=${history.length}`);
|
||
|
||
// Получаем базовый RAG результат
|
||
const ragResult = await ragAnswer({ tableId, userQuestion, product, threshold, forceReindex });
|
||
|
||
// Анализируем контекст беседы
|
||
const conversationContext = createConversationContext({
|
||
userQuestion,
|
||
ragAnswer: ragResult.answer,
|
||
ragContext: ragResult.context,
|
||
history,
|
||
product: ragResult.product,
|
||
priority: ragResult.priority,
|
||
date: ragResult.date
|
||
}, 'ragAnswerWithConversation');
|
||
|
||
// Если это уточняющий вопрос и есть история
|
||
if (conversationContext.isFollowUpQuestion && conversationContext.hasRagData) {
|
||
console.log(`[RAG] Обнаружен уточняющий вопрос с RAG данными`);
|
||
|
||
// Проверяем, есть ли точный ответ в первом поиске
|
||
if (ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= threshold) {
|
||
console.log(`[RAG] Найден точный ответ (score=${ragResult.score}), возвращаем ответ из базы без модификаций`);
|
||
return {
|
||
...ragResult,
|
||
// Возвращаем чистый ответ
|
||
answer: ragResult.answer,
|
||
conversationContext,
|
||
isFollowUp: true
|
||
};
|
||
}
|
||
|
||
// Модифицируем вопрос с учетом контекста (only if no confident match)
|
||
const contextualQuestion = `${userQuestion}\n\nКонтекст предыдущих ответов: ${history.map(msg => msg.content).join('\n')}`;
|
||
|
||
// Повторяем поиск с контекстуализированным вопросом
|
||
const contextualRagResult = await ragAnswer({
|
||
tableId,
|
||
userQuestion: contextualQuestion,
|
||
product,
|
||
threshold,
|
||
forceReindex
|
||
});
|
||
|
||
// Объединяем результаты
|
||
return {
|
||
...contextualRagResult,
|
||
conversationContext,
|
||
isFollowUp: true
|
||
};
|
||
}
|
||
|
||
return {
|
||
...ragResult,
|
||
conversationContext,
|
||
isFollowUp: false
|
||
};
|
||
}
|
||
|
||
// ✨ НОВОЕ: Функция для запуска AI Queue Worker
|
||
function startQueueWorker() {
|
||
if (USE_AI_QUEUE) {
|
||
aiQueue.startWorker();
|
||
logger.info('[RAG] ✅ AI Queue Worker запущен из ragService');
|
||
} else {
|
||
logger.info('[RAG] AI Queue отключена (USE_AI_QUEUE=false)');
|
||
}
|
||
}
|
||
|
||
// ✨ НОВОЕ: Функция для остановки AI Queue Worker
|
||
function stopQueueWorker() {
|
||
if (aiQueue && aiQueue.workerInterval) {
|
||
aiQueue.stopWorker();
|
||
logger.info('[RAG] ⏹️ AI Queue Worker остановлен');
|
||
}
|
||
}
|
||
|
||
// ✨ НОВОЕ: Получение статистики
|
||
function getQueueStats() {
|
||
return aiQueue.getStats();
|
||
}
|
||
|
||
function getCacheStats() {
|
||
return {
|
||
...aiCache.getStats(),
|
||
byType: aiCache.getStatsByType()
|
||
};
|
||
}
|
||
|
||
module.exports = {
|
||
ragAnswer,
|
||
getTableData,
|
||
generateLLMResponse,
|
||
ragAnswerWithConversation,
|
||
startQueueWorker, // ✨ НОВОЕ
|
||
stopQueueWorker, // ✨ НОВОЕ
|
||
getQueueStats, // ✨ НОВОЕ
|
||
getCacheStats // ✨ НОВОЕ
|
||
};
|