ваше сообщение коммита
This commit is contained in:
@@ -0,0 +1 @@
|
||||
ALTER TABLE ai_providers_settings ADD COLUMN embedding_model VARCHAR(128);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE ai_assistant_settings ADD COLUMN embedding_model VARCHAR(128);
|
||||
@@ -423,9 +423,8 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
|
||||
const userMessage = userMessageResult.rows[0];
|
||||
logger.info('User message saved', { messageId: userMessage.id, conversationId });
|
||||
|
||||
// Получаем ответ от ИИ, только если это было текстовое сообщение
|
||||
// --- Новая логика автоответа ИИ по RAG ---
|
||||
let aiMessage = null;
|
||||
// --- Новая логика автоответа ИИ ---
|
||||
let shouldGenerateAiReply = true;
|
||||
if (senderType === 'admin') {
|
||||
// Если админ пишет не себе, не отвечаем
|
||||
@@ -441,41 +440,70 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
|
||||
if (aiSettings && aiSettings.rules_id) {
|
||||
rules = await aiAssistantRulesService.getRuleById(aiSettings.rules_id);
|
||||
}
|
||||
logger.info('AI System Prompt:', aiSettings ? aiSettings.system_prompt : 'not set');
|
||||
logger.info('AI Rules:', rules ? JSON.stringify(rules.rules) : 'not set');
|
||||
// Получаем последние 10 сообщений из диалога для истории (до текущего сообщения)
|
||||
const historyResult = await db.getQuery()(
|
||||
'SELECT sender_type, content FROM messages WHERE conversation_id = $1 AND id < $2 ORDER BY created_at DESC LIMIT 10',
|
||||
[conversationId, userMessage.id]
|
||||
);
|
||||
const history = historyResult.rows.reverse().map(msg => ({
|
||||
role: msg.sender_type === 'user' ? 'user' : 'assistant',
|
||||
content: msg.content
|
||||
}));
|
||||
const detectedLanguage = language === 'auto' ? aiAssistant.detectLanguage(messageContent) : language;
|
||||
logger.info('Getting AI response for:', messageContent);
|
||||
const aiResponseContent = await aiAssistant.getResponse(
|
||||
messageContent,
|
||||
detectedLanguage,
|
||||
history,
|
||||
aiSettings ? aiSettings.system_prompt : '',
|
||||
rules ? rules.rules : null
|
||||
);
|
||||
logger.info('AI response received' + (aiResponseContent ? '' : ' (empty)'), { conversationId });
|
||||
|
||||
if (aiResponseContent) {
|
||||
const aiMessageResult = await db.getQuery()(
|
||||
`INSERT INTO messages
|
||||
(conversation_id, user_id, content, sender_type, role, channel)
|
||||
VALUES ($1, $2, $3, 'assistant', 'assistant', 'web')
|
||||
RETURNING *`,
|
||||
[conversationId, userId, aiResponseContent]
|
||||
);
|
||||
aiMessage = aiMessageResult.rows[0];
|
||||
logger.info('AI response saved', { messageId: aiMessage.id, conversationId });
|
||||
// --- RAG автоответ ---
|
||||
let ragTableId = null;
|
||||
if (aiSettings && aiSettings.selected_rag_tables) {
|
||||
ragTableId = Array.isArray(aiSettings.selected_rag_tables)
|
||||
? aiSettings.selected_rag_tables[0]
|
||||
: aiSettings.selected_rag_tables;
|
||||
}
|
||||
let ragResult = null;
|
||||
if (ragTableId) {
|
||||
const { ragAnswer, generateLLMResponse } = require('../services/ragService');
|
||||
const threshold = 0.3;
|
||||
logger.info(`[RAG] Запуск поиска по RAG: tableId=${ragTableId}, вопрос="${messageContent}", threshold=${threshold}`);
|
||||
const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: messageContent, threshold });
|
||||
logger.info(`[RAG] Результат поиска по RAG:`, ragResult);
|
||||
if (ragResult && ragResult.answer && ragResult.score && ragResult.score > threshold) {
|
||||
logger.info(`[RAG] Найден confident-ответ (score=${ragResult.score}), отправляем ответ из базы.`);
|
||||
// Прямой ответ из RAG
|
||||
const aiMessageResult = await db.getQuery()(
|
||||
`INSERT INTO messages
|
||||
(conversation_id, user_id, content, sender_type, role, channel)
|
||||
VALUES ($1, $2, $3, 'assistant', 'assistant', 'web')
|
||||
RETURNING *`,
|
||||
[conversationId, userId, ragResult.answer]
|
||||
);
|
||||
aiMessage = aiMessageResult.rows[0];
|
||||
} else if (ragResult) {
|
||||
logger.info(`[RAG] Нет confident-ответа (score=${ragResult.score}), переходим к генерации через LLM.`);
|
||||
// Генерация через LLM с подстановкой значений из RAG
|
||||
const historyResult = await db.getQuery()(
|
||||
'SELECT sender_type, content FROM messages WHERE conversation_id = $1 AND id < $2 ORDER BY created_at DESC LIMIT 10',
|
||||
[conversationId, userMessage.id]
|
||||
);
|
||||
const history = historyResult.rows.reverse().map(msg => ({
|
||||
role: msg.sender_type === 'user' ? 'user' : 'assistant',
|
||||
content: msg.content
|
||||
}));
|
||||
const llmResponse = await generateLLMResponse({
|
||||
userQuestion: messageContent,
|
||||
context: ragResult.context,
|
||||
answer: ragResult.answer,
|
||||
clarifyingAnswer: ragResult.clarifyingAnswer,
|
||||
objectionAnswer: ragResult.objectionAnswer,
|
||||
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
||||
history,
|
||||
model: aiSettings ? aiSettings.model : undefined,
|
||||
language: aiSettings && aiSettings.languages && aiSettings.languages.length > 0 ? aiSettings.languages[0] : 'ru'
|
||||
});
|
||||
if (llmResponse) {
|
||||
const aiMessageResult = await db.getQuery()(
|
||||
`INSERT INTO messages
|
||||
(conversation_id, user_id, content, sender_type, role, channel)
|
||||
VALUES ($1, $2, $3, 'assistant', 'assistant', 'web')
|
||||
RETURNING *`,
|
||||
[conversationId, userId, llmResponse]
|
||||
);
|
||||
aiMessage = aiMessageResult.rows[0];
|
||||
} else {
|
||||
logger.info(`[RAG] Нет ни одного результата, прошедшего порог (${threshold}).`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// --- конец RAG автоответа ---
|
||||
} catch (aiError) {
|
||||
logger.error('Error getting or saving AI response:', aiError);
|
||||
logger.error('Error getting or saving AI response (RAG):', aiError);
|
||||
// Не прерываем основной ответ, но логируем ошибку
|
||||
}
|
||||
}
|
||||
@@ -702,14 +730,32 @@ router.post('/ai-draft', requireAuth, async (req, res) => {
|
||||
role: msg.sender_type === 'user' ? 'user' : 'assistant',
|
||||
content: msg.content
|
||||
}));
|
||||
// --- RAG draft ---
|
||||
let ragTableId = null;
|
||||
if (aiSettings && aiSettings.selected_rag_tables) {
|
||||
ragTableId = Array.isArray(aiSettings.selected_rag_tables)
|
||||
? aiSettings.selected_rag_tables[0]
|
||||
: aiSettings.selected_rag_tables;
|
||||
}
|
||||
let ragResult = null;
|
||||
if (ragTableId) {
|
||||
const { ragAnswer } = require('../services/ragService');
|
||||
logger.info(`[RAG] [DRAFT] Запуск поиска по RAG: tableId=${ragTableId}, draft prompt="${promptText}"`);
|
||||
ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: promptText });
|
||||
logger.info(`[RAG] [DRAFT] Результат поиска по RAG:`, ragResult);
|
||||
}
|
||||
const { generateLLMResponse } = require('../services/ragService');
|
||||
const detectedLanguage = language === 'auto' ? aiAssistant.detectLanguage(promptText) : language;
|
||||
const aiResponseContent = await aiAssistant.getResponse(
|
||||
promptText,
|
||||
detectedLanguage,
|
||||
const aiResponseContent = await generateLLMResponse({
|
||||
userQuestion: promptText,
|
||||
context: ragResult && ragResult.context ? ragResult.context : '',
|
||||
answer: ragResult && ragResult.answer ? ragResult.answer : '',
|
||||
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
||||
history,
|
||||
aiSettings ? aiSettings.system_prompt : '',
|
||||
rules ? rules.rules : null
|
||||
);
|
||||
model: aiSettings ? aiSettings.model : undefined,
|
||||
language: aiSettings && aiSettings.languages && aiSettings.languages.length > 0 ? aiSettings.languages[0] : 'ru',
|
||||
rules: rules ? rules.rules : null
|
||||
});
|
||||
res.json({ success: true, aiMessage: aiResponseContent });
|
||||
} catch (error) {
|
||||
logger.error('Error generating AI draft:', error);
|
||||
|
||||
@@ -10,6 +10,10 @@ const aiAssistant = require('../services/ai-assistant');
|
||||
const dns = require('node:dns').promises;
|
||||
const aiAssistantSettingsService = require('../services/aiAssistantSettingsService');
|
||||
const aiAssistantRulesService = require('../services/aiAssistantRulesService');
|
||||
const telegramBot = require('../services/telegramBot');
|
||||
const EmailBotService = require('../services/emailBot');
|
||||
const emailBotService = new EmailBotService();
|
||||
const dbSettingsService = require('../services/dbSettingsService');
|
||||
|
||||
// Логируем версию ethers для отладки
|
||||
logger.info(`Ethers version: ${ethers.version || 'unknown'}`);
|
||||
@@ -185,8 +189,8 @@ router.get('/ai-settings/:provider', requireAdmin, async (req, res, next) => {
|
||||
router.put('/ai-settings/:provider', requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const { provider } = req.params;
|
||||
const { api_key, base_url, selected_model } = req.body;
|
||||
const updated = await aiProviderSettingsService.upsertProviderSettings({ provider, api_key, base_url, selected_model });
|
||||
const { api_key, base_url, selected_model, embedding_model } = req.body;
|
||||
const updated = await aiProviderSettingsService.upsertProviderSettings({ provider, api_key, base_url, selected_model, embedding_model });
|
||||
res.json({ success: true, settings: updated });
|
||||
} catch (error) {
|
||||
logger.error('Ошибка при сохранении AI-настроек:', error);
|
||||
@@ -252,7 +256,25 @@ router.get('/ai-assistant', requireAdmin, async (req, res, next) => {
|
||||
|
||||
router.put('/ai-assistant', requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const updated = await aiAssistantSettingsService.upsertSettings({ ...req.body, updated_by: req.session.userId || null });
|
||||
let { selected_rag_tables, ...rest } = req.body;
|
||||
// Приведение к массиву чисел
|
||||
if (typeof selected_rag_tables === 'string') {
|
||||
try {
|
||||
selected_rag_tables = JSON.parse(selected_rag_tables);
|
||||
} catch {
|
||||
selected_rag_tables = [Number(selected_rag_tables)];
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(selected_rag_tables)) {
|
||||
selected_rag_tables = [Number(selected_rag_tables)];
|
||||
}
|
||||
selected_rag_tables = selected_rag_tables.map(Number);
|
||||
|
||||
const updated = await aiAssistantSettingsService.upsertSettings({
|
||||
...rest,
|
||||
selected_rag_tables,
|
||||
updated_by: req.session.userId || null
|
||||
});
|
||||
res.json({ success: true, settings: updated });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@@ -309,23 +331,101 @@ router.delete('/ai-assistant-rules/:id', requireAdmin, async (req, res, next) =>
|
||||
}
|
||||
});
|
||||
|
||||
// Получить все email_settings для выпадающего списка
|
||||
router.get('/email-settings', requireAdmin, async (req, res, next) => {
|
||||
// Получить текущие настройки Email (для страницы Email)
|
||||
router.get('/email-settings', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const { rows } = await require('../db').getQuery()('SELECT id, from_email FROM email_settings ORDER BY id');
|
||||
res.json({ success: true, items: rows });
|
||||
const settings = await emailBotService.getSettingsFromDb();
|
||||
res.json({ success: true, settings });
|
||||
} catch (error) {
|
||||
res.status(404).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Получить список всех email (для ассистента)
|
||||
router.get('/email-settings/list', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const emails = await emailBotService.getAllEmailSettings();
|
||||
res.json({ success: true, items: emails });
|
||||
} catch (error) {
|
||||
res.status(404).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Получить текущие настройки Telegram-бота (для страницы Telegram)
|
||||
router.get('/telegram-settings', requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const settings = await telegramBot.getTelegramSettings();
|
||||
res.json({ success: true, settings });
|
||||
} catch (error) {
|
||||
res.status(404).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Получить список всех Telegram-ботов (для ассистента)
|
||||
router.get('/telegram-settings/list', requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const bots = await telegramBot.getAllBots();
|
||||
res.json({ success: true, items: bots });
|
||||
} catch (error) {
|
||||
res.status(404).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Получение списка моделей для выбранного AI-провайдера
|
||||
router.get('/ai-provider-models', requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const provider = req.query.provider;
|
||||
if (!provider) return res.status(400).json({ error: 'provider is required' });
|
||||
const settings = await aiProviderSettingsService.getProviderSettings(provider);
|
||||
if (!settings) return res.status(404).json({ error: 'Provider not found' });
|
||||
const models = await aiProviderSettingsService.getProviderModels(provider, {
|
||||
api_key: settings.api_key,
|
||||
base_url: settings.base_url,
|
||||
});
|
||||
res.json({ models });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Получить все telegram_settings для выпадающего списка
|
||||
router.get('/telegram-settings', requireAdmin, async (req, res, next) => {
|
||||
// Получить настройки базы данных
|
||||
router.get('/db-settings', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const { rows } = await require('../db').getQuery()('SELECT id, bot_username FROM telegram_settings ORDER BY id');
|
||||
res.json({ success: true, items: rows });
|
||||
const settings = await dbSettingsService.getSettings();
|
||||
res.json({ success: true, settings });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
res.status(404).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Обновить настройки базы данных
|
||||
router.put('/db-settings', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const { db_host, db_port, db_name, db_user, db_password } = req.body;
|
||||
const updated = await dbSettingsService.upsertSettings({ db_host, db_port, db_name, db_user, db_password });
|
||||
res.json({ success: true, settings: updated });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Получить все LLM-модели
|
||||
router.get('/llm-models', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const models = await aiProviderSettingsService.getAllLLMModels();
|
||||
res.json({ success: true, models });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Получить все embedding-модели
|
||||
router.get('/embedding-models', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const models = await aiProviderSettingsService.getAllEmbeddingModels();
|
||||
res.json({ success: true, models });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -54,9 +54,7 @@ router.post('/:id/columns', async (req, res, next) => {
|
||||
try {
|
||||
const tableId = req.params.id;
|
||||
const { name, type, options, order, tagIds, purpose } = req.body;
|
||||
let finalOptions = options;
|
||||
// Собираем options
|
||||
finalOptions = finalOptions || {};
|
||||
let finalOptions = options || {};
|
||||
if (type === 'tags' && Array.isArray(tagIds)) {
|
||||
finalOptions.tagIds = tagIds;
|
||||
}
|
||||
|
||||
@@ -20,19 +20,21 @@ async function getSettings() {
|
||||
return {
|
||||
...settings,
|
||||
telegramBot,
|
||||
supportEmail
|
||||
supportEmail,
|
||||
embedding_model: settings.embedding_model
|
||||
};
|
||||
}
|
||||
|
||||
async function upsertSettings({ system_prompt, selected_rag_tables, languages, model, rules, updated_by, telegram_settings_id, email_settings_id, system_message }) {
|
||||
async function upsertSettings({ system_prompt, selected_rag_tables, languages, model, embedding_model, rules, updated_by, telegram_settings_id, email_settings_id, system_message }) {
|
||||
const { rows } = await db.getQuery()(
|
||||
`INSERT INTO ${TABLE} (id, system_prompt, selected_rag_tables, languages, model, rules, updated_at, updated_by, telegram_settings_id, email_settings_id, system_message)
|
||||
VALUES (1, $1, $2, $3, $4, $5, NOW(), $6, $7, $8, $9)
|
||||
`INSERT INTO ${TABLE} (id, system_prompt, selected_rag_tables, languages, model, embedding_model, rules, updated_at, updated_by, telegram_settings_id, email_settings_id, system_message)
|
||||
VALUES (1, $1, $2, $3, $4, $5, $6, NOW(), $7, $8, $9, $10)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
system_prompt = EXCLUDED.system_prompt,
|
||||
selected_rag_tables = EXCLUDED.selected_rag_tables,
|
||||
languages = EXCLUDED.languages,
|
||||
model = EXCLUDED.model,
|
||||
embedding_model = EXCLUDED.embedding_model,
|
||||
rules = EXCLUDED.rules,
|
||||
updated_at = NOW(),
|
||||
updated_by = EXCLUDED.updated_by,
|
||||
@@ -40,7 +42,7 @@ async function upsertSettings({ system_prompt, selected_rag_tables, languages, m
|
||||
email_settings_id = EXCLUDED.email_settings_id,
|
||||
system_message = EXCLUDED.system_message
|
||||
RETURNING *`,
|
||||
[system_prompt, selected_rag_tables, languages, model, rules, updated_by, telegram_settings_id, email_settings_id, system_message]
|
||||
[system_prompt, selected_rag_tables, languages, model, embedding_model, rules, updated_by, telegram_settings_id, email_settings_id, system_message]
|
||||
);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
@@ -12,17 +12,18 @@ async function getProviderSettings(provider) {
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function upsertProviderSettings({ provider, api_key, base_url, selected_model }) {
|
||||
async function upsertProviderSettings({ provider, api_key, base_url, selected_model, embedding_model }) {
|
||||
const { rows } = await db.getQuery()(
|
||||
`INSERT INTO ${TABLE} (provider, api_key, base_url, selected_model, updated_at)
|
||||
VALUES ($1, $2, $3, $4, NOW())
|
||||
`INSERT INTO ${TABLE} (provider, api_key, base_url, selected_model, embedding_model, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, NOW())
|
||||
ON CONFLICT (provider) DO UPDATE SET
|
||||
api_key = EXCLUDED.api_key,
|
||||
base_url = EXCLUDED.base_url,
|
||||
selected_model = EXCLUDED.selected_model,
|
||||
embedding_model = EXCLUDED.embedding_model,
|
||||
updated_at = NOW()
|
||||
RETURNING *`,
|
||||
[provider, api_key, base_url, selected_model]
|
||||
[provider, api_key, base_url, selected_model, embedding_model]
|
||||
);
|
||||
return rows[0];
|
||||
}
|
||||
@@ -97,10 +98,28 @@ async function verifyProviderKey(provider, { api_key, base_url } = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
async function getAllLLMModels() {
|
||||
const { rows } = await db.getQuery()(
|
||||
`SELECT provider, selected_model FROM ${TABLE} WHERE selected_model IS NOT NULL AND selected_model <> ''`
|
||||
);
|
||||
// Возвращаем массив объектов { id, provider }
|
||||
return rows.map(r => ({ id: r.selected_model, provider: r.provider }));
|
||||
}
|
||||
|
||||
async function getAllEmbeddingModels() {
|
||||
const { rows } = await db.getQuery()(
|
||||
`SELECT provider, embedding_model FROM ${TABLE} WHERE embedding_model IS NOT NULL AND embedding_model <> ''`
|
||||
);
|
||||
// Возвращаем массив объектов { id, provider }
|
||||
return rows.map(r => ({ id: r.embedding_model, provider: r.provider }));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getProviderSettings,
|
||||
upsertProviderSettings,
|
||||
deleteProviderSettings,
|
||||
getProviderModels,
|
||||
verifyProviderKey,
|
||||
getAllLLMModels,
|
||||
getAllEmbeddingModels,
|
||||
};
|
||||
27
backend/services/dbSettingsService.js
Normal file
27
backend/services/dbSettingsService.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const db = require('../db');
|
||||
|
||||
class DbSettingsService {
|
||||
async getSettings() {
|
||||
const { rows } = await db.getQuery()('SELECT * FROM db_settings WHERE id = 1');
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
async upsertSettings({ db_host, db_port, db_name, db_user, db_password }) {
|
||||
const { rows } = await db.getQuery()(
|
||||
`INSERT INTO db_settings (id, db_host, db_port, db_name, db_user, db_password, updated_at)
|
||||
VALUES (1, $1, $2, $3, $4, $5, NOW())
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
db_host = EXCLUDED.db_host,
|
||||
db_port = EXCLUDED.db_port,
|
||||
db_name = EXCLUDED.db_name,
|
||||
db_user = EXCLUDED.db_user,
|
||||
db_password = EXCLUDED.db_password,
|
||||
updated_at = NOW()
|
||||
RETURNING *`,
|
||||
[db_host, db_port, db_name, db_user, db_password]
|
||||
);
|
||||
return rows[0];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new DbSettingsService();
|
||||
@@ -309,6 +309,11 @@ class EmailBotService {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async getAllEmailSettings() {
|
||||
const { rows } = await db.getQuery()('SELECT id, from_email FROM email_settings ORDER BY id');
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[EmailBot] module.exports = EmailBotService');
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
const { OpenAIEmbeddings } = require('@langchain/openai');
|
||||
const { HNSWLib } = require('@langchain/community/vectorstores/hnswlib');
|
||||
const db = require('../db');
|
||||
const { ChatOllama } = require('@langchain/ollama');
|
||||
const { OllamaEmbeddings } = require('@langchain/ollama');
|
||||
const { getProviderSettings } = require('./aiProviderSettingsService');
|
||||
const { OpenAIEmbeddings } = require('@langchain/openai');
|
||||
|
||||
console.log('[RAG] ragService.js loaded');
|
||||
|
||||
async function getTableData(tableId) {
|
||||
const columns = (await db.getQuery()('SELECT * FROM user_columns WHERE table_id = $1', [tableId])).rows;
|
||||
console.log('RAG getTableData: columns:', columns);
|
||||
const rows = (await db.getQuery()('SELECT * FROM user_rows WHERE table_id = $1', [tableId])).rows;
|
||||
const cellValues = (await db.getQuery()('SELECT * FROM user_cell_values WHERE row_id IN (SELECT id FROM user_rows WHERE table_id = $1)', [tableId])).rows;
|
||||
|
||||
@@ -17,7 +22,7 @@ async function getTableData(tableId) {
|
||||
const priorityColId = getColId('priority');
|
||||
const dateColId = getColId('date');
|
||||
|
||||
return rows.map(row => {
|
||||
const data = rows.map(row => {
|
||||
const cells = cellValues.filter(cell => cell.row_id === row.id);
|
||||
return {
|
||||
id: row.id,
|
||||
@@ -30,35 +35,107 @@ async function getTableData(tableId) {
|
||||
date: cells.find(c => c.column_id === dateColId)?.value,
|
||||
};
|
||||
});
|
||||
const questions = data.map(row => row.question);
|
||||
console.log('RAG getTableData: questions:', questions);
|
||||
if (!questions.length) {
|
||||
console.warn('RAG getTableData: questions array is empty! Проверьте структуру колонок и наличие данных.');
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async function ragAnswer({ tableId, userQuestion, userTags = [], product = null }) {
|
||||
async function getEmbeddingsProvider(providerName = 'ollama') {
|
||||
const settings = await getProviderSettings(providerName);
|
||||
if (!settings) throw new Error('Embeddings provider settings not found');
|
||||
switch (providerName) {
|
||||
case 'openai':
|
||||
return new OpenAIEmbeddings({
|
||||
apiKey: settings.api_key,
|
||||
baseURL: settings.base_url,
|
||||
model: settings.selected_model || undefined,
|
||||
});
|
||||
case 'ollama': {
|
||||
// Fallback: если не задан base_url, пробуем env, host.docker.internal, localhost
|
||||
let baseUrl = settings.base_url;
|
||||
if (!baseUrl) {
|
||||
baseUrl = process.env.OLLAMA_BASE_URL;
|
||||
}
|
||||
if (!baseUrl) {
|
||||
// Если в Docker — используем host.docker.internal
|
||||
baseUrl = 'http://host.docker.internal:11434';
|
||||
}
|
||||
// Если всё равно нет — последний fallback
|
||||
if (!baseUrl) {
|
||||
baseUrl = 'http://localhost:11434';
|
||||
}
|
||||
return new OllamaEmbeddings({
|
||||
model: settings.embedding_model || process.env.OLLAMA_EMBED_MODEL || 'mxbai-embed-large',
|
||||
baseUrl,
|
||||
});
|
||||
}
|
||||
// case 'gemini':
|
||||
// return new GeminiEmbeddings({ apiKey: settings.api_key });
|
||||
// Добавьте другие провайдеры по аналогии
|
||||
default:
|
||||
throw new Error('Unknown embeddings provider: ' + providerName);
|
||||
}
|
||||
}
|
||||
|
||||
async function ragAnswer({ tableId, userQuestion, userTags = [], product = null, embeddingsProvider = 'ollama', threshold = 0.3 }) {
|
||||
console.log('[RAG] Используется провайдер эмбеддингов:', embeddingsProvider);
|
||||
const data = await getTableData(tableId);
|
||||
const questions = data.map(row => row.question);
|
||||
// Триммируем вопросы для чистоты сравнения
|
||||
const questions = data.map(row => row.question && typeof row.question === 'string' ? row.question.trim() : row.question);
|
||||
|
||||
// Получаем embeddings-инстанс динамически
|
||||
const embeddingsInstance = await getEmbeddingsProvider(embeddingsProvider);
|
||||
|
||||
// Получаем embedding для всех вопросов
|
||||
const embeddings = await new OpenAIEmbeddings().embedDocuments(questions);
|
||||
const embeddings = await embeddingsInstance.embedDocuments(questions);
|
||||
console.log('Questions embedding length:', embeddings[0]?.length, 'Total questions:', questions.length);
|
||||
|
||||
// Получаем embedding для вопроса пользователя (trim)
|
||||
const userQuestionTrimmed = userQuestion && typeof userQuestion === 'string' ? userQuestion.trim() : userQuestion;
|
||||
const [userEmbedding] = await embeddingsInstance.embedDocuments([userQuestionTrimmed]);
|
||||
console.log('User embedding length:', userEmbedding?.length, 'User question:', userQuestionTrimmed);
|
||||
|
||||
// Явно сравниваем embeddings (отладка)
|
||||
console.log('[RAG] Embedding сравнение:');
|
||||
embeddings.forEach((emb, idx) => {
|
||||
const dot = emb.reduce((sum, v, i) => sum + v * userEmbedding[i], 0);
|
||||
console.log(` [${idx}] dot-product: ${dot} | question: "${questions[idx]}"`);
|
||||
});
|
||||
|
||||
// Создаём массив метаданных для каждого вопроса
|
||||
const metadatas = data.map(row => ({
|
||||
id: row.id,
|
||||
answer: row.answer,
|
||||
userTags: row.userTags,
|
||||
context: row.context,
|
||||
product: row.product,
|
||||
priority: row.priority,
|
||||
date: row.date,
|
||||
}));
|
||||
|
||||
// Создаём векторное хранилище
|
||||
const vectorStore = await HNSWLib.fromTexts(questions, data, new OpenAIEmbeddings());
|
||||
|
||||
// Получаем embedding для вопроса пользователя
|
||||
const [userEmbedding] = await new OpenAIEmbeddings().embedDocuments([userQuestion]);
|
||||
const vectorStore = await HNSWLib.fromTexts(questions, metadatas, embeddingsInstance);
|
||||
|
||||
// Ищем наиболее похожие вопросы (top-3)
|
||||
const results = await vectorStore.similaritySearchVectorWithScore(userEmbedding, 3);
|
||||
console.log('[RAG] Результаты поиска по векторам (score):', results.map(([doc, score]) => ({ ...doc.metadata, score })));
|
||||
|
||||
// Фильтруем по тегам/продукту, если нужно
|
||||
let filtered = results.map(([row, score]) => ({ ...row, score }));
|
||||
let filtered = results.map(([doc, score]) => ({ ...doc.metadata, score }));
|
||||
if (userTags.length) {
|
||||
filtered = filtered.filter(row => row.userTags && userTags.some(tag => row.userTags.includes(tag)));
|
||||
}
|
||||
if (product) {
|
||||
filtered = filtered.filter(row => row.product === product);
|
||||
}
|
||||
console.log('[RAG] Отфильтрованные результаты:', filtered);
|
||||
|
||||
// Берём лучший результат
|
||||
const best = filtered[0];
|
||||
// Берём лучший результат с учётом порога
|
||||
const best = filtered.find(row => row.score >= threshold);
|
||||
console.log(`[RAG] Выбранный ответ (порог ${threshold}):`, best);
|
||||
|
||||
// Формируем ответ
|
||||
return {
|
||||
|
||||
@@ -424,9 +424,16 @@ function clearSettingsCache() {
|
||||
telegramSettingsCache = null;
|
||||
}
|
||||
|
||||
async function getAllBots() {
|
||||
const { rows } = await db.getQuery()('SELECT id, bot_username FROM telegram_settings ORDER BY id');
|
||||
return rows;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getTelegramSettings,
|
||||
getBot,
|
||||
stopBot,
|
||||
initTelegramAuth,
|
||||
clearSettingsCache,
|
||||
getAllBots,
|
||||
};
|
||||
|
||||
@@ -110,10 +110,10 @@
|
||||
dependencies:
|
||||
"@types/json-schema" "^7.0.15"
|
||||
|
||||
"@eslint/core@^0.15.0":
|
||||
version "0.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.15.0.tgz#8fc04709a7b9a179d9f7d93068fc000cb8c5603d"
|
||||
integrity sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==
|
||||
"@eslint/core@^0.15.1":
|
||||
version "0.15.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.15.1.tgz#d530d44209cbfe2f82ef86d6ba08760196dd3b60"
|
||||
integrity sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==
|
||||
dependencies:
|
||||
"@types/json-schema" "^7.0.15"
|
||||
|
||||
@@ -143,11 +143,11 @@
|
||||
integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==
|
||||
|
||||
"@eslint/plugin-kit@^0.3.1":
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.2.tgz#0cad96b134d23a653348e3342f485636b5ef4732"
|
||||
integrity sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==
|
||||
version "0.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz#32926b59bd407d58d817941e48b2a7049359b1fd"
|
||||
integrity sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==
|
||||
dependencies:
|
||||
"@eslint/core" "^0.15.0"
|
||||
"@eslint/core" "^0.15.1"
|
||||
levn "^0.4.1"
|
||||
|
||||
"@ethereumjs/rlp@^4.0.1":
|
||||
@@ -367,9 +367,9 @@
|
||||
integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==
|
||||
|
||||
"@google/genai@^1.0.1":
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@google/genai/-/genai-1.6.0.tgz#7a14d505faebe17957b272476debd574d2eae1e0"
|
||||
integrity sha512-0vn8wMGesjiEsHeFsl10T8+SFqLj7q+RSE6mml66sE+jwI7U9wW2LQ3qYtwUEaI+P8ZYeEYE5IpYmNLcRQUBPQ==
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@google/genai/-/genai-1.7.0.tgz#4af2fe86343e990eebfb4adcd0fa744cb0e6907e"
|
||||
integrity sha512-s/OZLkrIfBwc+SFFaZoKdEogkw4in0YRTGc4Q483jnfchNBWzrNe560eZEfGJHQRPn6YfzJgECCx0sqEOMWvYw==
|
||||
dependencies:
|
||||
google-auth-library "^9.14.2"
|
||||
ws "^8.18.0"
|
||||
@@ -6056,9 +6056,9 @@ simple-update-notifier@^2.0.0:
|
||||
semver "^7.5.3"
|
||||
|
||||
simple-wcswidth@^1.0.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/simple-wcswidth/-/simple-wcswidth-1.1.1.tgz#a96ff1b5cff660262ea33850e19a0e7249caed50"
|
||||
integrity sha512-R3q3/eoeNBp24CNTASEUrffXi0j9TwPIEvSStlvSrsFimM17sV5EHcMOc86j3K+UWZyLYvH0hRmYGCpCoaJ4vw==
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz#66722f37629d5203f9b47c5477b1225b85d6525b"
|
||||
integrity sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==
|
||||
|
||||
sisteransi@^1.0.5:
|
||||
version "1.0.5"
|
||||
@@ -7219,9 +7219,9 @@ zip-stream@^6.0.1:
|
||||
readable-stream "^4.0.0"
|
||||
|
||||
zod-to-json-schema@^3.22.3, zod-to-json-schema@^3.22.4:
|
||||
version "3.24.5"
|
||||
resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz#d1095440b147fb7c2093812a53c54df8d5df50a3"
|
||||
integrity sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==
|
||||
version "3.24.6"
|
||||
resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz#5920f020c4d2647edfbb954fa036082b92c9e12d"
|
||||
integrity sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==
|
||||
|
||||
zod@^3.22.4, zod@^3.25.32:
|
||||
version "3.25.67"
|
||||
|
||||
Reference in New Issue
Block a user