diff --git a/backend/db.js b/backend/db.js index ae44c93..dbba042 100644 --- a/backend/db.js +++ b/backend/db.js @@ -76,9 +76,16 @@ async function reinitPoolFromDbSettings() { } } -// При старте приложения — сразу пробуем инициализировать из db_settings -if (process.env.NODE_ENV !== 'migration') { - reinitPoolFromDbSettings(); +// При старте приложения — убираем автоматический вызов reinitPoolFromDbSettings +// if (process.env.NODE_ENV !== 'migration') { +// reinitPoolFromDbSettings(); +// } + +// Экспортируем функцию для явной инициализации пула +async function initDbPool() { + if (process.env.NODE_ENV !== 'migration') { + await reinitPoolFromDbSettings(); + } } // Функция для сохранения гостевого сообщения в базе данных @@ -99,4 +106,4 @@ async function saveGuestMessageToDatabase(message, language, guestId) { } // Экспортируем функции для работы с базой данных -module.exports = { query, getQuery, pool, getPool, setPoolChangeCallback }; +module.exports = { query, getQuery, pool, getPool, setPoolChangeCallback, initDbPool }; diff --git a/backend/routes/tables.js b/backend/routes/tables.js index 452ccd9..3bb0cb1 100644 --- a/backend/routes/tables.js +++ b/backend/routes/tables.js @@ -4,6 +4,7 @@ const express = require('express'); const router = express.Router(); const db = require('../db'); const { requireAuth } = require('../middleware/auth'); +const vectorSearchClient = require('../services/vectorSearchClient'); router.use((req, res, next) => { console.log('Tables router received:', req.method, req.originalUrl); @@ -79,6 +80,13 @@ router.post('/:id/rows', async (req, res, next) => { 'INSERT INTO user_rows (table_id) VALUES ($1) RETURNING *', [tableId] ); + // Получаем все строки и значения для upsert + const rows = (await db.getQuery()('SELECT r.id as row_id, c.value as text, c2.value as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.table_id = $1', [tableId])).rows; + const upsertRows = rows.filter(r => r.row_id && r.text).map(r => ({ row_id: r.row_id, text: r.text, metadata: { answer: r.answer } })); + console.log('[DEBUG][upsertRows]', upsertRows); + if (upsertRows.length > 0) { + await vectorSearchClient.upsert(tableId, upsertRows); + } res.json(result.rows[0]); } catch (err) { next(err); @@ -94,6 +102,24 @@ router.patch('/cell/:cellId', async (req, res, next) => { 'UPDATE user_cell_values SET value = $1, updated_at = NOW() WHERE id = $2 RETURNING *', [value, cellId] ); + // Получаем row_id и table_id + const row = (await db.getQuery()('SELECT row_id FROM user_cell_values WHERE id = $1', [cellId])).rows[0]; + if (row) { + const rowId = row.row_id; + const table = (await db.getQuery()('SELECT table_id FROM user_rows WHERE id = $1', [rowId])).rows[0]; + if (table) { + const tableId = table.table_id; + // Получаем всю строку для upsert + const rowData = (await db.getQuery()('SELECT r.id as row_id, c.value as text, c2.value as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.id = $1', [rowId])).rows[0]; + if (rowData) { + const upsertRows = [{ row_id: rowData.row_id, text: rowData.text, metadata: { answer: rowData.answer } }].filter(r => r.row_id && r.text); + console.log('[DEBUG][upsertRows]', upsertRows); + if (upsertRows.length > 0) { + await vectorSearchClient.upsert(tableId, upsertRows); + } + } + } + } res.json(result.rows[0]); } catch (err) { next(err); @@ -110,6 +136,20 @@ router.post('/cell', async (req, res, next) => { RETURNING *`, [row_id, column_id, value] ); + // Получаем table_id + const table = (await db.getQuery()('SELECT table_id FROM user_rows WHERE id = $1', [row_id])).rows[0]; + if (table) { + const tableId = table.table_id; + // Получаем всю строку для upsert + const rowData = (await db.getQuery()('SELECT r.id as row_id, c.value as text, c2.value as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.id = $1', [row_id])).rows[0]; + if (rowData) { + const upsertRows = [{ row_id: rowData.row_id, text: rowData.text, metadata: { answer: rowData.answer } }].filter(r => r.row_id && r.text); + console.log('[DEBUG][upsertRows]', upsertRows); + if (upsertRows.length > 0) { + await vectorSearchClient.upsert(tableId, upsertRows); + } + } + } res.json(result.rows[0]); } catch (err) { next(err); @@ -120,7 +160,19 @@ router.post('/cell', async (req, res, next) => { router.delete('/row/:rowId', async (req, res, next) => { try { const rowId = req.params.rowId; + // Получаем table_id + const table = (await db.getQuery()('SELECT table_id FROM user_rows WHERE id = $1', [rowId])).rows[0]; await db.getQuery()('DELETE FROM user_rows WHERE id = $1', [rowId]); + if (table) { + const tableId = table.table_id; + // Получаем все строки для rebuild + const rows = (await db.getQuery()('SELECT r.id as row_id, c.value as text, c2.value as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.table_id = $1', [tableId])).rows; + const rebuildRows = rows.filter(r => r.row_id && r.text).map(r => ({ row_id: r.row_id, text: r.text, metadata: { answer: r.answer } })); + console.log('[DEBUG][rebuildRows]', rebuildRows); + if (rebuildRows.length > 0) { + await vectorSearchClient.rebuild(tableId, rebuildRows); + } + } res.json({ success: true }); } catch (err) { next(err); @@ -206,6 +258,51 @@ router.patch('/:id', async (req, res, next) => { } }); +// Получить id колонок с purpose 'question' и 'answer' +async function getQuestionAnswerColumnIds(tableId) { + const { rows } = await db.getQuery()( + `SELECT id, options FROM user_columns WHERE table_id = $1`, [tableId] + ); + let questionCol = null, answerCol = null; + for (const col of rows) { + if (col.options && col.options.purpose === 'question') questionCol = col.id; + if (col.options && col.options.purpose === 'answer') answerCol = col.id; + } + return { questionCol, answerCol }; +} + +// Пересобрать векторный индекс для таблицы (только для админа) +router.post('/:id/rebuild-index', requireAuth, async (req, res, next) => { + try { + if (!req.session.isAdmin) { + return res.status(403).json({ error: 'Доступ только для администратора' }); + } + const tableId = req.params.id; + const { questionCol, answerCol } = await getQuestionAnswerColumnIds(tableId); + if (!questionCol || !answerCol) { + return res.status(400).json({ error: 'Не найдены колонки с вопросами и ответами' }); + } + const rows = (await db.getQuery()( + `SELECT r.id as row_id, c.value as text, c2.value as answer + FROM user_rows r + LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = $2 + LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = $3 + WHERE r.table_id = $1`, + [tableId, questionCol, answerCol] + )).rows; + const rebuildRows = rows.filter(r => r.row_id && r.text).map(r => ({ row_id: r.row_id, text: r.text, metadata: { answer: r.answer } })); + console.log('[DEBUG][rebuildRows]', rebuildRows); + if (rebuildRows.length > 0) { + await vectorSearchClient.rebuild(tableId, rebuildRows); + res.json({ success: true, count: rebuildRows.length }); + } else { + res.status(400).json({ error: 'Нет валидных строк для индексации' }); + } + } catch (err) { + next(err); + } +}); + // DELETE: удалить таблицу и каскадно все связанные строки/столбцы/ячейки (доступно всем) router.delete('/:id', requireAuth, async (req, res, next) => { const dbModule = require('../db'); diff --git a/backend/server.js b/backend/server.js index 0aca925..413f7cc 100644 --- a/backend/server.js +++ b/backend/server.js @@ -5,6 +5,7 @@ const { initWSS } = require('./wsHub'); const logger = require('./utils/logger'); const { getBot } = require('./services/telegramBot'); const EmailBotService = require('./services/emailBot'); +const { initDbPool } = require('./db'); const PORT = process.env.PORT || 8000; @@ -44,10 +45,15 @@ async function initServices() { const server = http.createServer(app); initWSS(server); +async function startServer() { + await initDbPool(); // Дождаться пересоздания пула! + await initServices(); // Только теперь запускать сервисы + console.log(`Server is running on port ${PORT}`); +} + server.listen(PORT, async () => { try { - await initServices(); - console.log(`Server is running on port ${PORT}`); + await startServer(); } catch (error) { console.error('Error starting server:', error); process.exit(1); diff --git a/backend/services/telegramBot.js b/backend/services/telegramBot.js index b404d4e..9820fbf 100644 --- a/backend/services/telegramBot.js +++ b/backend/services/telegramBot.js @@ -8,6 +8,8 @@ const identityService = require('./identity-service'); const aiAssistant = require('./ai-assistant'); const { checkAdminRole } = require('./admin-role'); const { broadcastContactsUpdate } = require('../wsHub'); +const aiAssistantSettingsService = require('./aiAssistantSettingsService'); +const { ragAnswer, generateLLMResponse } = require('./ragService'); let botInstance = null; let telegramSettingsCache = null; @@ -335,8 +337,34 @@ async function getBot() { ] ); - // 3. Получить ответ от ИИ - const aiResponse = await aiAssistant.getResponse(content, 'auto'); + // 3. Получить ответ от ИИ (RAG + LLM) + const aiSettings = await aiAssistantSettingsService.getSettings(); + 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 aiResponse; + if (ragTableId) { + // Сначала ищем ответ через RAG + const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: content }); + if (ragResult && ragResult.answer) { + aiResponse = ragResult.answer; + } else { + aiResponse = await generateLLMResponse({ + userQuestion: content, + context: ragResult && ragResult.context ? ragResult.context : '', + answer: ragResult && ragResult.answer ? ragResult.answer : '', + systemPrompt: aiSettings ? aiSettings.system_prompt : '', + history: null, + model: aiSettings ? aiSettings.model : undefined, + language: aiSettings && aiSettings.languages && aiSettings.languages.length > 0 ? aiSettings.languages[0] : 'ru' + }); + } + } else { + aiResponse = await aiAssistant.getResponse(content, 'auto'); + } // 4. Сохранить ответ в БД с conversation_id await db.getQuery()( `INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at) diff --git a/backend/services/vectorSearchClient.js b/backend/services/vectorSearchClient.js index 38ac537..a31c2fd 100644 --- a/backend/services/vectorSearchClient.js +++ b/backend/services/vectorSearchClient.js @@ -1,57 +1,89 @@ const axios = require('axios'); +const logger = require('../utils/logger'); const VECTOR_SEARCH_URL = process.env.VECTOR_SEARCH_URL || 'http://vector-search:8001'; async function upsert(tableId, rows) { - const res = await axios.post(`${VECTOR_SEARCH_URL}/upsert`, { - table_id: String(tableId), - rows: rows.map(r => ({ - row_id: String(r.row_id), - text: r.text, - metadata: r.metadata || {} - })) - }); - return res.data; + logger.info(`[VectorSearch] upsert: tableId=${tableId}, rows=${rows.length}`); + try { + const res = await axios.post(`${VECTOR_SEARCH_URL}/upsert`, { + table_id: String(tableId), + rows: rows.map(r => ({ + row_id: String(r.row_id), + text: r.text, + metadata: r.metadata || {} + })) + }); + logger.info(`[VectorSearch] upsert result:`, res.data); + return res.data; + } catch (error) { + logger.error(`[VectorSearch] upsert error:`, error.message); + throw error; + } } async function search(tableId, query, topK = 3) { - const res = await axios.post(`${VECTOR_SEARCH_URL}/search`, { - table_id: String(tableId), - query, - top_k: topK - }); - return res.data.results; + logger.info(`[VectorSearch] search: tableId=${tableId}, query="${query}", topK=${topK}`); + try { + const res = await axios.post(`${VECTOR_SEARCH_URL}/search`, { + table_id: String(tableId), + query, + top_k: topK + }); + logger.info(`[VectorSearch] search result:`, res.data.results); + return res.data.results; + } catch (error) { + logger.error(`[VectorSearch] search error:`, error.message); + throw error; + } } async function remove(tableId, rowIds) { - const res = await axios.post(`${VECTOR_SEARCH_URL}/delete`, { - table_id: String(tableId), - row_ids: rowIds.map(id => String(id)) - }); - return res.data; + logger.info(`[VectorSearch] remove: tableId=${tableId}, rowIds=${rowIds}`); + try { + const res = await axios.post(`${VECTOR_SEARCH_URL}/remove`, { + table_id: String(tableId), + row_ids: rowIds.map(String) + }); + logger.info(`[VectorSearch] remove result:`, res.data); + return res.data; + } catch (error) { + logger.error(`[VectorSearch] remove error:`, error.message); + throw error; + } } async function rebuild(tableId, rows) { - const res = await axios.post(`${VECTOR_SEARCH_URL}/rebuild`, { - table_id: String(tableId), - rows: rows.map(r => ({ - row_id: String(r.row_id), - text: r.text, - metadata: r.metadata || {} - })) - }); - return res.data; + logger.info(`[VectorSearch] rebuild: tableId=${tableId}, rows=${rows.length}`); + try { + const res = await axios.post(`${VECTOR_SEARCH_URL}/rebuild`, { + table_id: String(tableId), + rows: rows.map(r => ({ + row_id: String(r.row_id), + text: r.text, + metadata: r.metadata || {} + })) + }); + logger.info(`[VectorSearch] rebuild result:`, res.data); + return res.data; + } catch (error) { + logger.error(`[VectorSearch] rebuild error:`, error.message); + throw error; + } } async function health() { + logger.info(`[VectorSearch] health check`); try { const res = await axios.get(`${VECTOR_SEARCH_URL}/health`, { timeout: 5000 }); + logger.info(`[VectorSearch] health result:`, res.data); return { status: 'ok', url: VECTOR_SEARCH_URL, response: res.data }; } catch (error) { + logger.error(`[VectorSearch] health error:`, error.message); return { status: 'error', url: VECTOR_SEARCH_URL, diff --git a/frontend/src/components/tables/UserTableView.vue b/frontend/src/components/tables/UserTableView.vue index eb62dbc..cf61f8a 100644 --- a/frontend/src/components/tables/UserTableView.vue +++ b/frontend/src/components/tables/UserTableView.vue @@ -2,6 +2,12 @@