ваше сообщение коммита

This commit is contained in:
2025-08-07 20:27:24 +03:00
parent cde35ac576
commit 0a72902c37
44 changed files with 3594 additions and 1447 deletions

View File

@@ -70,43 +70,50 @@ class AIAssistant {
}
// Создание экземпляра ChatOllama с нужными параметрами
createChat(language = 'ru', customSystemPrompt = '') {
createChat(customSystemPrompt = '') {
// Используем кастомный системный промпт, если он передан, иначе используем дефолтный
let systemPrompt = customSystemPrompt;
if (!systemPrompt) {
systemPrompt = language === 'ru'
? 'Вы - полезный ассистент. Отвечайте на русском языке кратко и по делу.'
: 'You are a helpful assistant. Respond in English briefly and to the point.';
systemPrompt = 'Вы - полезный ассистент. Отвечайте на русском языке кратко и по делу.';
}
return new ChatOllama({
baseUrl: this.baseUrl,
model: this.defaultModel,
system: systemPrompt,
temperature: 0.3, // Уменьшаем для более предсказуемых ответов
maxTokens: 100, // Еще больше уменьшаем для быстрого ответа
timeout: 60000, // Увеличиваем таймаут до 60 секунд
temperature: 0.7, // Восстанавливаем для более творческих ответов
maxTokens: 2048, // Восстанавливаем для полных ответов
timeout: 300000, // 5 минут для качественной обработки
numCtx: 4096, // Увеличиваем контекст для лучшего понимания
numGpu: 1, // Используем GPU
numThread: 8, // Оптимальное количество потоков
repeatPenalty: 1.1, // Штраф за повторения
topK: 40, // Разнообразие ответов
topP: 0.9, // Ядерная выборка
tfsZ: 1, // Tail free sampling
mirostat: 2, // Mirostat 2.0 для контроля качества
mirostatTau: 5, // Целевая перплексия
mirostatEta: 0.1, // Скорость адаптации
grammar: '', // Грамматика (если нужна)
seed: -1, // Случайный сид
numPredict: -1, // Неограниченная длина
stop: [], // Стоп-слова
stream: false, // Без стриминга для стабильности
options: {
num_ctx: 512, // Еще больше уменьшаем контекст для экономии памяти
num_thread: 12, // Увеличиваем количество потоков еще больше
num_gpu: 1,
num_gqa: 8,
rope_freq_base: 1000000,
rope_freq_scale: 0.5,
repeat_penalty: 1.1, // Добавляем штраф за повторения
top_k: 20, // Еще больше ограничиваем выбор токенов
top_p: 0.8, // Уменьшаем nucleus sampling
temperature: 0.1, // Еще больше уменьшаем для более предсказуемых ответов
numCtx: 4096,
numGpu: 1,
numThread: 8,
repeatPenalty: 1.1,
topK: 40,
topP: 0.9,
tfsZ: 1,
mirostat: 2,
mirostatTau: 5,
mirostatEta: 0.1
}
});
}
// Определение языка сообщения
detectLanguage(message) {
const cyrillicPattern = /[а-яА-ЯёЁ]/;
return cyrillicPattern.test(message) ? 'ru' : 'en';
}
// Определение приоритета запроса
getRequestPriority(message, history, rules) {
let priority = 0;
@@ -117,7 +124,7 @@ class AIAssistant {
}
// Приоритет по типу запроса
const urgentKeywords = ['срочно', 'urgent', 'важно', 'important', 'помоги', 'help'];
const urgentKeywords = ['срочно', 'важно', 'помоги'];
if (urgentKeywords.some(keyword => message.toLowerCase().includes(keyword))) {
priority += 20;
}
@@ -140,9 +147,9 @@ class AIAssistant {
}
// Основной метод для получения ответа
async getResponse(message, language = 'auto', history = null, systemPrompt = '', rules = null) {
async getResponse(message, history = null, systemPrompt = '', rules = null) {
try {
// console.log('getResponse called with:', { message, language, history, systemPrompt, rules });
// console.log('getResponse called with:', { message, history, systemPrompt, rules });
// Очищаем старый кэш
this.cleanupCache();
@@ -171,7 +178,6 @@ class AIAssistant {
// Добавляем запрос в очередь
const requestId = await aiQueue.addRequest({
message,
language,
history,
systemPrompt,
rules
@@ -181,7 +187,7 @@ class AIAssistant {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Request timeout - очередь перегружена'));
}, 60000); // 60 секунд таймаут для очереди
}, 180000); // 180 секунд таймаут для очереди (увеличено с 60)
const onCompleted = (item) => {
if (item.id === requestId) {
@@ -204,62 +210,6 @@ class AIAssistant {
aiQueue.on('completed', onCompleted);
aiQueue.on('failed', onFailed);
});
// Определяем язык, если не указан явно
const detectedLanguage = language === 'auto' ? this.detectLanguage(message) : language;
// console.log('Detected language:', detectedLanguage);
// Формируем system prompt с учётом правил
let fullSystemPrompt = systemPrompt || '';
if (rules && typeof rules === 'object') {
fullSystemPrompt += '\n' + JSON.stringify(rules, null, 2);
}
// Формируем массив сообщений для Qwen2.5/OpenAI API
const messages = [];
if (fullSystemPrompt) {
messages.push({ role: 'system', content: fullSystemPrompt });
}
if (Array.isArray(history) && history.length > 0) {
for (const msg of history) {
if (msg.role && msg.content) {
messages.push({ role: msg.role, content: msg.content });
}
}
}
// Добавляем текущее сообщение пользователя
messages.push({ role: 'user', content: message });
let response = null;
// Пробуем прямой API запрос (OpenAI-совместимый endpoint)
try {
// console.log('Trying direct API request...');
response = await this.fallbackRequestOpenAI(messages, detectedLanguage, fullSystemPrompt);
// console.log('Direct API response received:', response);
} catch (error) {
// console.error('Error in direct API request:', error);
// Если прямой запрос не удался, пробуем через ChatOllama (склеиваем сообщения в текст)
const chat = this.createChat(detectedLanguage, fullSystemPrompt);
try {
const prompt = messages.map(m => `${m.role === 'user' ? 'Пользователь' : m.role === 'assistant' ? 'Ассистент' : 'Система'}: ${m.content}`).join('\n');
// console.log('Sending request to ChatOllama...');
const chatResponse = await chat.invoke(prompt);
// console.log('ChatOllama response:', chatResponse);
response = chatResponse.content;
} catch (chatError) {
// console.error('Error using ChatOllama:', chatError);
throw chatError;
}
}
// Кэшируем ответ
if (response) {
aiCache.set(cacheKey, response);
}
return response;
} catch (error) {
// console.error('Error in getResponse:', error);
return 'Извините, я не смог обработать ваш запрос. Пожалуйста, попробуйте позже.';
@@ -267,9 +217,9 @@ class AIAssistant {
}
// Новый метод для OpenAI/Qwen2.5 совместимого endpoint
async fallbackRequestOpenAI(messages, language, systemPrompt = '') {
async fallbackRequestOpenAI(messages, systemPrompt = '') {
try {
// console.log('Using fallbackRequestOpenAI with:', { messages, language, systemPrompt });
// console.log('Using fallbackRequestOpenAI with:', { messages, systemPrompt });
const model = this.defaultModel;
// Создаем AbortController для таймаута
@@ -284,23 +234,25 @@ class AIAssistant {
messages,
stream: false,
options: {
temperature: 0.3,
num_predict: 150, // Уменьшаем максимальную длину ответа для ускорения
num_ctx: 512, // Уменьшаем контекст для экономии памяти и ускорения
num_thread: 12, // Увеличиваем количество потоков для ускорения
temperature: 0.7,
num_predict: 2048, // Восстанавливаем для полных ответов
num_ctx: 4096, // Восстанавливаем контекст для лучшего понимания
num_thread: 8, // Оптимальное количество потоков
num_gpu: 1, // Используем GPU если доступен
num_gqa: 8, // Оптимизация для qwen2.5
rope_freq_base: 1000000, // Оптимизация для qwen2.5
rope_freq_scale: 0.5, // Оптимизация для qwen2.5
repeat_penalty: 1.1, // Добавляем штраф за повторения
top_k: 20, // Уменьшаем выбор токенов для ускорения
top_p: 0.8, // Уменьшаем nucleus sampling для ускорения
mirostat: 2, // Используем mirostat для стабильности
mirostat_tau: 5.0, // Настройка mirostat
mirostat_eta: 0.1, // Настройка mirostat
},
}),
signal: controller.signal,
repeat_penalty: 1.1, // Восстанавливаем штраф за повторения
top_k: 40, // Восстанавливаем разнообразие ответов
top_p: 0.9, // Восстанавливаем nucleus sampling
tfs_z: 1, // Tail free sampling
mirostat: 2, // Mirostat 2.0 для контроля качества
mirostat_tau: 5, // Целевая перплексия
mirostat_eta: 0.1, // Скорость адаптации
seed: -1, // Случайный сид
stop: [] // Стоп-слова
}
})
});
clearTimeout(timeoutId);

View File

@@ -10,7 +10,7 @@ class AIQueue extends EventEmitter {
super();
this.queue = [];
this.processing = false;
this.maxConcurrent = 2; // Максимум 2 запроса одновременно
this.maxConcurrent = 1; // Максимум 1 запрос одновременно (последовательная обработка)
this.activeRequests = 0;
this.stats = {
total: 0,
@@ -51,6 +51,7 @@ class AIQueue extends EventEmitter {
}
this.processing = true;
logger.info(`[AIQueue] Начинаем обработку очереди. Запросов в очереди: ${this.queue.length}`);
while (this.queue.length > 0 && this.activeRequests < this.maxConcurrent) {
const item = this.queue.shift();
@@ -58,6 +59,7 @@ class AIQueue extends EventEmitter {
this.activeRequests++;
item.status = 'processing';
logger.info(`[AIQueue] Обрабатываем запрос ${item.id} (приоритет: ${item.priority})`);
try {
const startTime = Date.now();
@@ -71,7 +73,7 @@ class AIQueue extends EventEmitter {
this.stats.completed++;
this.updateAvgResponseTime(responseTime);
logger.info(`[AIQueue] Request ${item.id} completed in ${responseTime}ms`);
logger.info(`[AIQueue] Запрос ${item.id} завершен за ${responseTime}ms`);
// Эмитим событие о завершении
this.emit('completed', item);
@@ -81,7 +83,7 @@ class AIQueue extends EventEmitter {
item.error = error.message;
this.stats.failed++;
logger.error(`[AIQueue] Request ${item.id} failed:`, error.message);
logger.error(`[AIQueue] Запрос ${item.id} завершился с ошибкой:`, error.message);
// Эмитим событие об ошибке
this.emit('failed', item);
@@ -91,6 +93,7 @@ class AIQueue extends EventEmitter {
}
this.processing = false;
logger.info(`[AIQueue] Обработка очереди завершена. Осталось запросов: ${this.queue.length}`);
// Если в очереди еще есть запросы, продолжаем обработку
if (this.queue.length > 0) {
@@ -118,7 +121,7 @@ class AIQueue extends EventEmitter {
messages.push({ role: 'user', content: request.message });
// Прямой вызов API без очереди
return await aiAssistant.fallbackRequestOpenAI(messages, request.language, request.systemPrompt);
return await aiAssistant.fallbackRequestOpenAI(messages, request.systemPrompt);
}
// Обновление средней скорости ответа

View File

@@ -50,6 +50,7 @@ async function getSettings() {
);
supportEmail = em.rows[0] || null;
}
return {
...setting,
telegramBot,
@@ -58,12 +59,12 @@ async function getSettings() {
};
}
async function upsertSettings({ system_prompt, selected_rag_tables, languages, model, embedding_model, rules, updated_by, telegram_settings_id, email_settings_id, system_message }) {
async function upsertSettings({ system_prompt, selected_rag_tables, model, embedding_model, rules, updated_by, telegram_settings_id, email_settings_id, system_message }) {
const data = {
id: 1,
system_prompt,
selected_rag_tables,
languages,
languages: ['ru'], // Устанавливаем русский язык по умолчанию
model,
embedding_model,
rules,

View File

@@ -519,6 +519,20 @@ class AuthService {
} else {
// Если пользователь не является администратором, сбрасываем роль на "user", если она была "admin"
try {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
const userResult = await db.getQuery()(
`
SELECT u.id, u.role FROM users u
@@ -544,6 +558,76 @@ class AuthService {
}
}
/**
* Перепроверяет админский статус ВСЕХ пользователей с кошельками
* @returns {Promise<void>}
*/
async recheckAllUsersAdminStatus() {
logger.info('Starting recheck of admin status for all users with wallets');
try {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
// Получаем всех пользователей с кошельками
const usersResult = await db.getQuery()(
`
SELECT DISTINCT u.id, u.role, decrypt_text(ui.provider_id_encrypted, $1) as address
FROM users u
JOIN user_identities ui ON u.id = ui.user_id
WHERE ui.provider_encrypted = encrypt_text('wallet', $1)
`,
[encryptionKey]
);
logger.info(`Found ${usersResult.rows.length} users with wallets to recheck`);
// Перепроверяем каждого пользователя
for (const user of usersResult.rows) {
try {
const address = user.address;
const currentRole = user.role;
logger.info(`Rechecking admin status for user ${user.id} with address ${address}`);
// Проверяем баланс токенов
const isAdmin = await checkAdminRole(address);
// Определяем новую роль
const newRole = isAdmin ? 'admin' : 'user';
// Обновляем роль только если она изменилась
if (currentRole !== newRole) {
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [newRole, user.id]);
logger.info(`Updated user ${user.id} role from ${currentRole} to ${newRole} (address: ${address})`);
} else {
logger.info(`User ${user.id} role unchanged: ${currentRole} (address: ${address})`);
}
} catch (userError) {
logger.error(`Error rechecking user ${user.id}: ${userError.message}`);
// Продолжаем с другими пользователями
}
}
logger.info('Completed recheck of admin status for all users');
} catch (error) {
logger.error(`Error in recheckAllUsersAdminStatus: ${error.message}`);
throw error;
}
}
/**
* Очистка старых гостевых идентификаторов
* @param {number} userId - ID пользователя

View File

@@ -313,18 +313,6 @@ class EmailBotService {
return;
}
// Проверяем, не обрабатывали ли мы уже это письмо
if (messageId) {
const existingMessage = await encryptedDb.getData('messages', {
metadata: { $like: `%"messageId":"${messageId}"%` }
}, 1);
if (existingMessage.length > 0) {
logger.info(`[EmailBot] Письмо с Message-ID ${messageId} уже обработано, пропускаем`);
return;
}
}
// 1. Найти или создать пользователя
const { userId, role } = await identityService.findOrCreateUserWithRole('email', fromEmail);
if (await isUserBlocked(userId)) {
@@ -332,6 +320,31 @@ class EmailBotService {
return;
}
// Проверяем, не обрабатывали ли мы уже это письмо
if (messageId) {
// Проверка дубликатов на основе Message-ID
try {
const existingMessage = await encryptedDb.getData(
'messages',
{
user_id: userId,
channel: 'email',
direction: 'in',
message_id: messageId
},
1
);
if (existingMessage.length > 0) {
logger.info(`[EmailBot] Игнорируем дубликат письма от ${fromEmail} (Message-ID: ${messageId})`);
return;
}
} catch (error) {
logger.error(`[EmailBot] Ошибка при проверке дубликатов: ${error.message}`);
// Продолжаем обработку в случае ошибки
}
}
// 1.1 Найти или создать беседу
let conversationResult = await encryptedDb.getData(
'conversations',
@@ -376,13 +389,7 @@ class EmailBotService {
attachment_mimetype: att.contentType,
attachment_size: att.size,
attachment_data: att.content,
metadata: JSON.stringify({
subject,
html,
messageId: messageId,
uid: uid,
fromEmail: fromEmail
})
message_id: messageId // Сохраняем Message-ID для дедупликации (будет зашифрован в message_id_encrypted)
}
);
}
@@ -398,13 +405,7 @@ class EmailBotService {
role: role,
direction: 'in',
created_at: new Date(),
metadata: JSON.stringify({
subject,
html,
messageId: messageId,
uid: uid,
fromEmail: fromEmail
})
message_id: messageId // Сохраняем Message-ID для дедупликации (будет зашифрован в message_id_encrypted)
}
);
}
@@ -421,7 +422,7 @@ class EmailBotService {
if (ragTableId) {
// Сначала ищем ответ через RAG
const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: text });
if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= 0.3) {
if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= 0.1) {
aiResponse = ragResult.answer;
} else {
aiResponse = await generateLLMResponse({

View File

@@ -410,9 +410,10 @@ class EncryptedDataService {
*/
shouldEncryptColumn(column) {
const encryptableTypes = ['text', 'varchar', 'character varying', 'json', 'jsonb'];
const excludedColumns = ['created_at', 'updated_at', 'id', 'metadata']; // Добавляем metadata в исключения
return encryptableTypes.includes(column.data_type) &&
!column.column_name.includes('_encrypted') &&
!['created_at', 'updated_at', 'id'].includes(column.column_name);
!excludedColumns.includes(column.column_name);
}
/**

View File

@@ -122,7 +122,7 @@ async function ragAnswer({ tableId, userQuestion, product = null, threshold = 10
// Поиск
let results = [];
if (rowsForUpsert.length > 0) {
results = await vectorSearch.search(tableId, userQuestion, 2); // Уменьшаем до 2 результатов
results = await vectorSearch.search(tableId, userQuestion, 3); // Увеличиваем до 3 результатов для лучшего поиска
// console.log(`[RAG] Search completed, got ${results.length} results`);
// Подробное логирование результатов поиска
@@ -171,7 +171,7 @@ async function ragAnswer({ tableId, userQuestion, product = null, threshold = 10
product: best?.metadata?.product,
priority: best?.metadata?.priority,
date: best?.metadata?.date,
score: best?.score,
score: best?.score !== undefined && best?.score !== null ? Number(best.score) : null,
};
// Кэшируем результат
@@ -188,17 +188,48 @@ async function ragAnswer({ tableId, userQuestion, product = null, threshold = 10
* Возвращает объект: { 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;
try {
console.log('[RAG] Начинаем загрузку плейсхолдеров...');
// Получаем все колонки с плейсхолдерами
const columns = await encryptedDb.getData('user_columns', {});
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 {};
}
return map;
}
/**
@@ -235,67 +266,222 @@ async function generateLLMResponse({
date,
rules,
history,
model,
language
model
}) {
// console.log(`[RAG] generateLLMResponse called with:`, {
// userQuestion,
// context,
// answer,
// systemPrompt,
// userTags,
// product,
// priority,
// date,
// model,
// language
// });
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');
// Формируем промпт для LLM
let prompt = userQuestion;
// Создаем контекст беседы с RAG данными
const conversationContext = createConversationContext({
userQuestion,
ragAnswer: answer,
ragContext: context,
history,
product,
priority,
date
});
if (context) {
prompt += `\n\nКонтекст: ${context}`;
// Формируем улучшенный промпт для LLM с учетом найденной информации
let prompt = `Вопрос пользователя: ${userQuestion}`;
// Добавляем найденную информацию из RAG
if (answer) {
prompt += `\n\nНайденный ответ из базы знаний: ${answer}`;
}
if (answer) {
prompt += `\n\nНайденный ответ: ${answer}`;
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();
finalSystemPrompt = replacePlaceholders(systemPrompt, placeholders);
console.log(`[RAG] Подставлены плейсхолдеры в системный промпт`);
}
// --- КОНЕЦ ДОБАВЛЕНИЯ ---
// Получаем ответ от AI
const llmResponse = await aiAssistant.getResponse(
prompt,
language || 'auto',
history,
finalSystemPrompt,
rules
);
// Используем системный промпт из настроек, если он есть
if (finalSystemPrompt && finalSystemPrompt.trim()) {
prompt += `\n\nСистемная инструкция: ${finalSystemPrompt}`;
} else {
// Fallback инструкция, если системный промпт не настроен
prompt += `\n\nИнструкция: Используй найденную информацию из базы знаний для ответа. Если найденный ответ подходит к вопросу пользователя, используй его как основу. Если нужно дополнить или уточнить ответ, сделай это. Поддерживай естественную беседу, учитывая предыдущие сообщения. Отвечай на русском языке кратко и по делу. Если пользователь задает уточняющие вопросы, используй контекст предыдущих ответов.`;
}
// console.log(`[RAG] LLM response generated:`, llmResponse);
console.log(`[RAG] Сформированный промпт:`, prompt.substring(0, 200) + '...');
// Получаем ответ от AI с учетом истории беседы
let llmResponse;
try {
llmResponse = await aiAssistant.getResponse(
prompt,
history,
finalSystemPrompt,
rules
);
} catch (error) {
console.error(`[RAG] Error in getResponse:`, error.message);
// Fallback: если очередь перегружена, возвращаем найденный ответ напрямую
if (error.message.includes('очередь перегружена') && answer) {
console.log(`[RAG] Queue overloaded, returning direct answer from RAG`);
return answer;
}
// Другой fallback для других ошибок
return 'Извините, произошла ошибка при генерации ответа.';
}
console.log(`[RAG] LLM response generated:`, llmResponse ? llmResponse.substring(0, 100) + '...' : 'null');
return llmResponse;
} catch (error) {
// console.error(`[RAG] Error generating LLM response:`, error);
console.error(`[RAG] Error generating LLM response:`, error);
return 'Извините, произошла ошибка при генерации ответа.';
}
}
/**
* Создает контекст беседы с RAG данными
*/
function createConversationContext({
userQuestion,
ragAnswer,
ragContext,
history,
product,
priority,
date
}) {
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] Создан контекст беседы:`, {
hasRagData: context.hasRagData,
historyLength: context.conversationHistory.length,
isFollowUp: context.isFollowUpQuestion
});
return context;
}
/**
* Улучшенная функция RAG с поддержкой беседы
*/
async function ragAnswerWithConversation({
tableId,
userQuestion,
product = null,
threshold = 10,
history = [],
conversationId = null
}) {
console.log(`[RAG] ragAnswerWithConversation: tableId=${tableId}, question="${userQuestion}", historyLength=${history.length}`);
// Получаем базовый RAG результат
const ragResult = await ragAnswer({ tableId, userQuestion, product, threshold });
// Анализируем контекст беседы
const conversationContext = createConversationContext({
userQuestion,
ragAnswer: ragResult.answer,
ragContext: ragResult.context,
history,
product: ragResult.product,
priority: ragResult.priority,
date: ragResult.date
});
// Если это уточняющий вопрос и есть история
if (conversationContext.isFollowUpQuestion && conversationContext.hasRagData) {
console.log(`[RAG] Обнаружен уточняющий вопрос с RAG данными`);
// Проверяем, есть ли точный ответ в первом поиске
if (ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= 200) {
console.log(`[RAG] Найден точный ответ (score=${ragResult.score}), модифицируем с учетом контекста беседы`);
// Модифицируем точный ответ с учетом контекста беседы
let contextualAnswer = ragResult.answer;
if (history && history.length > 0) {
const contextSummary = history.slice(-3).map(msg => msg.content).join(' | ');
contextualAnswer = `Контекст: ${contextSummary}\n\nОтвет: ${ragResult.answer}`;
}
return {
...ragResult,
answer: contextualAnswer,
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
});
// Объединяем результаты
return {
...contextualRagResult,
conversationContext,
isFollowUp: true
};
}
return {
...ragResult,
conversationContext,
isFollowUp: false
};
}
module.exports = {
ragAnswer,
getTableData,
generateLLMResponse
generateLLMResponse,
ragAnswerWithConversation
};

View File

@@ -428,7 +428,7 @@ async function getBot() {
if (ragTableId) {
// Сначала ищем ответ через RAG
const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: content });
if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= 0.3) {
if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= 0.1) {
aiResponse = ragResult.answer;
} else {
aiResponse = await generateLLMResponse({