diff --git a/backend/db/migrations/038_add_embedding_model_to_ai_providers_settings.sql b/backend/db/migrations/038_add_embedding_model_to_ai_providers_settings.sql new file mode 100644 index 0000000..9bf47a2 --- /dev/null +++ b/backend/db/migrations/038_add_embedding_model_to_ai_providers_settings.sql @@ -0,0 +1 @@ +ALTER TABLE ai_providers_settings ADD COLUMN embedding_model VARCHAR(128); \ No newline at end of file diff --git a/backend/db/migrations/040_add_embedding_model_to_ai_assistant_settings.sql b/backend/db/migrations/040_add_embedding_model_to_ai_assistant_settings.sql new file mode 100644 index 0000000..d05af78 --- /dev/null +++ b/backend/db/migrations/040_add_embedding_model_to_ai_assistant_settings.sql @@ -0,0 +1 @@ +ALTER TABLE ai_assistant_settings ADD COLUMN embedding_model VARCHAR(128); \ No newline at end of file diff --git a/backend/routes/chat.js b/backend/routes/chat.js index fa95f42..c4e2de0 100644 --- a/backend/routes/chat.js +++ b/backend/routes/chat.js @@ -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); diff --git a/backend/routes/settings.js b/backend/routes/settings.js index b9d4ce3..e8dabc7 100644 --- a/backend/routes/settings.js +++ b/backend/routes/settings.js @@ -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 }); } }); diff --git a/backend/routes/tables.js b/backend/routes/tables.js index 452e309..452ccd9 100644 --- a/backend/routes/tables.js +++ b/backend/routes/tables.js @@ -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; } diff --git a/backend/services/aiAssistantSettingsService.js b/backend/services/aiAssistantSettingsService.js index cf33d82..0501e88 100644 --- a/backend/services/aiAssistantSettingsService.js +++ b/backend/services/aiAssistantSettingsService.js @@ -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]; } diff --git a/backend/services/aiProviderSettingsService.js b/backend/services/aiProviderSettingsService.js index a1d0df4..05fcb1b 100644 --- a/backend/services/aiProviderSettingsService.js +++ b/backend/services/aiProviderSettingsService.js @@ -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, }; \ No newline at end of file diff --git a/backend/services/dbSettingsService.js b/backend/services/dbSettingsService.js new file mode 100644 index 0000000..085a50f --- /dev/null +++ b/backend/services/dbSettingsService.js @@ -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(); \ No newline at end of file diff --git a/backend/services/emailBot.js b/backend/services/emailBot.js index ce3e209..13782de 100644 --- a/backend/services/emailBot.js +++ b/backend/services/emailBot.js @@ -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'); diff --git a/backend/services/ragService.js b/backend/services/ragService.js index 0efa196..f65fda2 100644 --- a/backend/services/ragService.js +++ b/backend/services/ragService.js @@ -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 { diff --git a/backend/services/telegramBot.js b/backend/services/telegramBot.js index 0ca7723..b404d4e 100644 --- a/backend/services/telegramBot.js +++ b/backend/services/telegramBot.js @@ -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, }; diff --git a/backend/yarn.lock b/backend/yarn.lock index ee9f01d..76d8d44 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -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" diff --git a/docker-compose.yml b/docker-compose.yml index 3e0bf22..ba8dc06 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,7 +43,6 @@ services: condition: service_started volumes: - ./backend:/app - - backend_node_modules:/app/node_modules - ./frontend/dist:/app/frontend_dist:ro environment: - NODE_ENV=${NODE_ENV:-development} @@ -60,6 +59,8 @@ services: - FRONTEND_URL=http://localhost:5173 ports: - "8000:8000" + extra_hosts: + - "host.docker.internal:host-gateway" # command: sh -c "yarn run dev" # Временно комментируем эту строку # command: nodemon server.js # Запускаем через nodemon diff --git a/frontend/src/components/tables/UserTableView.vue b/frontend/src/components/tables/UserTableView.vue index b7780a5..eb62dbc 100644 --- a/frontend/src/components/tables/UserTableView.vue +++ b/frontend/src/components/tables/UserTableView.vue @@ -129,11 +129,16 @@ function closeAddColModal() { async function handleAddColumn() { if (!newColName.value) return; const data = { name: newColName.value, type: newColType.value }; + const options = {}; if (newColType.value === 'tags') { data.tagIds = selectedTagIds.value; + options.tagIds = selectedTagIds.value; } if (newColPurpose.value) { - data.purpose = newColPurpose.value; + options.purpose = newColPurpose.value; + } + if (Object.keys(options).length > 0) { + data.options = options; } await tablesService.addColumn(props.tableId, data); closeAddColModal(); diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 1803db8..8f1f0a4 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -27,6 +27,11 @@ const routes = [ component: () => import('../views/SettingsView.vue'), // Добавляем дочерние маршруты children: [ + { + path: '', + name: 'settings-index', + component: () => import('@/views/settings/SettingsIndexView.vue'), + }, { path: 'ai', name: 'settings-ai', @@ -50,21 +55,35 @@ const routes = [ { path: 'telegram', name: 'settings-telegram', - component: () => import('../views/settings/TelegramSettingsView.vue'), + component: () => import('../views/settings/AI/TelegramSettingsView.vue'), }, { path: 'email', name: 'settings-email', - component: () => import('../views/settings/EmailSettingsView.vue'), + component: () => import('../views/settings/AI/EmailSettingsView.vue'), }, - // Опционально: перенаправление со /settings на первую подстраницу - { - path: '', - name: 'settings-index', - redirect: { name: 'settings-ai' } - } ] }, + { + path: '/settings/ai/openai', + name: 'openai-settings', + component: () => import('@/views/settings/AI/OpenAISettingsView.vue'), + }, + { + path: '/settings/ai/ollama', + name: 'ollama-settings', + component: () => import('@/views/settings/AI/OllamaSettingsView.vue'), + }, + { + path: '/settings/ai/database', + name: 'database-settings', + component: () => import('@/views/settings/AI/DatabaseSettingsView.vue'), + }, + { + path: '/settings/ai/assistant', + name: 'ai-assistant-settings', + component: () => import('@/views/settings/AI/AiAssistantSettings.vue'), + }, { path: '/tables', name: 'tables-list', @@ -120,6 +139,16 @@ const routes = [ name: 'dle-management', component: () => import('../views/DleManagementView.vue') }, + { + path: '/settings/ai/telegram', + name: 'telegram-settings', + component: () => import('@/views/settings/AI/TelegramSettingsView.vue'), + }, + { + path: '/settings/ai/email', + name: 'email-settings', + component: () => import('@/views/settings/AI/EmailSettingsView.vue'), + }, ]; const router = createRouter({ diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index dac11e8..aea2ed6 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -8,23 +8,12 @@ >

