301 lines
10 KiB
JavaScript
301 lines
10 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/HB3-ACCELERATOR
|
||
*/
|
||
|
||
const encryptedDb = require('./encryptedDatabaseService');
|
||
const vectorSearch = require('./vectorSearchClient');
|
||
const { getProviderSettings } = require('./aiProviderSettingsService');
|
||
|
||
// console.log('[RAG] ragService.js loaded');
|
||
|
||
// Простой кэш для RAG результатов
|
||
const ragCache = new Map();
|
||
const RAG_CACHE_TTL = 5 * 60 * 1000; // 5 минут
|
||
|
||
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 })));
|
||
|
||
const cellValues = 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 = 10 }) {
|
||
// console.log(`[RAG] ragAnswer called: tableId=${tableId}, userQuestion="${userQuestion}"`);
|
||
|
||
// Проверяем кэш
|
||
const cacheKey = `${tableId}:${userQuestion}:${product}`;
|
||
const cached = ragCache.get(cacheKey);
|
||
if (cached && (Date.now() - cached.timestamp) < RAG_CACHE_TTL) {
|
||
// console.log(`[RAG] Returning cached result for: ${cacheKey}`);
|
||
return cached.result;
|
||
}
|
||
|
||
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 (rowsForUpsert.length > 0) {
|
||
await vectorSearch.upsert(tableId, rowsForUpsert);
|
||
// console.log(`[RAG] Upsert completed`);
|
||
} else {
|
||
// console.log(`[RAG] No rows to upsert, skipping`);
|
||
}
|
||
|
||
// Поиск
|
||
let results = [];
|
||
if (rowsForUpsert.length > 0) {
|
||
results = await vectorSearch.search(tableId, userQuestion, 2); // Уменьшаем до 2 результатов
|
||
// 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,
|
||
};
|
||
|
||
// Кэшируем результат
|
||
ragCache.set(cacheKey, {
|
||
result,
|
||
timestamp: Date.now()
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Загрузка всех плейсхолдеров и их значений из пользовательских таблиц
|
||
* Возвращает объект: { placeholder1: value1, placeholder2: value2, ... }
|
||
*/
|
||
async function getAllPlaceholdersWithValues() {
|
||
// Получаем все плейсхолдеры и их значения (берём первое значение для каждого плейсхолдера)
|
||
const result = await encryptedDb.getData('user_columns', {});
|
||
|
||
// Группируем по плейсхолдеру (берём первое значение)
|
||
const map = {};
|
||
for (const row of result) {
|
||
if (row.placeholder && !(row.placeholder in map)) {
|
||
map[row.placeholder] = row.value;
|
||
}
|
||
}
|
||
return map;
|
||
}
|
||
|
||
/**
|
||
* Подставляет значения плейсхолдеров в строку (например, 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,
|
||
language
|
||
}) {
|
||
// console.log(`[RAG] generateLLMResponse called with:`, {
|
||
// userQuestion,
|
||
// context,
|
||
// answer,
|
||
// systemPrompt,
|
||
// userTags,
|
||
// product,
|
||
// priority,
|
||
// date,
|
||
// model,
|
||
// language
|
||
// });
|
||
|
||
try {
|
||
const aiAssistant = require('./ai-assistant');
|
||
|
||
// Формируем промпт для LLM
|
||
let prompt = userQuestion;
|
||
|
||
if (context) {
|
||
prompt += `\n\nКонтекст: ${context}`;
|
||
}
|
||
|
||
if (answer) {
|
||
prompt += `\n\nНайденный ответ: ${answer}`;
|
||
}
|
||
|
||
if (product) {
|
||
prompt += `\n\nПродукт: ${product}`;
|
||
}
|
||
|
||
// --- ДОБАВЛЕНО: подстановка плейсхолдеров ---
|
||
let finalSystemPrompt = systemPrompt;
|
||
if (systemPrompt && systemPrompt.includes('{')) {
|
||
const placeholders = await getAllPlaceholdersWithValues();
|
||
finalSystemPrompt = replacePlaceholders(systemPrompt, placeholders);
|
||
}
|
||
// --- КОНЕЦ ДОБАВЛЕНИЯ ---
|
||
|
||
// Получаем ответ от AI
|
||
const llmResponse = await aiAssistant.getResponse(
|
||
prompt,
|
||
language || 'auto',
|
||
history,
|
||
finalSystemPrompt,
|
||
rules
|
||
);
|
||
|
||
// console.log(`[RAG] LLM response generated:`, llmResponse);
|
||
return llmResponse;
|
||
} catch (error) {
|
||
// console.error(`[RAG] Error generating LLM response:`, error);
|
||
return 'Извините, произошла ошибка при генерации ответа.';
|
||
}
|
||
}
|
||
|
||
module.exports = {
|
||
ragAnswer,
|
||
getTableData,
|
||
generateLLMResponse
|
||
};
|