ваше сообщение коммита
This commit is contained in:
@@ -15,6 +15,7 @@ const messagesRoutes = require('./routes/messages');
|
||||
const userTagsRoutes = require('./routes/userTags');
|
||||
const tagsInitRoutes = require('./routes/tagsInit');
|
||||
const tagsRoutes = require('./routes/tags');
|
||||
const ragRoutes = require('./routes/rag'); // Новый роут для RAG-ассистента
|
||||
|
||||
// Проверка и создание директорий для хранения данных контрактов
|
||||
const ensureDirectoriesExist = () => {
|
||||
@@ -188,6 +189,7 @@ app.use('/api/messages', messagesRoutes);
|
||||
app.use('/api/tags', tagsInitRoutes);
|
||||
app.use('/api/tags', tagsRoutes);
|
||||
app.use('/api/identities', identitiesRoutes);
|
||||
app.use('/api/rag', ragRoutes); // Подключаем роут
|
||||
|
||||
const nonceStore = new Map(); // или любая другая реализация хранилища nonce
|
||||
|
||||
|
||||
@@ -351,9 +351,9 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
|
||||
} else {
|
||||
// Обычный пользователь — только в свой диалог
|
||||
convResult = await db.getQuery()(
|
||||
'SELECT * FROM conversations WHERE id = $1 AND user_id = $2',
|
||||
[conversationId, userId]
|
||||
);
|
||||
'SELECT * FROM conversations WHERE id = $1 AND user_id = $2',
|
||||
[conversationId, userId]
|
||||
);
|
||||
}
|
||||
if (convResult.rows.length === 0) {
|
||||
logger.warn('Conversation not found or access denied', { conversationId, userId });
|
||||
|
||||
@@ -2,6 +2,8 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../db');
|
||||
const { broadcastMessagesUpdate } = require('../wsHub');
|
||||
const telegramBot = require('../services/telegramBot');
|
||||
const emailBot = new (require('../services/emailBot'))();
|
||||
|
||||
// GET /api/messages?userId=123
|
||||
router.get('/', async (req, res) => {
|
||||
@@ -42,11 +44,100 @@ router.get('/', async (req, res) => {
|
||||
router.post('/', async (req, res) => {
|
||||
const { user_id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata } = req.body;
|
||||
try {
|
||||
const result = await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,NOW(),$7,$8,$9,$10,$11) RETURNING *`,
|
||||
[user_id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata]
|
||||
// Проверка наличия идентификатора для выбранного канала
|
||||
if (channel === 'email') {
|
||||
const emailIdentity = await db.getQuery()(
|
||||
'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2 LIMIT 1',
|
||||
[user_id, 'email']
|
||||
);
|
||||
if (emailIdentity.rows.length === 0) {
|
||||
return res.status(400).json({ error: 'У пользователя не указан email. Сообщение не отправлено.' });
|
||||
}
|
||||
}
|
||||
if (channel === 'telegram') {
|
||||
const tgIdentity = await db.getQuery()(
|
||||
'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2 LIMIT 1',
|
||||
[user_id, 'telegram']
|
||||
);
|
||||
if (tgIdentity.rows.length === 0) {
|
||||
return res.status(400).json({ error: 'У пользователя не привязан Telegram. Сообщение не отправлено.' });
|
||||
}
|
||||
}
|
||||
if (channel === 'wallet' || channel === 'web3' || channel === 'web') {
|
||||
const walletIdentity = await db.getQuery()(
|
||||
'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2 LIMIT 1',
|
||||
[user_id, 'wallet']
|
||||
);
|
||||
if (walletIdentity.rows.length === 0) {
|
||||
return res.status(400).json({ error: 'У пользователя не привязан кошелёк. Сообщение не отправлено.' });
|
||||
}
|
||||
}
|
||||
// 1. Проверяем, есть ли беседа для user_id
|
||||
let conversationResult = await db.getQuery()(
|
||||
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
|
||||
[user_id]
|
||||
);
|
||||
let conversation;
|
||||
if (conversationResult.rows.length === 0) {
|
||||
// 2. Если нет — создаём новую беседу
|
||||
const title = `Чат с пользователем ${user_id}`;
|
||||
const newConv = await db.getQuery()(
|
||||
'INSERT INTO conversations (user_id, title, created_at, updated_at) VALUES ($1, $2, NOW(), NOW()) RETURNING *',
|
||||
[user_id, title]
|
||||
);
|
||||
conversation = newConv.rows[0];
|
||||
} else {
|
||||
conversation = conversationResult.rows[0];
|
||||
}
|
||||
// 3. Сохраняем сообщение с conversation_id
|
||||
const result = await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,NOW(),$8,$9,$10,$11,$12) RETURNING *`,
|
||||
[user_id, conversation.id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata]
|
||||
);
|
||||
// 4. Если это исходящее сообщение для Telegram — отправляем через бота
|
||||
if (channel === 'telegram' && direction === 'out') {
|
||||
try {
|
||||
console.log(`[messages.js] Попытка отправки сообщения в Telegram для user_id=${user_id}`);
|
||||
// Получаем Telegram ID пользователя
|
||||
const tgIdentity = await db.getQuery()(
|
||||
'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2 LIMIT 1',
|
||||
[user_id, 'telegram']
|
||||
);
|
||||
console.log(`[messages.js] Результат поиска Telegram ID:`, tgIdentity.rows);
|
||||
if (tgIdentity.rows.length > 0) {
|
||||
const telegramId = tgIdentity.rows[0].provider_id;
|
||||
console.log(`[messages.js] Отправка сообщения в Telegram ID: ${telegramId}, текст: ${content}`);
|
||||
const bot = await telegramBot.getBot();
|
||||
try {
|
||||
const sendResult = await bot.telegram.sendMessage(telegramId, content);
|
||||
console.log(`[messages.js] Результат отправки в Telegram:`, sendResult);
|
||||
} catch (sendErr) {
|
||||
console.error(`[messages.js] Ошибка при отправке в Telegram:`, sendErr);
|
||||
}
|
||||
} else {
|
||||
console.warn(`[messages.js] Не найден Telegram ID для user_id=${user_id}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[messages.js] Ошибка отправки сообщения в Telegram:', err);
|
||||
}
|
||||
}
|
||||
// 5. Если это исходящее сообщение для Email — отправляем email
|
||||
if (channel === 'email' && direction === 'out') {
|
||||
try {
|
||||
// Получаем email пользователя
|
||||
const emailIdentity = await db.getQuery()(
|
||||
'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2 LIMIT 1',
|
||||
[user_id, 'email']
|
||||
);
|
||||
if (emailIdentity.rows.length > 0) {
|
||||
const email = emailIdentity.rows[0].provider_id;
|
||||
await emailBot.sendEmail(email, 'Новое сообщение', content);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[messages.js] Ошибка отправки email:', err);
|
||||
}
|
||||
}
|
||||
broadcastMessagesUpdate();
|
||||
res.json({ success: true, message: result.rows[0] });
|
||||
} catch (e) {
|
||||
@@ -123,4 +214,90 @@ router.get('/conversations', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Массовая рассылка сообщения во все каналы пользователя
|
||||
router.post('/broadcast', async (req, res) => {
|
||||
const { user_id, content } = req.body;
|
||||
if (!user_id || !content) {
|
||||
return res.status(400).json({ error: 'user_id и content обязательны' });
|
||||
}
|
||||
try {
|
||||
// Получаем все идентификаторы пользователя
|
||||
const identitiesRes = await db.getQuery()(
|
||||
'SELECT provider, provider_id FROM user_identities WHERE user_id = $1',
|
||||
[user_id]
|
||||
);
|
||||
const identities = identitiesRes.rows;
|
||||
// --- Найти или создать беседу (conversation) ---
|
||||
let conversationResult = await db.getQuery()(
|
||||
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
|
||||
[user_id]
|
||||
);
|
||||
let conversation;
|
||||
if (conversationResult.rows.length === 0) {
|
||||
const title = `Чат с пользователем ${user_id}`;
|
||||
const newConv = await db.getQuery()(
|
||||
'INSERT INTO conversations (user_id, title, created_at, updated_at) VALUES ($1, $2, NOW(), NOW()) RETURNING *',
|
||||
[user_id, title]
|
||||
);
|
||||
conversation = newConv.rows[0];
|
||||
} else {
|
||||
conversation = conversationResult.rows[0];
|
||||
}
|
||||
const results = [];
|
||||
let sent = false;
|
||||
// Email
|
||||
const email = identities.find(i => i.provider === 'email')?.provider_id;
|
||||
if (email) {
|
||||
try {
|
||||
await emailBot.sendEmail(email, 'Новое сообщение', content);
|
||||
// Сохраняем в messages с conversation_id
|
||||
await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8)`,
|
||||
[user_id, conversation.id, 'admin', content, 'email', 'user', 'out', JSON.stringify({ broadcast: true })]
|
||||
);
|
||||
results.push({ channel: 'email', status: 'sent' });
|
||||
sent = true;
|
||||
} catch (err) {
|
||||
results.push({ channel: 'email', status: 'error', error: err.message });
|
||||
}
|
||||
}
|
||||
// Telegram
|
||||
const telegram = identities.find(i => i.provider === 'telegram')?.provider_id;
|
||||
if (telegram) {
|
||||
try {
|
||||
const bot = await telegramBot.getBot();
|
||||
await bot.telegram.sendMessage(telegram, content);
|
||||
await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8)`,
|
||||
[user_id, conversation.id, 'admin', content, 'telegram', 'user', 'out', JSON.stringify({ broadcast: true })]
|
||||
);
|
||||
results.push({ channel: 'telegram', status: 'sent' });
|
||||
sent = true;
|
||||
} catch (err) {
|
||||
results.push({ channel: 'telegram', status: 'error', error: err.message });
|
||||
}
|
||||
}
|
||||
// Wallet/web3
|
||||
const wallet = identities.find(i => i.provider === 'wallet')?.provider_id;
|
||||
if (wallet) {
|
||||
// Здесь можно реализовать отправку через web3, если нужно
|
||||
await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8)`,
|
||||
[user_id, conversation.id, 'admin', content, 'wallet', 'user', 'out', JSON.stringify({ broadcast: true })]
|
||||
);
|
||||
results.push({ channel: 'wallet', status: 'saved' });
|
||||
sent = true;
|
||||
}
|
||||
if (!sent) {
|
||||
return res.status(400).json({ error: 'У пользователя нет ни одного канала для рассылки.' });
|
||||
}
|
||||
res.json({ success: true, results });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'Broadcast error', details: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
31
backend/routes/rag.js
Normal file
31
backend/routes/rag.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { ragAnswer, generateLLMResponse } = require('../services/ragService');
|
||||
|
||||
router.post('/answer', async (req, res) => {
|
||||
const { tableId, question, userTags, product, systemPrompt, priority, date, rules, history, model, language } = req.body;
|
||||
try {
|
||||
const ragResult = await ragAnswer({ tableId, userQuestion: question, userTags, product });
|
||||
const llmResponse = await generateLLMResponse({
|
||||
userQuestion: question,
|
||||
context: ragResult.context,
|
||||
clarifyingAnswer: ragResult.clarifyingAnswer,
|
||||
objectionAnswer: ragResult.objectionAnswer,
|
||||
answer: ragResult.answer,
|
||||
systemPrompt,
|
||||
userTags: userTags?.join ? userTags.join(', ') : userTags,
|
||||
product,
|
||||
priority: priority || ragResult.priority,
|
||||
date: date || ragResult.date,
|
||||
rules,
|
||||
history,
|
||||
model,
|
||||
language
|
||||
});
|
||||
res.json({ ...ragResult, llmResponse });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -53,10 +53,19 @@ router.get('/:id', async (req, res, next) => {
|
||||
router.post('/:id/columns', async (req, res, next) => {
|
||||
try {
|
||||
const tableId = req.params.id;
|
||||
const { name, type, options, order } = req.body;
|
||||
const { name, type, options, order, tagIds, purpose } = req.body;
|
||||
let finalOptions = options;
|
||||
// Собираем options
|
||||
finalOptions = finalOptions || {};
|
||||
if (type === 'tags' && Array.isArray(tagIds)) {
|
||||
finalOptions.tagIds = tagIds;
|
||||
}
|
||||
if (purpose) {
|
||||
finalOptions.purpose = purpose;
|
||||
}
|
||||
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]
|
||||
[tableId, name, type, finalOptions ? JSON.stringify(finalOptions) : null, order || 0]
|
||||
);
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
|
||||
@@ -52,30 +52,126 @@ router.put('/profile', requireAuth, async (req, res) => {
|
||||
});
|
||||
*/
|
||||
|
||||
// Получение списка пользователей с контактами
|
||||
router.get('/', async (req, res, next) => {
|
||||
// Получение списка пользователей с фильтрацией
|
||||
router.get('/', requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
const usersResult = await db.getQuery()('SELECT id, first_name, last_name, created_at, preferred_language FROM users ORDER BY id');
|
||||
const users = usersResult.rows;
|
||||
// Получаем все user_identities разом
|
||||
const identitiesResult = await db.getQuery()('SELECT user_id, provider, provider_id FROM user_identities');
|
||||
const identities = identitiesResult.rows;
|
||||
// Группируем идентификаторы по user_id
|
||||
const identityMap = {};
|
||||
for (const id of identities) {
|
||||
if (!identityMap[id.user_id]) identityMap[id.user_id] = {};
|
||||
if (!identityMap[id.user_id][id.provider]) identityMap[id.user_id][id.provider] = id.provider_id;
|
||||
const {
|
||||
tagIds = '',
|
||||
dateFrom = '',
|
||||
dateTo = '',
|
||||
contactType = 'all',
|
||||
search = '',
|
||||
newMessages = ''
|
||||
} = req.query;
|
||||
const adminId = req.user && req.user.id;
|
||||
|
||||
// --- Формируем условия ---
|
||||
const where = [];
|
||||
const params = [];
|
||||
let idx = 1;
|
||||
|
||||
// Фильтр по дате
|
||||
if (dateFrom) {
|
||||
where.push(`DATE(u.created_at) >= $${idx++}`);
|
||||
params.push(dateFrom);
|
||||
}
|
||||
// Собираем контакты
|
||||
if (dateTo) {
|
||||
where.push(`DATE(u.created_at) <= $${idx++}`);
|
||||
params.push(dateTo);
|
||||
}
|
||||
|
||||
// Фильтр по типу контакта
|
||||
if (contactType !== 'all') {
|
||||
where.push(`EXISTS (
|
||||
SELECT 1 FROM user_identities ui
|
||||
WHERE ui.user_id = u.id AND ui.provider = $${idx++}
|
||||
)`);
|
||||
params.push(contactType);
|
||||
}
|
||||
|
||||
// Фильтр по поиску
|
||||
if (search) {
|
||||
where.push(`(
|
||||
LOWER(u.first_name) LIKE $${idx} OR
|
||||
LOWER(u.last_name) LIKE $${idx} OR
|
||||
EXISTS (SELECT 1 FROM user_identities ui WHERE ui.user_id = u.id AND LOWER(ui.provider_id) LIKE $${idx})
|
||||
)`);
|
||||
params.push(`%${search.toLowerCase()}%`);
|
||||
idx++;
|
||||
}
|
||||
|
||||
// --- Основной SQL ---
|
||||
let sql = `
|
||||
SELECT u.id, u.first_name, u.last_name, u.created_at, u.preferred_language,
|
||||
(SELECT provider_id FROM user_identities WHERE user_id = u.id AND provider = 'email' LIMIT 1) AS email,
|
||||
(SELECT provider_id FROM user_identities WHERE user_id = u.id AND provider = 'telegram' LIMIT 1) AS telegram,
|
||||
(SELECT provider_id FROM user_identities WHERE user_id = u.id AND provider = 'wallet' LIMIT 1) AS wallet
|
||||
FROM users u
|
||||
`;
|
||||
|
||||
// Фильтрация по тегам
|
||||
if (tagIds) {
|
||||
const tagIdArr = tagIds.split(',').map(Number).filter(Boolean);
|
||||
if (tagIdArr.length > 0) {
|
||||
sql += `
|
||||
JOIN user_tags ut ON ut.user_id = u.id
|
||||
WHERE ut.tag_id = ANY($${idx++})
|
||||
GROUP BY u.id
|
||||
HAVING COUNT(DISTINCT ut.tag_id) = $${idx++}
|
||||
`;
|
||||
params.push(tagIdArr);
|
||||
params.push(tagIdArr.length);
|
||||
}
|
||||
} else if (where.length > 0) {
|
||||
sql += ` WHERE ${where.join(' AND ')} `;
|
||||
}
|
||||
|
||||
if (!tagIds) {
|
||||
sql += ' ORDER BY u.id ';
|
||||
}
|
||||
|
||||
// --- Выполняем запрос ---
|
||||
const usersResult = await db.getQuery()(sql, params);
|
||||
let users = usersResult.rows;
|
||||
|
||||
// --- Фильтрация по новым сообщениям ---
|
||||
if (newMessages === 'yes' && adminId) {
|
||||
// Получаем время последнего прочтения для каждого пользователя
|
||||
const readRes = await db.getQuery()(
|
||||
'SELECT user_id, last_read_at FROM admin_read_messages WHERE admin_id = $1',
|
||||
[adminId]
|
||||
);
|
||||
const readMap = {};
|
||||
for (const row of readRes.rows) {
|
||||
readMap[row.user_id] = row.last_read_at;
|
||||
}
|
||||
// Получаем последнее сообщение для каждого пользователя
|
||||
const msgRes = await db.getQuery()(
|
||||
`SELECT user_id, MAX(created_at) as last_msg_at FROM messages GROUP BY user_id`
|
||||
);
|
||||
const msgMap = {};
|
||||
for (const row of msgRes.rows) {
|
||||
msgMap[row.user_id] = row.last_msg_at;
|
||||
}
|
||||
// Оставляем только тех, у кого есть новые сообщения
|
||||
users = users.filter(u => {
|
||||
const lastRead = readMap[u.id];
|
||||
const lastMsg = msgMap[u.id];
|
||||
return lastMsg && (!lastRead || new Date(lastMsg) > new Date(lastRead));
|
||||
});
|
||||
}
|
||||
|
||||
// --- Формируем ответ ---
|
||||
const contacts = users.map(u => ({
|
||||
id: u.id,
|
||||
name: [u.first_name, u.last_name].filter(Boolean).join(' ') || null,
|
||||
email: identityMap[u.id]?.email || null,
|
||||
telegram: identityMap[u.id]?.telegram || null,
|
||||
wallet: identityMap[u.id]?.wallet || null,
|
||||
email: u.email || null,
|
||||
telegram: u.telegram || null,
|
||||
wallet: u.wallet || null,
|
||||
created_at: u.created_at,
|
||||
preferred_language: u.preferred_language || []
|
||||
}));
|
||||
|
||||
res.json({ success: true, contacts });
|
||||
} catch (error) {
|
||||
logger.error('Error fetching contacts:', error);
|
||||
|
||||
@@ -140,14 +140,30 @@ class EmailBotService {
|
||||
const html = parsed.html || '';
|
||||
// 1. Найти или создать пользователя
|
||||
const { userId, role } = await identityService.findOrCreateUserWithRole('email', fromEmail);
|
||||
// 2. Сохранить письмо и вложения в messages
|
||||
// 1.1 Найти или создать беседу
|
||||
let conversationResult = await db.getQuery()(
|
||||
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
|
||||
[userId]
|
||||
);
|
||||
let conversation;
|
||||
if (conversationResult.rows.length === 0) {
|
||||
const title = `Чат с пользователем ${userId}`;
|
||||
const newConv = await db.getQuery()(
|
||||
'INSERT INTO conversations (user_id, title, created_at, updated_at) VALUES ($1, $2, NOW(), NOW()) RETURNING *',
|
||||
[userId, title]
|
||||
);
|
||||
conversation = newConv.rows[0];
|
||||
} else {
|
||||
conversation = conversationResult.rows[0];
|
||||
}
|
||||
// 2. Сохранять все сообщения с conversation_id
|
||||
let hasAttachments = parsed.attachments && parsed.attachments.length > 0;
|
||||
if (hasAttachments) {
|
||||
for (const att of parsed.attachments) {
|
||||
await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NOW(), $7, $8, $9, $10, $11)`,
|
||||
[userId, 'user', text, 'email', role, 'in',
|
||||
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8, $9, $10, $11, $12)`,
|
||||
[userId, conversation.id, 'user', text, 'email', role, 'in',
|
||||
att.filename,
|
||||
att.contentType,
|
||||
att.size,
|
||||
@@ -158,18 +174,18 @@ class EmailBotService {
|
||||
}
|
||||
} else {
|
||||
await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, sender_type, content, channel, role, direction, created_at, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NOW(), $7)`,
|
||||
[userId, 'user', text, 'email', role, 'in', JSON.stringify({ subject, html })]
|
||||
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8)`,
|
||||
[userId, conversation.id, 'user', text, 'email', role, 'in', JSON.stringify({ subject, html })]
|
||||
);
|
||||
}
|
||||
// 3. Получить ответ от ИИ
|
||||
const aiResponse = await aiAssistant.getResponse(text, 'auto');
|
||||
// 4. Сохранить ответ в БД
|
||||
// 4. Сохранить ответ в БД с conversation_id
|
||||
await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, sender_type, content, channel, role, direction, created_at, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NOW(), $7)`,
|
||||
[userId, 'assistant', aiResponse, 'email', role, 'out', JSON.stringify({ subject, html })]
|
||||
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8)`,
|
||||
[userId, conversation.id, 'assistant', aiResponse, 'email', role, 'out', JSON.stringify({ subject, html })]
|
||||
);
|
||||
// 5. Отправить ответ на email
|
||||
await this.sendEmail(fromEmail, 'Re: ' + subject, aiResponse);
|
||||
|
||||
104
backend/services/ragService.js
Normal file
104
backend/services/ragService.js
Normal file
@@ -0,0 +1,104 @@
|
||||
const { OpenAIEmbeddings } = require('@langchain/openai');
|
||||
const { HNSWLib } = require('@langchain/community/vectorstores/hnswlib');
|
||||
const db = require('../db');
|
||||
const { ChatOllama } = require('@langchain/ollama');
|
||||
|
||||
async function getTableData(tableId) {
|
||||
const columns = (await db.getQuery()('SELECT * FROM user_columns WHERE table_id = $1', [tableId])).rows;
|
||||
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;
|
||||
|
||||
const getColId = purpose => columns.find(col => col.options?.purpose === purpose)?.id;
|
||||
const questionColId = getColId('question');
|
||||
const answerColId = getColId('answer');
|
||||
const userTagsColId = getColId('userTags');
|
||||
const contextColId = getColId('context');
|
||||
const productColId = getColId('product');
|
||||
const priorityColId = getColId('priority');
|
||||
const dateColId = getColId('date');
|
||||
|
||||
return rows.map(row => {
|
||||
const cells = cellValues.filter(cell => cell.row_id === row.id);
|
||||
return {
|
||||
id: row.id,
|
||||
question: cells.find(c => c.column_id === questionColId)?.value,
|
||||
answer: cells.find(c => c.column_id === answerColId)?.value,
|
||||
userTags: cells.find(c => c.column_id === userTagsColId)?.value,
|
||||
context: cells.find(c => c.column_id === contextColId)?.value,
|
||||
product: cells.find(c => c.column_id === productColId)?.value,
|
||||
priority: cells.find(c => c.column_id === priorityColId)?.value,
|
||||
date: cells.find(c => c.column_id === dateColId)?.value,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function ragAnswer({ tableId, userQuestion, userTags = [], product = null }) {
|
||||
const data = await getTableData(tableId);
|
||||
const questions = data.map(row => row.question);
|
||||
|
||||
// Получаем embedding для всех вопросов
|
||||
const embeddings = await new OpenAIEmbeddings().embedDocuments(questions);
|
||||
|
||||
// Создаём векторное хранилище
|
||||
const vectorStore = await HNSWLib.fromTexts(questions, data, new OpenAIEmbeddings());
|
||||
|
||||
// Получаем embedding для вопроса пользователя
|
||||
const [userEmbedding] = await new OpenAIEmbeddings().embedDocuments([userQuestion]);
|
||||
|
||||
// Ищем наиболее похожие вопросы (top-3)
|
||||
const results = await vectorStore.similaritySearchVectorWithScore(userEmbedding, 3);
|
||||
|
||||
// Фильтруем по тегам/продукту, если нужно
|
||||
let filtered = results.map(([row, score]) => ({ ...row, 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);
|
||||
}
|
||||
|
||||
// Берём лучший результат
|
||||
const best = filtered[0];
|
||||
|
||||
// Формируем ответ
|
||||
return {
|
||||
answer: best?.answer,
|
||||
context: best?.context,
|
||||
product: best?.product,
|
||||
priority: best?.priority,
|
||||
date: best?.date,
|
||||
score: best?.score,
|
||||
};
|
||||
}
|
||||
|
||||
async function generateLLMResponse({ userQuestion, context, clarifyingAnswer, objectionAnswer, answer, systemPrompt, userTags, product, priority, date, rules, history, model, language }) {
|
||||
// Подставляем значения в шаблон промта
|
||||
let prompt = (systemPrompt || '')
|
||||
.replace('{context}', context || '')
|
||||
.replace('{clarifyingAnswer}', clarifyingAnswer || '')
|
||||
.replace('{objectionAnswer}', objectionAnswer || '')
|
||||
.replace('{answer}', answer || '')
|
||||
.replace('{question}', userQuestion || '')
|
||||
.replace('{userTags}', userTags || '')
|
||||
.replace('{product}', product || '')
|
||||
.replace('{priority}', priority || '')
|
||||
.replace('{date}', date || '')
|
||||
.replace('{rules}', rules || '')
|
||||
.replace('{history}', history || '')
|
||||
.replace('{model}', model || '')
|
||||
.replace('{language}', language || '');
|
||||
|
||||
const chat = new ChatOllama({
|
||||
baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
|
||||
model: process.env.OLLAMA_MODEL || 'qwen2.5',
|
||||
system: prompt,
|
||||
temperature: 0.7,
|
||||
maxTokens: 1000,
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
const response = await chat.invoke(`Вопрос пользователя: ${userQuestion}`);
|
||||
return response.content;
|
||||
}
|
||||
|
||||
module.exports = { ragAnswer, generateLLMResponse };
|
||||
@@ -267,8 +267,23 @@ async function getBot() {
|
||||
const telegramId = ctx.from.id.toString();
|
||||
// 1. Найти или создать пользователя
|
||||
const { userId, role } = await identityService.findOrCreateUserWithRole('telegram', telegramId);
|
||||
|
||||
// 2. Сохранить входящее сообщение в messages
|
||||
// 1.1 Найти или создать беседу
|
||||
let conversationResult = await db.getQuery()(
|
||||
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
|
||||
[userId]
|
||||
);
|
||||
let conversation;
|
||||
if (conversationResult.rows.length === 0) {
|
||||
const title = `Чат с пользователем ${userId}`;
|
||||
const newConv = await db.getQuery()(
|
||||
'INSERT INTO conversations (user_id, title, created_at, updated_at) VALUES ($1, $2, NOW(), NOW()) RETURNING *',
|
||||
[userId, title]
|
||||
);
|
||||
conversation = newConv.rows[0];
|
||||
} else {
|
||||
conversation = conversationResult.rows[0];
|
||||
}
|
||||
// 2. Сохранять все сообщения с conversation_id
|
||||
let content = text;
|
||||
let attachmentMeta = {};
|
||||
// Проверяем вложения (фото, документ, аудио, видео)
|
||||
@@ -310,9 +325,9 @@ async function getBot() {
|
||||
}
|
||||
// Сохраняем сообщение в БД
|
||||
await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NOW(), $7, $8, $9, $10)`,
|
||||
[userId, 'user', content, 'telegram', role, 'in',
|
||||
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8, $9, $10, $11)`,
|
||||
[userId, conversation.id, 'user', content, 'telegram', role, 'in',
|
||||
attachmentMeta.attachment_filename || null,
|
||||
attachmentMeta.attachment_mimetype || null,
|
||||
attachmentMeta.attachment_size || null,
|
||||
@@ -322,11 +337,11 @@ async function getBot() {
|
||||
|
||||
// 3. Получить ответ от ИИ
|
||||
const aiResponse = await aiAssistant.getResponse(content, 'auto');
|
||||
// 4. Сохранить ответ в БД
|
||||
// 4. Сохранить ответ в БД с conversation_id
|
||||
await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, sender_type, content, channel, role, direction, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NOW())`,
|
||||
[userId, 'assistant', aiResponse, 'telegram', role, 'out']
|
||||
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())`,
|
||||
[userId, conversation.id, 'assistant', aiResponse, 'telegram', role, 'out']
|
||||
);
|
||||
// 5. Отправить ответ пользователю
|
||||
await ctx.reply(aiResponse);
|
||||
|
||||
Reference in New Issue
Block a user