Настройки

-
Загрузка данных пользователя...

Для доступа к настройкам необходимо .

-
-
- ИИ - Блокчейн - Безопасность - Интерфейс -
-
- -
@@ -32,7 +21,7 @@ + + \ No newline at end of file diff --git a/frontend/src/views/settings/AI/DatabaseSettingsView.vue b/frontend/src/views/settings/AI/DatabaseSettingsView.vue new file mode 100644 index 0000000..8c46c15 --- /dev/null +++ b/frontend/src/views/settings/AI/DatabaseSettingsView.vue @@ -0,0 +1,210 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/settings/AI/EmailSettingsView.vue b/frontend/src/views/settings/AI/EmailSettingsView.vue new file mode 100644 index 0000000..0e99c2f --- /dev/null +++ b/frontend/src/views/settings/AI/EmailSettingsView.vue @@ -0,0 +1,225 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/settings/AI/OllamaSettingsView.vue b/frontend/src/views/settings/AI/OllamaSettingsView.vue new file mode 100644 index 0000000..4941057 --- /dev/null +++ b/frontend/src/views/settings/AI/OllamaSettingsView.vue @@ -0,0 +1,65 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/settings/AI/OpenAISettingsView.vue b/frontend/src/views/settings/AI/OpenAISettingsView.vue new file mode 100644 index 0000000..befc01b --- /dev/null +++ b/frontend/src/views/settings/AI/OpenAISettingsView.vue @@ -0,0 +1,65 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/settings/AI/TelegramSettingsView.vue b/frontend/src/views/settings/AI/TelegramSettingsView.vue new file mode 100644 index 0000000..f452864 --- /dev/null +++ b/frontend/src/views/settings/AI/TelegramSettingsView.vue @@ -0,0 +1,186 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/settings/AIProviderSettings.vue b/frontend/src/views/settings/AIProviderSettings.vue index 8823c1f..ee534e4 100644 --- a/frontend/src/views/settings/AIProviderSettings.vue +++ b/frontend/src/views/settings/AIProviderSettings.vue @@ -15,10 +15,18 @@
- + +
+
+ +
@@ -49,7 +57,9 @@ const props = defineProps({ const apiKey = ref(''); const baseUrl = ref(''); const selectedModel = ref(''); +const selectedEmbeddingModel = ref(''); const models = ref([]); +const embeddingModels = ref([]); const hasSettings = ref(false); const verifying = ref(false); const verifyStatus = ref(null); @@ -65,9 +75,11 @@ async function loadSettings() { apiKey.value = data.settings.api_key || ''; baseUrl.value = data.settings.base_url || ''; selectedModel.value = data.settings.selected_model || ''; + selectedEmbeddingModel.value = data.settings.embedding_model || ''; hasSettings.value = true; if (apiKey.value || props.provider === 'ollama') { await loadModels(); + await loadEmbeddingModels(); } } else { hasSettings.value = false; @@ -82,13 +94,30 @@ async function loadModels() { const { data } = await axios.get(`/api/settings/ai-settings/${props.provider}/models`); models.value = data.models || []; if (!selectedModel.value && models.value.length) { - selectedModel.value = models.value[0].id || models.value[0]; + const first = models.value[0]; + selectedModel.value = first.id || first.name || first; } } catch (e) { models.value = []; } } +async function loadEmbeddingModels() { + try { + const { data } = await axios.get(`/api/settings/ai-settings/${props.provider}/models`); + embeddingModels.value = (data.models || []).filter(m => { + const name = m.id || m.name || m; + return name && name.toLowerCase().includes('embed'); + }); + if (!selectedEmbeddingModel.value && embeddingModels.value.length) { + const first = embeddingModels.value[0]; + selectedEmbeddingModel.value = first.id || first.name || first; + } + } catch (e) { + embeddingModels.value = []; + } +} + async function onVerify() { verifying.value = true; verifyStatus.value = null; @@ -119,6 +148,7 @@ async function onSave() { api_key: apiKey.value, base_url: baseUrl.value, selected_model: selectedModel.value, + embedding_model: selectedEmbeddingModel.value, }); saveStatus.value = true; hasSettings.value = true; @@ -135,7 +165,9 @@ async function onDelete() { apiKey.value = ''; baseUrl.value = ''; selectedModel.value = ''; + selectedEmbeddingModel.value = ''; models.value = []; + embeddingModels.value = []; hasSettings.value = false; } @@ -151,8 +183,9 @@ watch([apiKey, baseUrl], () => { \ No newline at end of file diff --git a/frontend/src/views/settings/AiSettingsView.vue b/frontend/src/views/settings/AiSettingsView.vue index e75d64a..7b25a1e 100644 --- a/frontend/src/views/settings/AiSettingsView.vue +++ b/frontend/src/views/settings/AiSettingsView.vue @@ -1,46 +1,37 @@ \ No newline at end of file diff --git a/frontend/src/views/settings/DatabaseSettingsView.vue b/frontend/src/views/settings/DatabaseSettingsView.vue deleted file mode 100644 index 4ecd60c..0000000 --- a/frontend/src/views/settings/DatabaseSettingsView.vue +++ /dev/null @@ -1,171 +0,0 @@ - - - - - \ No newline at end of file diff --git a/frontend/src/views/settings/EmailSettingsView.vue b/frontend/src/views/settings/EmailSettingsView.vue deleted file mode 100644 index cd9d90b..0000000 --- a/frontend/src/views/settings/EmailSettingsView.vue +++ /dev/null @@ -1,186 +0,0 @@ - - - - - \ No newline at end of file diff --git a/frontend/src/views/settings/InterfaceSettingsView.vue b/frontend/src/views/settings/InterfaceSettingsView.vue index 5b8e667..b3983b5 100644 --- a/frontend/src/views/settings/InterfaceSettingsView.vue +++ b/frontend/src/views/settings/InterfaceSettingsView.vue @@ -1,5 +1,6 @@