diff --git a/RAG_TASKS.md b/RAG_TASKS.md index 2c7624f..76b275d 100644 --- a/RAG_TASKS.md +++ b/RAG_TASKS.md @@ -126,4 +126,82 @@ --- +## Требования к CRM-интерфейсу для работы с контактами, тегами и настройками RAG-ассистента + +### 1. Раздел "Контакты" в CRM +- **Фильтры:** + - Новые пользователи (по дате создания или статусу "новый"). + - Новые входящие сообщения (по наличию непрочитанных/неотвеченных сообщений). + - Теги (мультиселект по тегам пользователя). +- **Детали контакта:** + - Просмотр истории сообщений. + - Список тегов пользователя. + - Добавление/удаление тегов через выпадающий список или автокомплит (создание связи в таблице user_tags). + +### 2. Настройки ИИ-ассистента +- **Выбор RAG-таблиц:** + - В настройках ассистента отображается список всех доступных RAG-таблиц. + - Администратор выбирает (чекбоксами или мультиселектом), какие таблицы использовать для поиска ответов. + - Для каждой выбранной таблицы отображается список тегов, которые она содержит. +- **Связь с тегами:** + - При генерации ответа ИИ использует только те RAG-таблицы и записи, которые соответствуют тегам пользователя. + +### 3. Рекомендации по интерфейсу (Vue) +- Компоненты: + - `ContactList.vue` — фильтры, список пользователей + - `ContactDetails.vue` — история сообщений, теги, добавление тегов + - `AssistantSettings.vue` — выбор RAG-таблиц + - `RagTableSelector.vue` — список таблиц с чекбоксами + - `TagList.vue` — просмотр тегов в выбранной таблице + +### 4. Схема действий администратора +1. В разделе "Контакты" находит нового пользователя/сообщение через фильтры. +2. В деталях контакта добавляет нужные теги пользователю. +3. В настройках ассистента выбирает, какие RAG-таблицы использовать для поиска по тегам. +4. ИИ-ассистент при ответе использует только релевантные RAG-таблицы и теги. + +### 5. Пример структуры таблиц для RAG и тегов +- `users` — пользователи +- `messages` — сообщения +- `tags` — справочник тегов +- `user_tags` — связь пользователей и тегов (user_id, tag_id) +- `rag_tables` — таблицы знаний (например, FAQ, инструкции) +- `rag_entries` — записи в таблицах знаний (content, rag_table_id, ...) +- `rag_entry_tags` — связь записей знаний и тегов (rag_entry_id, tag_id) + +--- + +## План внедрения RAG-ассистента в CRM + +1. **Создать RAG-таблицы для ИИ-ассистента** + - Таблицы для хранения знаний о компании, продуктах, услугах (например, `rag_tables`, `rag_entries`). + - Возможность добавлять, редактировать, удалять записи через UI. + - Каждая запись может быть связана с тегами (например, категория продукта, язык, сегмент клиента). + +2. **Создать таблицы с тегами для пользователей** + - Таблица тегов (`tags`). + - Связующая таблица `user_tags` (user_id, tag_id). + - UI для управления тегами и их привязкой к пользователям. + +3. **Отредактировать страницу настройки ИИ-ассистента** + - Добавить выбор, какие RAG-таблицы использовать для поиска. + - Отображать список тегов, связанных с выбранными таблицами. + - Возможность быстро подключать/отключать таблицы и теги. + +4. **Добавить в раздел "Контакты" фильтры (отдельные компоненты)** + - Фильтр по новым пользователям. + - Фильтр по новым входящим сообщениям. + - Фильтр по тегам (мультиселект). + - Каждый фильтр реализовать отдельным Vue-компонентом для переиспользования. + +5. **В "Детали контакта" добавить инлайн-кнопки** + - Кнопки: + - Сгенерировать (ответ с помощью ИИ) + - Редактировать (отредактировать сгенерированный ответ) + - Отправить (отправить ответ пользователю) + - Добавить в RAG-таблицу (сделать сообщение или ответ частью базы знаний) + - Кнопки должны быть доступны для каждого сообщения в истории. + +--- + **Этот документ будет дополняться по мере реализации каждого этапа.** \ No newline at end of file diff --git a/backend/db/migrations/029_create_ai_assistant_settings.sql b/backend/db/migrations/029_create_ai_assistant_settings.sql new file mode 100644 index 0000000..f0c9a01 --- /dev/null +++ b/backend/db/migrations/029_create_ai_assistant_settings.sql @@ -0,0 +1,21 @@ +CREATE TABLE IF NOT EXISTS ai_assistant_settings ( + id SERIAL PRIMARY KEY, + system_prompt TEXT, + selected_rag_tables INTEGER[], + languages TEXT[], + model TEXT, + rules JSONB, + updated_at TIMESTAMP DEFAULT NOW(), + updated_by INTEGER, +); + +-- Вставить дефолтную строку (глобальные настройки) +INSERT INTO ai_assistant_settings (system_prompt, selected_rag_tables, languages, model, rules) +VALUES ( + 'Вы — полезный ассистент. Отвечайте на русском языке.', + ARRAY[]::INTEGER[], + ARRAY['ru'], + 'qwen2.5', + '{"checkUserTags": true, "searchRagFirst": true, "generateIfNoRag": true, "requireAdminApproval": true}' +) +ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/backend/db/migrations/030_create_ai_assistant_rules.sql b/backend/db/migrations/030_create_ai_assistant_rules.sql new file mode 100644 index 0000000..620dace --- /dev/null +++ b/backend/db/migrations/030_create_ai_assistant_rules.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS ai_assistant_rules ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + rules JSONB NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +ALTER TABLE ai_assistant_settings +ADD COLUMN IF NOT EXISTS rules_id INTEGER REFERENCES ai_assistant_rules(id); \ No newline at end of file diff --git a/backend/db/migrations/031_alter_ai_assistant_settings_add_fields.sql b/backend/db/migrations/031_alter_ai_assistant_settings_add_fields.sql new file mode 100644 index 0000000..6af4cb4 --- /dev/null +++ b/backend/db/migrations/031_alter_ai_assistant_settings_add_fields.sql @@ -0,0 +1,5 @@ +-- Добавление недостающих полей для интеграции с Telegram и Email, а также для системного сообщения +ALTER TABLE ai_assistant_settings + ADD COLUMN IF NOT EXISTS telegram_settings_id INTEGER REFERENCES telegram_settings(id), + ADD COLUMN IF NOT EXISTS email_settings_id INTEGER REFERENCES email_settings(id), + ADD COLUMN IF NOT EXISTS system_message TEXT; \ No newline at end of file diff --git a/backend/routes/chat.js b/backend/routes/chat.js index b854168..2a9b1ac 100644 --- a/backend/routes/chat.js +++ b/backend/routes/chat.js @@ -6,6 +6,8 @@ const db = require('../db'); const logger = require('../utils/logger'); const { requireAuth } = require('../middleware/auth'); const crypto = require('crypto'); +const aiAssistantSettingsService = require('../services/aiAssistantSettingsService'); +const aiAssistantRulesService = require('../services/aiAssistantRulesService'); // Настройка multer для обработки файлов в памяти const storage = multer.memoryStorage(); @@ -61,19 +63,28 @@ async function processGuestMessages(userId, guestId) { const guestMessages = guestMessagesResult.rows; logger.info(`Found ${guestMessages.length} guest messages for guest ID ${guestId}`); - // Создаем новый диалог для этих сообщений - const firstMessage = guestMessages[0]; - const title = firstMessage.content - ? (firstMessage.content.length > 30 ? `${firstMessage.content.substring(0, 30)}...` : firstMessage.content) - : (firstMessage.attachment_filename ? `Файл: ${firstMessage.attachment_filename}` : 'Новый диалог'); - - const newConversationResult = await db.getQuery()( - 'INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING *', - [userId, title] + // --- Новый порядок: ищем последний диалог пользователя --- + let conversation = null; + const lastConvResult = await db.getQuery()( + 'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1', + [userId] ); - - const conversation = newConversationResult.rows[0]; - logger.info(`Created new conversation ${conversation.id} for guest messages`); + if (lastConvResult.rows.length > 0) { + conversation = lastConvResult.rows[0]; + } else { + // Если нет ни одного диалога, создаём новый + const firstMessage = guestMessages[0]; + const title = firstMessage.content + ? (firstMessage.content.length > 30 ? `${firstMessage.content.substring(0, 30)}...` : firstMessage.content) + : (firstMessage.attachment_filename ? `Файл: ${firstMessage.attachment_filename}` : 'Новый диалог'); + const newConversationResult = await db.getQuery()( + 'INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING *', + [userId, title] + ); + conversation = newConversationResult.rows[0]; + logger.info(`Created new conversation ${conversation.id} for guest messages`); + } + // --- КОНЕЦ блока поиска/создания диалога --- // Отслеживаем успешные сохранения сообщений const savedMessageIds = []; @@ -81,7 +92,6 @@ async function processGuestMessages(userId, guestId) { // Обрабатываем каждое гостевое сообщение for (const guestMessage of guestMessages) { logger.info(`Processing guest message ID ${guestMessage.id}: ${guestMessage.content || guestMessage.attachment_filename || '(empty)'}`); - try { // Сохраняем сообщение пользователя в таблицу messages, включая данные файла const userMessageResult = await db.getQuery()( @@ -103,39 +113,59 @@ async function processGuestMessages(userId, guestId) { guestMessage.attachment_data // BYTEA ] ); - const savedUserMessage = userMessageResult.rows[0]; logger.info(`Saved user message with ID ${savedUserMessage.id}`); savedMessageIds.push(guestMessage.id); - - // Получаем ответ от ИИ только для текстовых сообщений - if (!guestMessage.is_ai && guestMessage.content) { - logger.info('Getting AI response for:', guestMessage.content); - const language = guestMessage.language || 'auto'; - // Предполагаем, что aiAssistant.getResponse принимает только текст - const aiResponseContent = await aiAssistant.getResponse(guestMessage.content, language); - logger.info('AI response received' + (aiResponseContent ? '' : ' (empty)'), 'for conversation', conversation.id); - - if (aiResponseContent) { - // Сохраняем ответ от ИИ (у него нет вложений) - const aiMessageResult = await db.getQuery()( - `INSERT INTO messages - (conversation_id, content, sender_type, role, channel, created_at, user_id) - VALUES - ($1, $2, 'assistant', 'assistant', 'web', $3, $4) - RETURNING *`, - [ - conversation.id, - aiResponseContent, - new Date(), - userId - ] + // --- Генерируем ответ ИИ на гостевое сообщение, если это текст --- + if (guestMessage.content) { + // Проверяем, что на это сообщение ещё нет ответа ассистента + const aiReplyExists = await db.getQuery()( + `SELECT 1 FROM messages WHERE conversation_id = $1 AND sender_type = 'assistant' AND created_at > $2 LIMIT 1`, + [conversation.id, guestMessage.created_at] + ); + if (!aiReplyExists.rows.length) { + try { + // Получаем настройки ассистента + const aiSettings = await aiAssistantSettingsService.getSettings(); + let rules = null; + if (aiSettings && aiSettings.rules_id) { + rules = await aiAssistantRulesService.getRuleById(aiSettings.rules_id); + } + // Получаем историю сообщений до этого guestMessage (до created_at) + const historyResult = await db.getQuery()( + 'SELECT sender_type, content FROM messages WHERE conversation_id = $1 AND created_at < $2 ORDER BY created_at DESC LIMIT 10', + [conversation.id, guestMessage.created_at] ); - logger.info(`Saved AI response with ID ${aiMessageResult.rows[0].id}`); + const history = historyResult.rows.reverse().map(msg => ({ + role: msg.sender_type === 'user' ? 'user' : 'assistant', + content: msg.content + })); + // Язык guestMessage.language или auto + const detectedLanguage = guestMessage.language === 'auto' ? aiAssistant.detectLanguage(guestMessage.content) : guestMessage.language; + logger.info('Getting AI response for guest message:', guestMessage.content); + const aiResponseContent = await aiAssistant.getResponse( + guestMessage.content, + detectedLanguage, + history, + aiSettings ? aiSettings.system_prompt : '', + rules ? rules.rules : null + ); + logger.info('AI response for guest message received' + (aiResponseContent ? '' : ' (empty)'), { conversationId: conversation.id }); + if (aiResponseContent) { + await db.getQuery()( + `INSERT INTO messages + (conversation_id, user_id, content, sender_type, role, channel) + VALUES ($1, $2, $3, 'assistant', 'assistant', 'web')`, + [conversation.id, userId, aiResponseContent] + ); + logger.info('AI response for guest message saved', { conversationId: conversation.id }); + } + } catch (aiError) { + logger.error('Error getting or saving AI response for guest message:', aiError); + } } - } else { - logger.info(`Skipping AI response for guest message ID ${guestMessage.id} (is_ai: ${guestMessage.is_ai}, hasContent: ${!!guestMessage.content})`); } + // --- конец блока генерации ответа ИИ --- } catch (error) { logger.error(`Error processing guest message ${guestMessage.id}: ${error.message}`, { stack: error.stack }); // Продолжаем с другими сообщениями в случае ошибки @@ -254,10 +284,28 @@ router.post('/guest-message', upload.array('attachments'), async (req, res) => { // Не прерываем ответ пользователю из-за ошибки сессии } + // Получаем настройки ассистента для systemMessage + let telegramBotUrl = null; + let supportEmailAddr = null; + try { + const aiSettings = await aiAssistantSettingsService.getSettings(); + if (aiSettings && aiSettings.telegramBot && aiSettings.telegramBot.bot_username) { + telegramBotUrl = `https://t.me/${aiSettings.telegramBot.bot_username}`; + } + if (aiSettings && aiSettings.supportEmail && aiSettings.supportEmail.from_email) { + supportEmailAddr = aiSettings.supportEmail.from_email; + } + } catch (e) { + logger.error('Ошибка получения настроек ассистента для systemMessage:', e); + } + res.json({ success: true, messageId: savedMessageId, // Возвращаем ID сохраненного сообщения - guestId: guestId // Возвращаем использованный guestId + guestId: guestId, // Возвращаем использованный guestId + systemMessage: 'Для продолжения диалога авторизуйтесь: подключите кошелек, перейдите в чат-бот Telegram или отправьте письмо на email.', + telegramBotUrl, + supportEmail: supportEmailAddr }); } catch (error) { logger.error('Error saving guest message:', error); @@ -303,18 +351,27 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re } conversation = convResult.rows[0]; } else { - // Создаем новый диалог, если ID не предоставлен - const title = message - ? (message.length > 50 ? `${message.substring(0, 50)}...` : message) - : (file ? `Файл: ${file.originalname}` : 'Новый диалог'); - - const newConvResult = await db.getQuery()( - 'INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING *', - [userId, title] + // Ищем последний диалог пользователя + const lastConvResult = await db.getQuery()( + 'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1', + [userId] ); - conversation = newConvResult.rows[0]; - conversationId = conversation.id; - logger.info('Created new conversation', { conversationId, userId }); + if (lastConvResult.rows.length > 0) { + conversation = lastConvResult.rows[0]; + conversationId = conversation.id; + } else { + // Создаем новый диалог, если нет ни одного + const title = message + ? (message.length > 50 ? `${message.substring(0, 50)}...` : message) + : (file ? `Файл: ${file.originalname}` : 'Новый диалог'); + const newConvResult = await db.getQuery()( + 'INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING *', + [userId, title] + ); + conversation = newConvResult.rows[0]; + conversationId = conversation.id; + logger.info('Created new conversation', { conversationId, userId }); + } } // Подготавливаем данные для вставки сообщения пользователя @@ -348,9 +405,32 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re let aiMessage = null; if (messageContent) { // Только для текстовых сообщений try { + // Получаем настройки ассистента + const aiSettings = await aiAssistantSettingsService.getSettings(); + let rules = null; + 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); + 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) { @@ -396,6 +476,12 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re return formatted; }; + // Обновляем updated_at у диалога + await db.getQuery()( + 'UPDATE conversations SET updated_at = NOW() WHERE id = $1', + [conversationId] + ); + res.json({ success: true, conversationId: conversationId, @@ -541,6 +627,26 @@ router.get('/history', requireAuth, async (req, res) => { } }); +// --- Новый роут для связывания гостя после аутентификации --- +router.post('/process-guest', requireAuth, async (req, res) => { + const userId = req.session.userId; + const { guestId } = req.body; + if (!guestId) { + return res.status(400).json({ success: false, error: 'guestId is required' }); + } + try { + const result = await module.exports.processGuestMessages(userId, guestId); + if (result && result.conversationId) { + return res.json({ success: true, conversationId: result.conversationId }); + } else { + return res.json({ success: false, error: result.error || 'No conversation created' }); + } + } catch (error) { + logger.error('Error in /process-guest:', error); + return res.status(500).json({ success: false, error: 'Internal error' }); + } +}); + // Экспортируем маршрутизатор и функцию processGuestMessages отдельно module.exports = router; module.exports.processGuestMessages = processGuestMessages; diff --git a/backend/routes/settings.js b/backend/routes/settings.js index 8ca07a6..b9d4ce3 100644 --- a/backend/routes/settings.js +++ b/backend/routes/settings.js @@ -8,6 +8,8 @@ const authTokenService = require('../services/authTokenService'); const aiProviderSettingsService = require('../services/aiProviderSettingsService'); const aiAssistant = require('../services/ai-assistant'); const dns = require('node:dns').promises; +const aiAssistantSettingsService = require('../services/aiAssistantSettingsService'); +const aiAssistantRulesService = require('../services/aiAssistantRulesService'); // Логируем версию ethers для отладки logger.info(`Ethers version: ${ethers.version || 'unknown'}`); @@ -239,4 +241,92 @@ router.post('/ai-settings/:provider/verify', requireAdmin, async (req, res, next } }); +router.get('/ai-assistant', requireAdmin, async (req, res, next) => { + try { + const settings = await aiAssistantSettingsService.getSettings(); + res.json({ success: true, settings }); + } catch (error) { + next(error); + } +}); + +router.put('/ai-assistant', requireAdmin, async (req, res, next) => { + try { + const updated = await aiAssistantSettingsService.upsertSettings({ ...req.body, updated_by: req.session.userId || null }); + res.json({ success: true, settings: updated }); + } catch (error) { + next(error); + } +}); + +// Получить все наборы правил +router.get('/ai-assistant-rules', requireAdmin, async (req, res, next) => { + try { + const rules = await aiAssistantRulesService.getAllRules(); + res.json({ success: true, rules }); + } catch (error) { + next(error); + } +}); + +// Получить набор правил по id +router.get('/ai-assistant-rules/:id', requireAdmin, async (req, res, next) => { + try { + const rule = await aiAssistantRulesService.getRuleById(req.params.id); + res.json({ success: true, rule }); + } catch (error) { + next(error); + } +}); + +// Создать набор правил +router.post('/ai-assistant-rules', requireAdmin, async (req, res, next) => { + try { + const created = await aiAssistantRulesService.createRule(req.body); + res.json({ success: true, rule: created }); + } catch (error) { + next(error); + } +}); + +// Обновить набор правил +router.put('/ai-assistant-rules/:id', requireAdmin, async (req, res, next) => { + try { + const updated = await aiAssistantRulesService.updateRule(req.params.id, req.body); + res.json({ success: true, rule: updated }); + } catch (error) { + next(error); + } +}); + +// Удалить набор правил +router.delete('/ai-assistant-rules/:id', requireAdmin, async (req, res, next) => { + try { + await aiAssistantRulesService.deleteRule(req.params.id); + res.json({ success: true }); + } catch (error) { + next(error); + } +}); + +// Получить все email_settings для выпадающего списка +router.get('/email-settings', requireAdmin, async (req, res, next) => { + try { + const { rows } = await require('../db').getQuery()('SELECT id, from_email FROM email_settings ORDER BY id'); + res.json({ success: true, items: rows }); + } catch (error) { + next(error); + } +}); + +// Получить все telegram_settings для выпадающего списка +router.get('/telegram-settings', requireAdmin, async (req, res, next) => { + try { + const { rows } = await require('../db').getQuery()('SELECT id, bot_username FROM telegram_settings ORDER BY id'); + res.json({ success: true, items: rows }); + } catch (error) { + next(error); + } +}); + module.exports = router; \ No newline at end of file diff --git a/backend/services/ai-assistant.js b/backend/services/ai-assistant.js index 64d05a1..e2828fd 100644 --- a/backend/services/ai-assistant.js +++ b/backend/services/ai-assistant.js @@ -34,30 +34,51 @@ class AIAssistant { } // Основной метод для получения ответа - async getResponse(message, language = 'auto') { + async getResponse(message, language = 'auto', history = null, systemPrompt = '', rules = null) { try { - console.log('getResponse called with:', { message, language }); + console.log('getResponse called with:', { message, language, history, systemPrompt, rules }); // Определяем язык, если не указан явно const detectedLanguage = language === 'auto' ? this.detectLanguage(message) : language; - console.log('Detected language:', detectedLanguage); - // Сначала пробуем прямой API запрос + // Формируем system prompt с учётом правил + let fullSystemPrompt = systemPrompt || ''; + if (rules && typeof rules === 'object') { + fullSystemPrompt += '\n' + JSON.stringify(rules, null, 2); + } + + // Формируем массив сообщений для Qwen2.5/OpenAI API + const messages = []; + if (fullSystemPrompt) { + messages.push({ role: 'system', content: fullSystemPrompt }); + } + if (Array.isArray(history) && history.length > 0) { + for (const msg of history) { + if (msg.role && msg.content) { + messages.push({ role: msg.role, content: msg.content }); + } + } + } + // Добавляем текущее сообщение пользователя + messages.push({ role: 'user', content: message }); + + // Пробуем прямой API запрос (OpenAI-совместимый endpoint) try { console.log('Trying direct API request...'); - const response = await this.fallbackRequest(message, detectedLanguage); + const response = await this.fallbackRequestOpenAI(messages, detectedLanguage); console.log('Direct API response received:', response); return response; } catch (error) { console.error('Error in direct API request:', error); } - // Если прямой запрос не удался, пробуем через ChatOllama + // Если прямой запрос не удался, пробуем через ChatOllama (склеиваем сообщения в текст) const chat = this.createChat(detectedLanguage); try { + const prompt = messages.map(m => `${m.role === 'user' ? 'Пользователь' : m.role === 'assistant' ? 'Ассистент' : 'Система'}: ${m.content}`).join('\n'); console.log('Sending request to ChatOllama...'); - const response = await chat.invoke(message); + const response = await chat.invoke(prompt); console.log('ChatOllama response:', response); return response.content; } catch (error) { @@ -70,24 +91,17 @@ class AIAssistant { } } - // Альтернативный метод запроса через прямой API - async fallbackRequest(message, language) { + // Новый метод для OpenAI/Qwen2.5 совместимого endpoint + async fallbackRequestOpenAI(messages, language) { try { - console.log('Using fallback request method with:', { message, language }); - - const systemPrompt = - language === 'ru' - ? 'Вы - полезный ассистент. Отвечайте на русском языке.' - : 'You are a helpful assistant. Respond in English.'; - - console.log('Sending request to Ollama API...'); - const response = await fetch(`${this.baseUrl}/api/generate`, { + console.log('Using fallbackRequestOpenAI with:', { messages, language }); + const model = this.defaultModel; + const response = await fetch(`${this.baseUrl}/v1/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - model: this.defaultModel, - prompt: message, - system: systemPrompt, + model, + messages, stream: false, options: { temperature: 0.7, @@ -95,16 +109,17 @@ class AIAssistant { }, }), }); - if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } - const data = await response.json(); - console.log('Ollama API response:', data); - return data.response; + // Qwen2.5/OpenAI API возвращает ответ в data.choices[0].message.content + if (data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content) { + return data.choices[0].message.content; + } + return data.response || ''; } catch (error) { - console.error('Error in fallback request:', error); + console.error('Error in fallbackRequestOpenAI:', error); throw error; } } diff --git a/backend/services/aiAssistantRulesService.js b/backend/services/aiAssistantRulesService.js new file mode 100644 index 0000000..776eb14 --- /dev/null +++ b/backend/services/aiAssistantRulesService.js @@ -0,0 +1,35 @@ +const db = require('../db'); +const TABLE = 'ai_assistant_rules'; + +async function getAllRules() { + const { rows } = await db.getQuery()(`SELECT * FROM ${TABLE} ORDER BY id`); + return rows; +} + +async function getRuleById(id) { + const { rows } = await db.getQuery()(`SELECT * FROM ${TABLE} WHERE id = $1`, [id]); + return rows[0] || null; +} + +async function createRule({ name, description, rules }) { + const { rows } = await db.getQuery()( + `INSERT INTO ${TABLE} (name, description, rules, created_at, updated_at) + VALUES ($1, $2, $3, NOW(), NOW()) RETURNING *`, + [name, description, rules] + ); + return rows[0]; +} + +async function updateRule(id, { name, description, rules }) { + const { rows } = await db.getQuery()( + `UPDATE ${TABLE} SET name = $1, description = $2, rules = $3, updated_at = NOW() WHERE id = $4 RETURNING *`, + [name, description, rules, id] + ); + return rows[0]; +} + +async function deleteRule(id) { + await db.getQuery()(`DELETE FROM ${TABLE} WHERE id = $1`, [id]); +} + +module.exports = { getAllRules, getRuleById, createRule, updateRule, deleteRule }; \ No newline at end of file diff --git a/backend/services/aiAssistantSettingsService.js b/backend/services/aiAssistantSettingsService.js new file mode 100644 index 0000000..cf33d82 --- /dev/null +++ b/backend/services/aiAssistantSettingsService.js @@ -0,0 +1,48 @@ +const db = require('../db'); +const TABLE = 'ai_assistant_settings'; + +async function getSettings() { + const { rows } = await db.getQuery()(`SELECT * FROM ${TABLE} ORDER BY id LIMIT 1`); + const settings = rows[0] || null; + if (!settings) return null; + + // Получаем связанные данные из telegram_settings и email_settings + let telegramBot = null; + let supportEmail = null; + if (settings.telegram_settings_id) { + const tg = await db.getQuery()('SELECT * FROM telegram_settings WHERE id = $1', [settings.telegram_settings_id]); + telegramBot = tg.rows[0] || null; + } + if (settings.email_settings_id) { + const em = await db.getQuery()('SELECT * FROM email_settings WHERE id = $1', [settings.email_settings_id]); + supportEmail = em.rows[0] || null; + } + return { + ...settings, + telegramBot, + supportEmail + }; +} + +async function upsertSettings({ system_prompt, selected_rag_tables, languages, 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) + ON CONFLICT (id) DO UPDATE SET + system_prompt = EXCLUDED.system_prompt, + selected_rag_tables = EXCLUDED.selected_rag_tables, + languages = EXCLUDED.languages, + model = EXCLUDED.model, + rules = EXCLUDED.rules, + updated_at = NOW(), + updated_by = EXCLUDED.updated_by, + telegram_settings_id = EXCLUDED.telegram_settings_id, + 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] + ); + return rows[0]; +} + +module.exports = { getSettings, upsertSettings }; \ No newline at end of file diff --git a/frontend/src/components/Message.vue b/frontend/src/components/Message.vue index f2363b2..bb92535 100644 --- a/frontend/src/components/Message.vue +++ b/frontend/src/components/Message.vue @@ -15,6 +15,12 @@
+ +
+ + +
+
@@ -168,6 +174,14 @@ const formatFileSize = (bytes) => { return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; +function openTelegram(url) { + window.open(url, '_blank'); +} +function copyEmail(email) { + navigator.clipboard.writeText(email); + // Можно добавить уведомление "Email скопирован" +} + \ No newline at end of file diff --git a/frontend/src/components/ai-assistant/RuleEditor.vue b/frontend/src/components/ai-assistant/RuleEditor.vue new file mode 100644 index 0000000..2b75c6c --- /dev/null +++ b/frontend/src/components/ai-assistant/RuleEditor.vue @@ -0,0 +1,122 @@ + + + \ No newline at end of file diff --git a/frontend/src/composables/useChat.js b/frontend/src/composables/useChat.js index 56b0460..7499832 100644 --- a/frontend/src/composables/useChat.js +++ b/frontend/src/composables/useChat.js @@ -3,6 +3,15 @@ import api from '../api/axios'; import { getFromStorage, setToStorage, removeFromStorage } from '../utils/storage'; import { generateUniqueId } from '../utils/helpers'; +function initGuestId() { + let id = getFromStorage('guestId', ''); + if (!id) { + id = generateUniqueId(); + setToStorage('guestId', id); + } + return id; +} + export function useChat(auth) { const messages = ref([]); const newMessage = ref(''); @@ -20,7 +29,7 @@ export function useChat(auth) { isLinkingGuest: false, // Флаг для процесса связывания гостевых сообщений (пока не используется активно) }); - const guestId = ref(getFromStorage('guestId', '')); + const guestId = ref(initGuestId()); const shouldLoadHistory = computed(() => { return auth.isAuthenticated.value || !!guestId.value; @@ -133,7 +142,7 @@ export function useChat(auth) { // Очищаем гостевые данные после успешной аутентификации и загрузки if (authType) { removeFromStorage('guestMessages'); - removeFromStorage('guestId'); + // removeFromStorage('guestId'); // Удаление guestId теперь только после успешного связывания guestId.value = ''; } @@ -219,7 +228,7 @@ export function useChat(auth) { let apiUrl = '/api/chat/message'; if (isGuestMessage) { if (!guestId.value) { - guestId.value = generateUniqueId(); + guestId.value = initGuestId(); setToStorage('guestId', guestId.value); } formData.append('guestId', guestId.value); @@ -254,6 +263,20 @@ export function useChat(auth) { }); } + // Добавляем системное сообщение для гостя (только на клиенте, не сохраняется в истории) + if (isGuestMessage && response.data.systemMessage) { + messages.value.push({ + id: `system-${Date.now()}`, + content: response.data.systemMessage, + sender_type: 'system', + role: 'system', + timestamp: new Date().toISOString(), + isSystem: true, + telegramBotUrl: response.data.telegramBotUrl, + supportEmail: response.data.supportEmail + }); + } + // Сохраняем гостевое сообщение (если нужно) // В текущей реализации HomeView гостевые сообщения из localstorage загружаются только при старте // Если нужна синхронизация после отправки, логику нужно добавить/изменить @@ -325,6 +348,23 @@ export function useChat(auth) { } }; + // --- Связывание гостевых сообщений после аутентификации --- + const linkGuestMessagesAfterAuth = async () => { + if (!guestId.value) return; + try { + const response = await api.post('/api/chat/process-guest', { guestId: guestId.value }); + if (response.data.success && response.data.conversationId) { + // Можно сразу загрузить историю по этому диалогу, если нужно + await loadMessages({ initial: true }); + // Удаляем guestId только после успешного связывания + removeFromStorage('guestId'); + guestId.value = ''; + } + } catch (error) { + console.error('[useChat] Ошибка связывания гостевых сообщений:', error); + } + }; + // --- Watchers --- // Сортировка сообщений при изменении watch(messages, (newMessages) => { @@ -379,5 +419,6 @@ export function useChat(auth) { loadMessages, handleSendMessage, loadGuestMessagesFromStorage, // Экспортируем на всякий случай + linkGuestMessagesAfterAuth, // Экспортируем для вызова после авторизации }; } \ No newline at end of file diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index 4fe96a1..0d07880 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -64,6 +64,7 @@ messageLoading, loadMessages, handleSendMessage, + linkGuestMessagesAfterAuth, } = useChat(auth); // ===================================================================== @@ -91,10 +92,12 @@ // ===================================================================== // Функция обновления сообщений после авторизации - const handleAuthEvent = (eventData) => { + const handleAuthEvent = async (eventData) => { console.log('[HomeView] Получено событие изменения авторизации:', eventData); if (eventData.isAuthenticated) { - // Пользователь только что авторизовался - загрузим сообщения + // Сначала связываем гостевые сообщения, если есть + await linkGuestMessagesAfterAuth(); + // Затем загружаем сообщения (если не было гостя, просто загрузка) loadMessages({ initial: true, authType: eventData.authType || 'wallet' }); } else { // Пользователь вышел из системы - можно очистить или обновить данные diff --git a/frontend/src/views/settings/AiAssistantSettings.vue b/frontend/src/views/settings/AiAssistantSettings.vue new file mode 100644 index 0000000..c3aebac --- /dev/null +++ b/frontend/src/views/settings/AiAssistantSettings.vue @@ -0,0 +1,211 @@ +