diff --git a/RAG_TASKS.md b/RAG_TASKS.md new file mode 100644 index 0000000..2c7624f --- /dev/null +++ b/RAG_TASKS.md @@ -0,0 +1,129 @@ +# Внедрение RAG-ассистента: поэтапный план + +--- + +## Особенности проекта: разнообразие клиентов, каналов и данных + +- **Клиенты:** + - Различные сегменты: B2B, B2C, VIP, оптовые и розничные покупатели, корпоративные клиенты, частные лица и др. + - Различные сценарии взаимодействия (покупка, поддержка, консультация, возврат и т.д.). + +- **Каналы коммуникации:** + - Веб-чат + - Email + - Telegram/мессенджеры + - Возможна интеграция с другими каналами (WhatsApp, телефон и др.) + +- **Типы данных:** + - Текстовые сообщения + - Аудио, видео, изображения (мультимодальные данные) + - Вложения (документы, сканы, фото товаров и т.д.) + +- **Языки:** + - Русский + - Английский + - Испанский + - Китайский + - Возможность расширения на другие языки + +- **Товары и услуги:** + - Широкий ассортимент товаров (разные категории, бренды, характеристики) + - Различные услуги (консультации, сервис, доставка, гарантия, возврат и др.) + - Возможность кросс-продаж и рекомендаций + +- **Требования к RAG:** + - Гибкая фильтрация знаний по сегменту клиента, языку, категории товара/услуги, каналу обращения + - Поддержка мультиязычности и мультимодальности + - Масштабируемость для добавления новых ассистентов, сегментов, каналов и языков + +--- + +## Этап 1. Проектирование и подготовка инфраструктуры +1. **Проектирование схемы хранения знаний (RAG):** + - Описать структуру таблицы `knowledge_documents` (миграция). + - Определить поля: id, content, language, type (текст/медиа), метаданные, дата, автор и т.д. +2. **Подготовка backend:** + - Создать миграцию и модель для `knowledge_documents`. + - Подготовить базовые CRUD-эндпоинты для работы с базой знаний. + +--- + +## Этап 2. Интеграция векторного поиска (RAG) +1. **Реализация векторного хранилища:** + - Реализовать методы инициализации и поиска (`initVectorStore`, `findSimilarDocuments`) в `ai-assistant.js`. + - Настроить хранение эмбеддингов для документов. +2. **API для поиска знаний:** + - Добавить эндпоинт для поиска релевантных знаний по запросу пользователя. + +--- + +## Этап 3. Интеграция RAG в pipeline ассистента +1. **Модификация логики ответа ассистента:** + - При получении сообщения пользователя — искать релевантные знания и включать их в prompt LLM. + - Обеспечить мультиязычность поиска и генерации ответа. +2. **Логирование и трассировка:** + - Сохранять, какие знания были использованы для ответа. + +--- + +## Этап 4. Интерфейс для админа +1. **UI для управления знаниями:** + - Добавить на фронте раздел для просмотра, добавления, редактирования и удаления знаний. +2. **UI для модерации ответов ассистента:** + - Кнопки "Редактировать", "Отправить", "Добавить в RAG" для сообщений и ответов. + - Возможность быстро добавить сообщение пользователя или ответ ассистента в базу знаний. + +--- + +## Этап 5. Поддержка мультимодальности и мультиязычности +1. **Обработка вложений (аудио, видео, картинки):** + - Решить, как хранить и индексировать такие данные (например, хранить ссылки и метаданные, а не сами файлы). +2. **Мультиязычный поиск и генерация:** + - Проверить корректность работы эмбеддингов и LLM для разных языков. + +--- + +## Этап 6. Тестирование и оптимизация +1. **Покрытие тестами ключевых сценариев (unit, интеграционные).** +2. **Оптимизация скорости поиска и генерации.** +3. **Документация для команды.** + +--- + +## Бизнес-логика управления знаниями и тегами для RAG-ассистента + +### 1. Гибкая система тегов и связей с пользователями +- Пользователь может создавать собственные таблицы тегов (например, "покупатель", "поставщик", "VIP-клиент" и т.д.). +- В таблице тегов должна быть возможность добавлять ссылки (relation) на пользователей из таблицы `users`. +- Для одного тега может быть привязано несколько пользователей (мультисвязь). +- Для одного пользователя может быть несколько тегов. + +### 2. Управление знаниями (FAQ, инструкции, ответы) +- Пользователь может создавать таблицы с вопросами и ответами (например, FAQ для определённой группы клиентов). +- Каждая запись (вопрос-ответ) может быть связана с определённым тегом или группой тегов. +- Возможна фильтрация и поиск знаний по тегам, языку, типу клиента и другим параметрам. + +### 3. Использование тегов и знаний в RAG-ассистенте +- При обработке запроса пользователя RAG-ассистент определяет его теги (по связям в таблице тегов). +- Для генерации ответа ассистент использует только те знания (вопросы/ответы), которые соответствуют тегам пользователя. +- Администратор может добавлять новые теги, связывать их с пользователями, а также создавать и редактировать знания для каждой группы. + +### 4. UI/UX требования +- В интерфейсе создания/редактирования пользовательских таблиц должен быть доступен тип столбца "relation" (связь с users). +- Для ячеек типа "relation" реализовать выпадающий список с поиском по пользователям. +- Для таблиц знаний — возможность выбора одного или нескольких тегов для каждой записи. + +**Пример структуры:** +- Таблица `user_tags`: id, name, [user_id (relation, мультисвязь)] +- Таблица `faq`: id, question, answer, [tag_id (relation, мультисвязь)] + +**Применение:** +- RAG-ассистент использует связи между пользователями, тегами и знаниями для персонализации ответов и поиска релевантной информации. + +### 5. Безопасность и контроль +- Только администратор может создавать и редактировать системные теги и знания. +- Обычные пользователи могут видеть только свои теги и связанные с ними знания. + +--- + +**Этот документ будет дополняться по мере реализации каждого этапа.** \ No newline at end of file diff --git a/backend/db/migrations/028_create_dynamic_tables.sql b/backend/db/migrations/028_create_dynamic_tables.sql new file mode 100644 index 0000000..ed33cbd --- /dev/null +++ b/backend/db/migrations/028_create_dynamic_tables.sql @@ -0,0 +1,36 @@ +-- Миграция для динамических пользовательских таблиц (аналог Notion) + +CREATE TABLE IF NOT EXISTS user_tables ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS user_columns ( + id SERIAL PRIMARY KEY, + table_id INTEGER NOT NULL REFERENCES user_tables(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + type VARCHAR(50) NOT NULL, -- text, number, select, multiselect, date, etc. + options JSONB DEFAULT NULL, -- для select/multiselect + "order" INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS user_rows ( + id SERIAL PRIMARY KEY, + table_id INTEGER NOT NULL REFERENCES user_tables(id) ON DELETE CASCADE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS user_cell_values ( + id SERIAL PRIMARY KEY, + row_id INTEGER NOT NULL REFERENCES user_rows(id) ON DELETE CASCADE, + column_id INTEGER NOT NULL REFERENCES user_columns(id) ON DELETE CASCADE, + value TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(row_id, column_id) +); \ No newline at end of file diff --git a/backend/middleware/errorHandler.js b/backend/middleware/errorHandler.js index 0644c5d..fbc8b00 100644 --- a/backend/middleware/errorHandler.js +++ b/backend/middleware/errorHandler.js @@ -5,13 +5,12 @@ const { ERROR_CODES } = require('../utils/constants'); /** * Middleware для обработки ошибок */ -// eslint-disable-next-line no-unused-vars -const errorHandler = (err, req, res, /* next */) => { +const errorHandler = (err, req, res, next) => { + console.log('errorHandler called, arguments:', arguments); + console.log('typeof res:', typeof res, 'isFunction:', typeof res === 'function'); console.error('errorHandler: err =', err); console.error('errorHandler: typeof err =', typeof err); console.error('errorHandler: stack =', err && err.stack); - console.log('errorHandler called, typeof res:', typeof res, 'res:', res); - console.log('typeof res:', typeof res, 'isFunction:', typeof res === 'function'); // Логируем ошибку logger.error(`Error: ${err.message}`, { stack: err.stack, diff --git a/backend/nodemon.json b/backend/nodemon.json index e493199..29bab23 100644 --- a/backend/nodemon.json +++ b/backend/nodemon.json @@ -5,6 +5,5 @@ "env": { "NODE_ENV": "development" }, - "ext": "js,json,env", - "exec": "node server.js" + "ext": "js,json,env" } diff --git a/backend/routes/tables.js b/backend/routes/tables.js new file mode 100644 index 0000000..e207244 --- /dev/null +++ b/backend/routes/tables.js @@ -0,0 +1,220 @@ +const express = require('express'); +const router = express.Router(); +const db = require('../db'); +const { requireAuth } = require('../middleware/auth'); + +router.use((req, res, next) => { + console.log('Tables router received:', req.method, req.originalUrl); + next(); +}); + +// Получить список всех таблиц (доступно всем) +router.get('/', async (req, res, next) => { + try { + const result = await db.getQuery()('SELECT * FROM user_tables ORDER BY id'); + res.json(result.rows); + } catch (err) { + next(err); + } +}); + +// Создать новую таблицу (доступно всем) +router.post('/', async (req, res, next) => { + try { + const { name, description } = req.body; + const result = await db.getQuery()( + 'INSERT INTO user_tables (name, description) VALUES ($1, $2) RETURNING *', + [name, description || null] + ); + res.json(result.rows[0]); + } catch (err) { + next(err); + } +}); + +// Получить структуру и данные таблицы (доступно всем) +router.get('/:id', async (req, res, next) => { + try { + const tableId = req.params.id; + const columns = (await db.getQuery()('SELECT * FROM user_columns WHERE table_id = $1 ORDER BY "order" ASC, id ASC', [tableId])).rows; + const rows = (await db.getQuery()('SELECT * FROM user_rows WHERE table_id = $1 ORDER BY id', [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; + res.json({ columns, rows, cellValues }); + } catch (err) { + next(err); + } +}); + +// Добавить столбец (доступно всем) +router.post('/:id/columns', async (req, res, next) => { + try { + const tableId = req.params.id; + const { name, type, options, order } = req.body; + const result = await db.getQuery()( + 'INSERT INTO user_columns (table_id, name, type, options, "order") VALUES ($1, $2, $3, $4, $5) RETURNING *', + [tableId, name, type, options ? JSON.stringify(options) : null, order || 0] + ); + res.json(result.rows[0]); + } catch (err) { + next(err); + } +}); + +// Добавить строку (доступно всем) +router.post('/:id/rows', async (req, res, next) => { + try { + const tableId = req.params.id; + const result = await db.getQuery()( + 'INSERT INTO user_rows (table_id) VALUES ($1) RETURNING *', + [tableId] + ); + res.json(result.rows[0]); + } catch (err) { + next(err); + } +}); + +// Изменить значение ячейки (доступно всем) +router.patch('/cell/:cellId', async (req, res, next) => { + try { + const cellId = req.params.cellId; + const { value } = req.body; + const result = await db.getQuery()( + 'UPDATE user_cell_values SET value = $1, updated_at = NOW() WHERE id = $2 RETURNING *', + [value, cellId] + ); + res.json(result.rows[0]); + } catch (err) { + next(err); + } +}); + +// Создать/обновить значение ячейки (upsert) (доступно всем) +router.post('/cell', async (req, res, next) => { + try { + const { row_id, column_id, value } = req.body; + const result = await db.getQuery()( + `INSERT INTO user_cell_values (row_id, column_id, value) VALUES ($1, $2, $3) + ON CONFLICT (row_id, column_id) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW() + RETURNING *`, + [row_id, column_id, value] + ); + res.json(result.rows[0]); + } catch (err) { + next(err); + } +}); + +// Удалить строку (доступно всем) +router.delete('/row/:rowId', async (req, res, next) => { + try { + const rowId = req.params.rowId; + await db.getQuery()('DELETE FROM user_rows WHERE id = $1', [rowId]); + res.json({ success: true }); + } catch (err) { + next(err); + } +}); + +// Удалить столбец (доступно всем) +router.delete('/column/:columnId', async (req, res, next) => { + try { + const columnId = req.params.columnId; + await db.getQuery()('DELETE FROM user_columns WHERE id = $1', [columnId]); + res.json({ success: true }); + } catch (err) { + next(err); + } +}); + +// PATCH для обновления столбца (доступно всем) +router.patch('/column/:columnId', async (req, res, next) => { + try { + const columnId = req.params.columnId; + const { name, type, options, order } = req.body; + + // Построение динамического запроса + const updates = []; + const values = []; + let paramIndex = 1; + + if (name !== undefined) { + updates.push(`name = $${paramIndex++}`); + values.push(name); + } + if (type !== undefined) { + updates.push(`type = $${paramIndex++}`); + values.push(type); + } + if (options !== undefined) { + updates.push(`options = $${paramIndex++}`); + values.push(options ? JSON.stringify(options) : null); + } + if (order !== undefined) { + updates.push(`"order" = $${paramIndex++}`); + values.push(order); + } + + if (updates.length === 0) { + return res.status(400).json({ error: 'No fields to update' }); + } + + updates.push(`updated_at = NOW()`); + values.push(columnId); + + const query = `UPDATE user_columns SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING *`; + const result = await db.getQuery()(query, values); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Column not found' }); + } + + res.json(result.rows[0]); + } catch (err) { + next(err); + } +}); + +// PATCH: обновить название/описание таблицы (доступно всем) +router.patch('/:id', async (req, res, next) => { + try { + const tableId = req.params.id; + const { name, description } = req.body; + const result = await db.getQuery()( + `UPDATE user_tables SET + name = COALESCE($1, name), + description = COALESCE($2, description), + updated_at = NOW() + WHERE id = $3 RETURNING *`, + [name, description, tableId] + ); + res.json(result.rows[0]); + } catch (err) { + next(err); + } +}); + +// DELETE: удалить таблицу и каскадно все связанные строки/столбцы/ячейки (доступно всем) +router.delete('/:id', requireAuth, async (req, res, next) => { + try { + const tableId = Number(req.params.id); + console.log('Backend: typeof tableId:', typeof tableId, 'value:', tableId); + // Проверяем, существует ли таблица + const checkResult = await db.getQuery()('SELECT id, name FROM user_tables WHERE id = $1', [tableId]); + console.log('Backend: Table check result:', checkResult.rows); + if (checkResult.rows.length === 0) { + console.log('Backend: Table not found'); + return res.status(404).json({ error: 'Table not found' }); + } + // Удаляем только основную таблицу - каскадное удаление сработает автоматически + console.log('Backend: Executing DELETE query for table_id:', tableId); + const result = await db.getQuery()('DELETE FROM user_tables WHERE id = $1', [tableId]); + console.log('Backend: Delete result - rowCount:', result.rowCount); + res.json({ success: true, deleted: result.rowCount }); + } catch (err) { + console.error('Backend: Error deleting table:', err); + next(err); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 8c02ab6..46b09c8 100644 --- a/backend/server.js +++ b/backend/server.js @@ -15,6 +15,8 @@ const pgSession = require('connect-pg-simple')(session); const authService = require('./services/auth-service'); const logger = require('./utils/logger'); const EmailBotService = require('./services/emailBot.js'); +const tablesRouter = require('./routes/tables'); +const errorHandler = require('./middleware/errorHandler'); const PORT = process.env.PORT || 8000; @@ -94,6 +96,7 @@ app.use('/api/users', usersRouter); app.use('/api/auth', authRouter); app.use('/api/identities', identitiesRouter); app.use('/api/chat', chatRouter); +app.use('/api/tables', tablesRouter); // Эндпоинт для проверки состояния сервера app.get('/api/health', (req, res) => { @@ -122,4 +125,6 @@ process.on('uncaughtException', (err) => { logger.error('Uncaught Exception:', err); }); +app.use(errorHandler); + module.exports = app; diff --git a/docker-compose.yml b/docker-compose.yml index e8c234c..3e0bf22 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3.8' +# version: '3.8' services: postgres: @@ -60,7 +60,8 @@ services: - FRONTEND_URL=http://localhost:5173 ports: - "8000:8000" - command: sh -c "yarn run dev" + # command: sh -c "yarn run dev" # Временно комментируем эту строку + # command: nodemon server.js # Запускаем через nodemon frontend: build: diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 07e0786..71fa492 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -20,7 +20,7 @@ \ No newline at end of file diff --git a/frontend/src/components/cells/CellDate.vue b/frontend/src/components/cells/CellDate.vue new file mode 100644 index 0000000..c8b4fb3 --- /dev/null +++ b/frontend/src/components/cells/CellDate.vue @@ -0,0 +1,16 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/cells/CellNumber.vue b/frontend/src/components/cells/CellNumber.vue new file mode 100644 index 0000000..4d5a02b --- /dev/null +++ b/frontend/src/components/cells/CellNumber.vue @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/cells/CellSelect.vue b/frontend/src/components/cells/CellSelect.vue new file mode 100644 index 0000000..cdf2553 --- /dev/null +++ b/frontend/src/components/cells/CellSelect.vue @@ -0,0 +1,20 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/cells/CellText.vue b/frontend/src/components/cells/CellText.vue new file mode 100644 index 0000000..37ebb8b --- /dev/null +++ b/frontend/src/components/cells/CellText.vue @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/identity/EmailConnect.vue b/frontend/src/components/identity/EmailConnect.vue index e4023c4..7f95773 100644 --- a/frontend/src/components/identity/EmailConnect.vue +++ b/frontend/src/components/identity/EmailConnect.vue @@ -38,10 +38,10 @@ + + \ No newline at end of file diff --git a/frontend/src/components/tables/DynamicTableEditor.vue b/frontend/src/components/tables/DynamicTableEditor.vue new file mode 100644 index 0000000..3fcd0a4 --- /dev/null +++ b/frontend/src/components/tables/DynamicTableEditor.vue @@ -0,0 +1,276 @@ +