ваше сообщение коммита
This commit is contained in:
@@ -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