diff --git a/backend/db/migrations/038_add_embedding_model_to_ai_providers_settings.sql b/backend/db/migrations/038_add_embedding_model_to_ai_providers_settings.sql
new file mode 100644
index 0000000..9bf47a2
--- /dev/null
+++ b/backend/db/migrations/038_add_embedding_model_to_ai_providers_settings.sql
@@ -0,0 +1 @@
+ALTER TABLE ai_providers_settings ADD COLUMN embedding_model VARCHAR(128);
\ No newline at end of file
diff --git a/backend/db/migrations/040_add_embedding_model_to_ai_assistant_settings.sql b/backend/db/migrations/040_add_embedding_model_to_ai_assistant_settings.sql
new file mode 100644
index 0000000..d05af78
--- /dev/null
+++ b/backend/db/migrations/040_add_embedding_model_to_ai_assistant_settings.sql
@@ -0,0 +1 @@
+ALTER TABLE ai_assistant_settings ADD COLUMN embedding_model VARCHAR(128);
\ No newline at end of file
diff --git a/backend/routes/chat.js b/backend/routes/chat.js
index fa95f42..c4e2de0 100644
--- a/backend/routes/chat.js
+++ b/backend/routes/chat.js
@@ -423,9 +423,8 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
const userMessage = userMessageResult.rows[0];
logger.info('User message saved', { messageId: userMessage.id, conversationId });
- // Получаем ответ от ИИ, только если это было текстовое сообщение
+ // --- Новая логика автоответа ИИ по RAG ---
let aiMessage = null;
- // --- Новая логика автоответа ИИ ---
let shouldGenerateAiReply = true;
if (senderType === 'admin') {
// Если админ пишет не себе, не отвечаем
@@ -441,41 +440,70 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
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,
- history,
- aiSettings ? aiSettings.system_prompt : '',
- rules ? rules.rules : null
- );
- logger.info('AI response received' + (aiResponseContent ? '' : ' (empty)'), { conversationId });
-
- if (aiResponseContent) {
- const aiMessageResult = await db.getQuery()(
- `INSERT INTO messages
- (conversation_id, user_id, content, sender_type, role, channel)
- VALUES ($1, $2, $3, 'assistant', 'assistant', 'web')
- RETURNING *`,
- [conversationId, userId, aiResponseContent]
- );
- aiMessage = aiMessageResult.rows[0];
- logger.info('AI response saved', { messageId: aiMessage.id, conversationId });
+ // --- RAG автоответ ---
+ let ragTableId = null;
+ if (aiSettings && aiSettings.selected_rag_tables) {
+ ragTableId = Array.isArray(aiSettings.selected_rag_tables)
+ ? aiSettings.selected_rag_tables[0]
+ : aiSettings.selected_rag_tables;
}
+ let ragResult = null;
+ if (ragTableId) {
+ const { ragAnswer, generateLLMResponse } = require('../services/ragService');
+ const threshold = 0.3;
+ logger.info(`[RAG] Запуск поиска по RAG: tableId=${ragTableId}, вопрос="${messageContent}", threshold=${threshold}`);
+ const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: messageContent, threshold });
+ logger.info(`[RAG] Результат поиска по RAG:`, ragResult);
+ if (ragResult && ragResult.answer && ragResult.score && ragResult.score > threshold) {
+ logger.info(`[RAG] Найден confident-ответ (score=${ragResult.score}), отправляем ответ из базы.`);
+ // Прямой ответ из RAG
+ const aiMessageResult = await db.getQuery()(
+ `INSERT INTO messages
+ (conversation_id, user_id, content, sender_type, role, channel)
+ VALUES ($1, $2, $3, 'assistant', 'assistant', 'web')
+ RETURNING *`,
+ [conversationId, userId, ragResult.answer]
+ );
+ aiMessage = aiMessageResult.rows[0];
+ } else if (ragResult) {
+ logger.info(`[RAG] Нет confident-ответа (score=${ragResult.score}), переходим к генерации через LLM.`);
+ // Генерация через LLM с подстановкой значений из RAG
+ 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 llmResponse = await generateLLMResponse({
+ userQuestion: messageContent,
+ context: ragResult.context,
+ answer: ragResult.answer,
+ clarifyingAnswer: ragResult.clarifyingAnswer,
+ objectionAnswer: ragResult.objectionAnswer,
+ systemPrompt: aiSettings ? aiSettings.system_prompt : '',
+ history,
+ model: aiSettings ? aiSettings.model : undefined,
+ language: aiSettings && aiSettings.languages && aiSettings.languages.length > 0 ? aiSettings.languages[0] : 'ru'
+ });
+ if (llmResponse) {
+ const aiMessageResult = await db.getQuery()(
+ `INSERT INTO messages
+ (conversation_id, user_id, content, sender_type, role, channel)
+ VALUES ($1, $2, $3, 'assistant', 'assistant', 'web')
+ RETURNING *`,
+ [conversationId, userId, llmResponse]
+ );
+ aiMessage = aiMessageResult.rows[0];
+ } else {
+ logger.info(`[RAG] Нет ни одного результата, прошедшего порог (${threshold}).`);
+ }
+ }
+ }
+ // --- конец RAG автоответа ---
} catch (aiError) {
- logger.error('Error getting or saving AI response:', aiError);
+ logger.error('Error getting or saving AI response (RAG):', aiError);
// Не прерываем основной ответ, но логируем ошибку
}
}
@@ -702,14 +730,32 @@ router.post('/ai-draft', requireAuth, async (req, res) => {
role: msg.sender_type === 'user' ? 'user' : 'assistant',
content: msg.content
}));
+ // --- RAG draft ---
+ let ragTableId = null;
+ if (aiSettings && aiSettings.selected_rag_tables) {
+ ragTableId = Array.isArray(aiSettings.selected_rag_tables)
+ ? aiSettings.selected_rag_tables[0]
+ : aiSettings.selected_rag_tables;
+ }
+ let ragResult = null;
+ if (ragTableId) {
+ const { ragAnswer } = require('../services/ragService');
+ logger.info(`[RAG] [DRAFT] Запуск поиска по RAG: tableId=${ragTableId}, draft prompt="${promptText}"`);
+ ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: promptText });
+ logger.info(`[RAG] [DRAFT] Результат поиска по RAG:`, ragResult);
+ }
+ const { generateLLMResponse } = require('../services/ragService');
const detectedLanguage = language === 'auto' ? aiAssistant.detectLanguage(promptText) : language;
- const aiResponseContent = await aiAssistant.getResponse(
- promptText,
- detectedLanguage,
+ const aiResponseContent = await generateLLMResponse({
+ userQuestion: promptText,
+ context: ragResult && ragResult.context ? ragResult.context : '',
+ answer: ragResult && ragResult.answer ? ragResult.answer : '',
+ systemPrompt: aiSettings ? aiSettings.system_prompt : '',
history,
- aiSettings ? aiSettings.system_prompt : '',
- rules ? rules.rules : null
- );
+ model: aiSettings ? aiSettings.model : undefined,
+ language: aiSettings && aiSettings.languages && aiSettings.languages.length > 0 ? aiSettings.languages[0] : 'ru',
+ rules: rules ? rules.rules : null
+ });
res.json({ success: true, aiMessage: aiResponseContent });
} catch (error) {
logger.error('Error generating AI draft:', error);
diff --git a/backend/routes/settings.js b/backend/routes/settings.js
index b9d4ce3..e8dabc7 100644
--- a/backend/routes/settings.js
+++ b/backend/routes/settings.js
@@ -10,6 +10,10 @@ const aiAssistant = require('../services/ai-assistant');
const dns = require('node:dns').promises;
const aiAssistantSettingsService = require('../services/aiAssistantSettingsService');
const aiAssistantRulesService = require('../services/aiAssistantRulesService');
+const telegramBot = require('../services/telegramBot');
+const EmailBotService = require('../services/emailBot');
+const emailBotService = new EmailBotService();
+const dbSettingsService = require('../services/dbSettingsService');
// Логируем версию ethers для отладки
logger.info(`Ethers version: ${ethers.version || 'unknown'}`);
@@ -185,8 +189,8 @@ router.get('/ai-settings/:provider', requireAdmin, async (req, res, next) => {
router.put('/ai-settings/:provider', requireAdmin, async (req, res, next) => {
try {
const { provider } = req.params;
- const { api_key, base_url, selected_model } = req.body;
- const updated = await aiProviderSettingsService.upsertProviderSettings({ provider, api_key, base_url, selected_model });
+ const { api_key, base_url, selected_model, embedding_model } = req.body;
+ const updated = await aiProviderSettingsService.upsertProviderSettings({ provider, api_key, base_url, selected_model, embedding_model });
res.json({ success: true, settings: updated });
} catch (error) {
logger.error('Ошибка при сохранении AI-настроек:', error);
@@ -252,7 +256,25 @@ router.get('/ai-assistant', requireAdmin, async (req, res, next) => {
router.put('/ai-assistant', requireAdmin, async (req, res, next) => {
try {
- const updated = await aiAssistantSettingsService.upsertSettings({ ...req.body, updated_by: req.session.userId || null });
+ let { selected_rag_tables, ...rest } = req.body;
+ // Приведение к массиву чисел
+ if (typeof selected_rag_tables === 'string') {
+ try {
+ selected_rag_tables = JSON.parse(selected_rag_tables);
+ } catch {
+ selected_rag_tables = [Number(selected_rag_tables)];
+ }
+ }
+ if (!Array.isArray(selected_rag_tables)) {
+ selected_rag_tables = [Number(selected_rag_tables)];
+ }
+ selected_rag_tables = selected_rag_tables.map(Number);
+
+ const updated = await aiAssistantSettingsService.upsertSettings({
+ ...rest,
+ selected_rag_tables,
+ updated_by: req.session.userId || null
+ });
res.json({ success: true, settings: updated });
} catch (error) {
next(error);
@@ -309,23 +331,101 @@ router.delete('/ai-assistant-rules/:id', requireAdmin, async (req, res, next) =>
}
});
-// Получить все email_settings для выпадающего списка
-router.get('/email-settings', requireAdmin, async (req, res, next) => {
+// Получить текущие настройки Email (для страницы Email)
+router.get('/email-settings', requireAdmin, async (req, res) => {
try {
- const { rows } = await require('../db').getQuery()('SELECT id, from_email FROM email_settings ORDER BY id');
- res.json({ success: true, items: rows });
+ const settings = await emailBotService.getSettingsFromDb();
+ res.json({ success: true, settings });
+ } catch (error) {
+ res.status(404).json({ success: false, error: error.message });
+ }
+});
+
+// Получить список всех email (для ассистента)
+router.get('/email-settings/list', requireAdmin, async (req, res) => {
+ try {
+ const emails = await emailBotService.getAllEmailSettings();
+ res.json({ success: true, items: emails });
+ } catch (error) {
+ res.status(404).json({ success: false, error: error.message });
+ }
+});
+
+// Получить текущие настройки Telegram-бота (для страницы Telegram)
+router.get('/telegram-settings', requireAdmin, async (req, res, next) => {
+ try {
+ const settings = await telegramBot.getTelegramSettings();
+ res.json({ success: true, settings });
+ } catch (error) {
+ res.status(404).json({ success: false, error: error.message });
+ }
+});
+
+// Получить список всех Telegram-ботов (для ассистента)
+router.get('/telegram-settings/list', requireAdmin, async (req, res, next) => {
+ try {
+ const bots = await telegramBot.getAllBots();
+ res.json({ success: true, items: bots });
+ } catch (error) {
+ res.status(404).json({ success: false, error: error.message });
+ }
+});
+
+// Получение списка моделей для выбранного AI-провайдера
+router.get('/ai-provider-models', requireAdmin, async (req, res, next) => {
+ try {
+ const provider = req.query.provider;
+ if (!provider) return res.status(400).json({ error: 'provider is required' });
+ const settings = await aiProviderSettingsService.getProviderSettings(provider);
+ if (!settings) return res.status(404).json({ error: 'Provider not found' });
+ const models = await aiProviderSettingsService.getProviderModels(provider, {
+ api_key: settings.api_key,
+ base_url: settings.base_url,
+ });
+ res.json({ models });
} catch (error) {
next(error);
}
});
-// Получить все telegram_settings для выпадающего списка
-router.get('/telegram-settings', requireAdmin, async (req, res, next) => {
+// Получить настройки базы данных
+router.get('/db-settings', requireAdmin, async (req, res) => {
try {
- const { rows } = await require('../db').getQuery()('SELECT id, bot_username FROM telegram_settings ORDER BY id');
- res.json({ success: true, items: rows });
+ const settings = await dbSettingsService.getSettings();
+ res.json({ success: true, settings });
} catch (error) {
- next(error);
+ res.status(404).json({ success: false, error: error.message });
+ }
+});
+
+// Обновить настройки базы данных
+router.put('/db-settings', requireAdmin, async (req, res) => {
+ try {
+ const { db_host, db_port, db_name, db_user, db_password } = req.body;
+ const updated = await dbSettingsService.upsertSettings({ db_host, db_port, db_name, db_user, db_password });
+ res.json({ success: true, settings: updated });
+ } catch (error) {
+ res.status(500).json({ success: false, error: error.message });
+ }
+});
+
+// Получить все LLM-модели
+router.get('/llm-models', requireAdmin, async (req, res) => {
+ try {
+ const models = await aiProviderSettingsService.getAllLLMModels();
+ res.json({ success: true, models });
+ } catch (error) {
+ res.status(500).json({ success: false, error: error.message });
+ }
+});
+
+// Получить все embedding-модели
+router.get('/embedding-models', requireAdmin, async (req, res) => {
+ try {
+ const models = await aiProviderSettingsService.getAllEmbeddingModels();
+ res.json({ success: true, models });
+ } catch (error) {
+ res.status(500).json({ success: false, error: error.message });
}
});
diff --git a/backend/routes/tables.js b/backend/routes/tables.js
index 452e309..452ccd9 100644
--- a/backend/routes/tables.js
+++ b/backend/routes/tables.js
@@ -54,9 +54,7 @@ router.post('/:id/columns', async (req, res, next) => {
try {
const tableId = req.params.id;
const { name, type, options, order, tagIds, purpose } = req.body;
- let finalOptions = options;
- // Собираем options
- finalOptions = finalOptions || {};
+ let finalOptions = options || {};
if (type === 'tags' && Array.isArray(tagIds)) {
finalOptions.tagIds = tagIds;
}
diff --git a/backend/services/aiAssistantSettingsService.js b/backend/services/aiAssistantSettingsService.js
index cf33d82..0501e88 100644
--- a/backend/services/aiAssistantSettingsService.js
+++ b/backend/services/aiAssistantSettingsService.js
@@ -20,19 +20,21 @@ async function getSettings() {
return {
...settings,
telegramBot,
- supportEmail
+ supportEmail,
+ embedding_model: settings.embedding_model
};
}
-async function upsertSettings({ system_prompt, selected_rag_tables, languages, model, rules, updated_by, telegram_settings_id, email_settings_id, system_message }) {
+async function upsertSettings({ system_prompt, selected_rag_tables, languages, model, embedding_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)
+ `INSERT INTO ${TABLE} (id, system_prompt, selected_rag_tables, languages, model, embedding_model, rules, updated_at, updated_by, telegram_settings_id, email_settings_id, system_message)
+ VALUES (1, $1, $2, $3, $4, $5, $6, NOW(), $7, $8, $9, $10)
ON CONFLICT (id) DO UPDATE SET
system_prompt = EXCLUDED.system_prompt,
selected_rag_tables = EXCLUDED.selected_rag_tables,
languages = EXCLUDED.languages,
model = EXCLUDED.model,
+ embedding_model = EXCLUDED.embedding_model,
rules = EXCLUDED.rules,
updated_at = NOW(),
updated_by = EXCLUDED.updated_by,
@@ -40,7 +42,7 @@ async function upsertSettings({ system_prompt, selected_rag_tables, languages, m
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]
+ [system_prompt, selected_rag_tables, languages, model, embedding_model, rules, updated_by, telegram_settings_id, email_settings_id, system_message]
);
return rows[0];
}
diff --git a/backend/services/aiProviderSettingsService.js b/backend/services/aiProviderSettingsService.js
index a1d0df4..05fcb1b 100644
--- a/backend/services/aiProviderSettingsService.js
+++ b/backend/services/aiProviderSettingsService.js
@@ -12,17 +12,18 @@ async function getProviderSettings(provider) {
return rows[0] || null;
}
-async function upsertProviderSettings({ provider, api_key, base_url, selected_model }) {
+async function upsertProviderSettings({ provider, api_key, base_url, selected_model, embedding_model }) {
const { rows } = await db.getQuery()(
- `INSERT INTO ${TABLE} (provider, api_key, base_url, selected_model, updated_at)
- VALUES ($1, $2, $3, $4, NOW())
+ `INSERT INTO ${TABLE} (provider, api_key, base_url, selected_model, embedding_model, updated_at)
+ VALUES ($1, $2, $3, $4, $5, NOW())
ON CONFLICT (provider) DO UPDATE SET
api_key = EXCLUDED.api_key,
base_url = EXCLUDED.base_url,
selected_model = EXCLUDED.selected_model,
+ embedding_model = EXCLUDED.embedding_model,
updated_at = NOW()
RETURNING *`,
- [provider, api_key, base_url, selected_model]
+ [provider, api_key, base_url, selected_model, embedding_model]
);
return rows[0];
}
@@ -97,10 +98,28 @@ async function verifyProviderKey(provider, { api_key, base_url } = {}) {
}
}
+async function getAllLLMModels() {
+ const { rows } = await db.getQuery()(
+ `SELECT provider, selected_model FROM ${TABLE} WHERE selected_model IS NOT NULL AND selected_model <> ''`
+ );
+ // Возвращаем массив объектов { id, provider }
+ return rows.map(r => ({ id: r.selected_model, provider: r.provider }));
+}
+
+async function getAllEmbeddingModels() {
+ const { rows } = await db.getQuery()(
+ `SELECT provider, embedding_model FROM ${TABLE} WHERE embedding_model IS NOT NULL AND embedding_model <> ''`
+ );
+ // Возвращаем массив объектов { id, provider }
+ return rows.map(r => ({ id: r.embedding_model, provider: r.provider }));
+}
+
module.exports = {
getProviderSettings,
upsertProviderSettings,
deleteProviderSettings,
getProviderModels,
verifyProviderKey,
+ getAllLLMModels,
+ getAllEmbeddingModels,
};
\ No newline at end of file
diff --git a/backend/services/dbSettingsService.js b/backend/services/dbSettingsService.js
new file mode 100644
index 0000000..085a50f
--- /dev/null
+++ b/backend/services/dbSettingsService.js
@@ -0,0 +1,27 @@
+const db = require('../db');
+
+class DbSettingsService {
+ async getSettings() {
+ const { rows } = await db.getQuery()('SELECT * FROM db_settings WHERE id = 1');
+ return rows[0];
+ }
+
+ async upsertSettings({ db_host, db_port, db_name, db_user, db_password }) {
+ const { rows } = await db.getQuery()(
+ `INSERT INTO db_settings (id, db_host, db_port, db_name, db_user, db_password, updated_at)
+ VALUES (1, $1, $2, $3, $4, $5, NOW())
+ ON CONFLICT (id) DO UPDATE SET
+ db_host = EXCLUDED.db_host,
+ db_port = EXCLUDED.db_port,
+ db_name = EXCLUDED.db_name,
+ db_user = EXCLUDED.db_user,
+ db_password = EXCLUDED.db_password,
+ updated_at = NOW()
+ RETURNING *`,
+ [db_host, db_port, db_name, db_user, db_password]
+ );
+ return rows[0];
+ }
+}
+
+module.exports = new DbSettingsService();
\ No newline at end of file
diff --git a/backend/services/emailBot.js b/backend/services/emailBot.js
index ce3e209..13782de 100644
--- a/backend/services/emailBot.js
+++ b/backend/services/emailBot.js
@@ -309,6 +309,11 @@ class EmailBotService {
throw err;
}
}
+
+ async getAllEmailSettings() {
+ const { rows } = await db.getQuery()('SELECT id, from_email FROM email_settings ORDER BY id');
+ return rows;
+ }
}
console.log('[EmailBot] module.exports = EmailBotService');
diff --git a/backend/services/ragService.js b/backend/services/ragService.js
index 0efa196..f65fda2 100644
--- a/backend/services/ragService.js
+++ b/backend/services/ragService.js
@@ -1,10 +1,15 @@
-const { OpenAIEmbeddings } = require('@langchain/openai');
const { HNSWLib } = require('@langchain/community/vectorstores/hnswlib');
const db = require('../db');
const { ChatOllama } = require('@langchain/ollama');
+const { OllamaEmbeddings } = require('@langchain/ollama');
+const { getProviderSettings } = require('./aiProviderSettingsService');
+const { OpenAIEmbeddings } = require('@langchain/openai');
+
+console.log('[RAG] ragService.js loaded');
async function getTableData(tableId) {
const columns = (await db.getQuery()('SELECT * FROM user_columns WHERE table_id = $1', [tableId])).rows;
+ console.log('RAG getTableData: columns:', columns);
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;
@@ -17,7 +22,7 @@ async function getTableData(tableId) {
const priorityColId = getColId('priority');
const dateColId = getColId('date');
- return rows.map(row => {
+ const data = rows.map(row => {
const cells = cellValues.filter(cell => cell.row_id === row.id);
return {
id: row.id,
@@ -30,35 +35,107 @@ async function getTableData(tableId) {
date: cells.find(c => c.column_id === dateColId)?.value,
};
});
+ const questions = data.map(row => row.question);
+ console.log('RAG getTableData: questions:', questions);
+ if (!questions.length) {
+ console.warn('RAG getTableData: questions array is empty! Проверьте структуру колонок и наличие данных.');
+ }
+ return data;
}
-async function ragAnswer({ tableId, userQuestion, userTags = [], product = null }) {
+async function getEmbeddingsProvider(providerName = 'ollama') {
+ const settings = await getProviderSettings(providerName);
+ if (!settings) throw new Error('Embeddings provider settings not found');
+ switch (providerName) {
+ case 'openai':
+ return new OpenAIEmbeddings({
+ apiKey: settings.api_key,
+ baseURL: settings.base_url,
+ model: settings.selected_model || undefined,
+ });
+ case 'ollama': {
+ // Fallback: если не задан base_url, пробуем env, host.docker.internal, localhost
+ let baseUrl = settings.base_url;
+ if (!baseUrl) {
+ baseUrl = process.env.OLLAMA_BASE_URL;
+ }
+ if (!baseUrl) {
+ // Если в Docker — используем host.docker.internal
+ baseUrl = 'http://host.docker.internal:11434';
+ }
+ // Если всё равно нет — последний fallback
+ if (!baseUrl) {
+ baseUrl = 'http://localhost:11434';
+ }
+ return new OllamaEmbeddings({
+ model: settings.embedding_model || process.env.OLLAMA_EMBED_MODEL || 'mxbai-embed-large',
+ baseUrl,
+ });
+ }
+ // case 'gemini':
+ // return new GeminiEmbeddings({ apiKey: settings.api_key });
+ // Добавьте другие провайдеры по аналогии
+ default:
+ throw new Error('Unknown embeddings provider: ' + providerName);
+ }
+}
+
+async function ragAnswer({ tableId, userQuestion, userTags = [], product = null, embeddingsProvider = 'ollama', threshold = 0.3 }) {
+ console.log('[RAG] Используется провайдер эмбеддингов:', embeddingsProvider);
const data = await getTableData(tableId);
- const questions = data.map(row => row.question);
+ // Триммируем вопросы для чистоты сравнения
+ const questions = data.map(row => row.question && typeof row.question === 'string' ? row.question.trim() : row.question);
+
+ // Получаем embeddings-инстанс динамически
+ const embeddingsInstance = await getEmbeddingsProvider(embeddingsProvider);
// Получаем embedding для всех вопросов
- const embeddings = await new OpenAIEmbeddings().embedDocuments(questions);
+ const embeddings = await embeddingsInstance.embedDocuments(questions);
+ console.log('Questions embedding length:', embeddings[0]?.length, 'Total questions:', questions.length);
+
+ // Получаем embedding для вопроса пользователя (trim)
+ const userQuestionTrimmed = userQuestion && typeof userQuestion === 'string' ? userQuestion.trim() : userQuestion;
+ const [userEmbedding] = await embeddingsInstance.embedDocuments([userQuestionTrimmed]);
+ console.log('User embedding length:', userEmbedding?.length, 'User question:', userQuestionTrimmed);
+
+ // Явно сравниваем embeddings (отладка)
+ console.log('[RAG] Embedding сравнение:');
+ embeddings.forEach((emb, idx) => {
+ const dot = emb.reduce((sum, v, i) => sum + v * userEmbedding[i], 0);
+ console.log(` [${idx}] dot-product: ${dot} | question: "${questions[idx]}"`);
+ });
+
+ // Создаём массив метаданных для каждого вопроса
+ const metadatas = data.map(row => ({
+ id: row.id,
+ answer: row.answer,
+ userTags: row.userTags,
+ context: row.context,
+ product: row.product,
+ priority: row.priority,
+ date: row.date,
+ }));
// Создаём векторное хранилище
- const vectorStore = await HNSWLib.fromTexts(questions, data, new OpenAIEmbeddings());
-
- // Получаем embedding для вопроса пользователя
- const [userEmbedding] = await new OpenAIEmbeddings().embedDocuments([userQuestion]);
+ const vectorStore = await HNSWLib.fromTexts(questions, metadatas, embeddingsInstance);
// Ищем наиболее похожие вопросы (top-3)
const results = await vectorStore.similaritySearchVectorWithScore(userEmbedding, 3);
+ console.log('[RAG] Результаты поиска по векторам (score):', results.map(([doc, score]) => ({ ...doc.metadata, score })));
// Фильтруем по тегам/продукту, если нужно
- let filtered = results.map(([row, score]) => ({ ...row, score }));
+ let filtered = results.map(([doc, score]) => ({ ...doc.metadata, 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);
}
+ console.log('[RAG] Отфильтрованные результаты:', filtered);
- // Берём лучший результат
- const best = filtered[0];
+ // Берём лучший результат с учётом порога
+ const best = filtered.find(row => row.score >= threshold);
+ console.log(`[RAG] Выбранный ответ (порог ${threshold}):`, best);
// Формируем ответ
return {
diff --git a/backend/services/telegramBot.js b/backend/services/telegramBot.js
index 0ca7723..b404d4e 100644
--- a/backend/services/telegramBot.js
+++ b/backend/services/telegramBot.js
@@ -424,9 +424,16 @@ function clearSettingsCache() {
telegramSettingsCache = null;
}
+async function getAllBots() {
+ const { rows } = await db.getQuery()('SELECT id, bot_username FROM telegram_settings ORDER BY id');
+ return rows;
+}
+
module.exports = {
+ getTelegramSettings,
getBot,
stopBot,
initTelegramAuth,
clearSettingsCache,
+ getAllBots,
};
diff --git a/backend/yarn.lock b/backend/yarn.lock
index ee9f01d..76d8d44 100644
--- a/backend/yarn.lock
+++ b/backend/yarn.lock
@@ -110,10 +110,10 @@
dependencies:
"@types/json-schema" "^7.0.15"
-"@eslint/core@^0.15.0":
- version "0.15.0"
- resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.15.0.tgz#8fc04709a7b9a179d9f7d93068fc000cb8c5603d"
- integrity sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==
+"@eslint/core@^0.15.1":
+ version "0.15.1"
+ resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.15.1.tgz#d530d44209cbfe2f82ef86d6ba08760196dd3b60"
+ integrity sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==
dependencies:
"@types/json-schema" "^7.0.15"
@@ -143,11 +143,11 @@
integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==
"@eslint/plugin-kit@^0.3.1":
- version "0.3.2"
- resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.2.tgz#0cad96b134d23a653348e3342f485636b5ef4732"
- integrity sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz#32926b59bd407d58d817941e48b2a7049359b1fd"
+ integrity sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==
dependencies:
- "@eslint/core" "^0.15.0"
+ "@eslint/core" "^0.15.1"
levn "^0.4.1"
"@ethereumjs/rlp@^4.0.1":
@@ -367,9 +367,9 @@
integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==
"@google/genai@^1.0.1":
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/@google/genai/-/genai-1.6.0.tgz#7a14d505faebe17957b272476debd574d2eae1e0"
- integrity sha512-0vn8wMGesjiEsHeFsl10T8+SFqLj7q+RSE6mml66sE+jwI7U9wW2LQ3qYtwUEaI+P8ZYeEYE5IpYmNLcRQUBPQ==
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/@google/genai/-/genai-1.7.0.tgz#4af2fe86343e990eebfb4adcd0fa744cb0e6907e"
+ integrity sha512-s/OZLkrIfBwc+SFFaZoKdEogkw4in0YRTGc4Q483jnfchNBWzrNe560eZEfGJHQRPn6YfzJgECCx0sqEOMWvYw==
dependencies:
google-auth-library "^9.14.2"
ws "^8.18.0"
@@ -6056,9 +6056,9 @@ simple-update-notifier@^2.0.0:
semver "^7.5.3"
simple-wcswidth@^1.0.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/simple-wcswidth/-/simple-wcswidth-1.1.1.tgz#a96ff1b5cff660262ea33850e19a0e7249caed50"
- integrity sha512-R3q3/eoeNBp24CNTASEUrffXi0j9TwPIEvSStlvSrsFimM17sV5EHcMOc86j3K+UWZyLYvH0hRmYGCpCoaJ4vw==
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz#66722f37629d5203f9b47c5477b1225b85d6525b"
+ integrity sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==
sisteransi@^1.0.5:
version "1.0.5"
@@ -7219,9 +7219,9 @@ zip-stream@^6.0.1:
readable-stream "^4.0.0"
zod-to-json-schema@^3.22.3, zod-to-json-schema@^3.22.4:
- version "3.24.5"
- resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz#d1095440b147fb7c2093812a53c54df8d5df50a3"
- integrity sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==
+ version "3.24.6"
+ resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz#5920f020c4d2647edfbb954fa036082b92c9e12d"
+ integrity sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==
zod@^3.22.4, zod@^3.25.32:
version "3.25.67"
diff --git a/docker-compose.yml b/docker-compose.yml
index 3e0bf22..ba8dc06 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -43,7 +43,6 @@ services:
condition: service_started
volumes:
- ./backend:/app
- - backend_node_modules:/app/node_modules
- ./frontend/dist:/app/frontend_dist:ro
environment:
- NODE_ENV=${NODE_ENV:-development}
@@ -60,6 +59,8 @@ services:
- FRONTEND_URL=http://localhost:5173
ports:
- "8000:8000"
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
# command: sh -c "yarn run dev" # Временно комментируем эту строку
# command: nodemon server.js # Запускаем через nodemon
diff --git a/frontend/src/components/tables/UserTableView.vue b/frontend/src/components/tables/UserTableView.vue
index b7780a5..eb62dbc 100644
--- a/frontend/src/components/tables/UserTableView.vue
+++ b/frontend/src/components/tables/UserTableView.vue
@@ -129,11 +129,16 @@ function closeAddColModal() {
async function handleAddColumn() {
if (!newColName.value) return;
const data = { name: newColName.value, type: newColType.value };
+ const options = {};
if (newColType.value === 'tags') {
data.tagIds = selectedTagIds.value;
+ options.tagIds = selectedTagIds.value;
}
if (newColPurpose.value) {
- data.purpose = newColPurpose.value;
+ options.purpose = newColPurpose.value;
+ }
+ if (Object.keys(options).length > 0) {
+ data.options = options;
}
await tablesService.addColumn(props.tableId, data);
closeAddColModal();
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
index 1803db8..8f1f0a4 100644
--- a/frontend/src/router/index.js
+++ b/frontend/src/router/index.js
@@ -27,6 +27,11 @@ const routes = [
component: () => import('../views/SettingsView.vue'),
// Добавляем дочерние маршруты
children: [
+ {
+ path: '',
+ name: 'settings-index',
+ component: () => import('@/views/settings/SettingsIndexView.vue'),
+ },
{
path: 'ai',
name: 'settings-ai',
@@ -50,21 +55,35 @@ const routes = [
{
path: 'telegram',
name: 'settings-telegram',
- component: () => import('../views/settings/TelegramSettingsView.vue'),
+ component: () => import('../views/settings/AI/TelegramSettingsView.vue'),
},
{
path: 'email',
name: 'settings-email',
- component: () => import('../views/settings/EmailSettingsView.vue'),
+ component: () => import('../views/settings/AI/EmailSettingsView.vue'),
},
- // Опционально: перенаправление со /settings на первую подстраницу
- {
- path: '',
- name: 'settings-index',
- redirect: { name: 'settings-ai' }
- }
]
},
+ {
+ path: '/settings/ai/openai',
+ name: 'openai-settings',
+ component: () => import('@/views/settings/AI/OpenAISettingsView.vue'),
+ },
+ {
+ path: '/settings/ai/ollama',
+ name: 'ollama-settings',
+ component: () => import('@/views/settings/AI/OllamaSettingsView.vue'),
+ },
+ {
+ path: '/settings/ai/database',
+ name: 'database-settings',
+ component: () => import('@/views/settings/AI/DatabaseSettingsView.vue'),
+ },
+ {
+ path: '/settings/ai/assistant',
+ name: 'ai-assistant-settings',
+ component: () => import('@/views/settings/AI/AiAssistantSettings.vue'),
+ },
{
path: '/tables',
name: 'tables-list',
@@ -120,6 +139,16 @@ const routes = [
name: 'dle-management',
component: () => import('../views/DleManagementView.vue')
},
+ {
+ path: '/settings/ai/telegram',
+ name: 'telegram-settings',
+ component: () => import('@/views/settings/AI/TelegramSettingsView.vue'),
+ },
+ {
+ path: '/settings/ai/email',
+ name: 'email-settings',
+ component: () => import('@/views/settings/AI/EmailSettingsView.vue'),
+ },
];
const router = createRouter({
diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue
index dac11e8..aea2ed6 100644
--- a/frontend/src/views/SettingsView.vue
+++ b/frontend/src/views/SettingsView.vue
@@ -8,23 +8,12 @@
>
Настройки
-
Загрузка данных пользователя...
Для доступа к настройкам необходимо войти .
-
-
-
@@ -32,7 +21,7 @@
+
+
\ No newline at end of file
diff --git a/frontend/src/views/settings/AI/DatabaseSettingsView.vue b/frontend/src/views/settings/AI/DatabaseSettingsView.vue
new file mode 100644
index 0000000..8c46c15
--- /dev/null
+++ b/frontend/src/views/settings/AI/DatabaseSettingsView.vue
@@ -0,0 +1,210 @@
+
+
+
+
×
+
База данных: интеграция и настройки
+
+
+
+
Host: {{ form.dbHost }}
+
Port: {{ form.dbPort }}
+
Database: {{ form.dbName }}
+
User: {{ form.dbUser }}
+
Password: ••••••••••••••••••••••••••••••••
+
Изменить
+
Закрыть
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/views/settings/AI/EmailSettingsView.vue b/frontend/src/views/settings/AI/EmailSettingsView.vue
new file mode 100644
index 0000000..0e99c2f
--- /dev/null
+++ b/frontend/src/views/settings/AI/EmailSettingsView.vue
@@ -0,0 +1,225 @@
+
+
+
+
×
+
Email: интеграция и настройки
+
+
+
+
SMTP Host: {{ form.smtpHost }}
+
SMTP Port: {{ form.smtpPort }}
+
SMTP User: {{ form.smtpUser }}
+
IMAP Host: {{ form.imapHost }}
+
IMAP Port: {{ form.imapPort }}
+
From Email: {{ form.fromEmail }}
+
Изменить
+
Закрыть
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/views/settings/AI/OllamaSettingsView.vue b/frontend/src/views/settings/AI/OllamaSettingsView.vue
new file mode 100644
index 0000000..4941057
--- /dev/null
+++ b/frontend/src/views/settings/AI/OllamaSettingsView.vue
@@ -0,0 +1,65 @@
+
+
+
+
×
+
Ollama: интеграция и настройки
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/views/settings/AI/OpenAISettingsView.vue b/frontend/src/views/settings/AI/OpenAISettingsView.vue
new file mode 100644
index 0000000..befc01b
--- /dev/null
+++ b/frontend/src/views/settings/AI/OpenAISettingsView.vue
@@ -0,0 +1,65 @@
+
+
+
+
×
+
OpenAI: интеграция и настройки
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/views/settings/AI/TelegramSettingsView.vue b/frontend/src/views/settings/AI/TelegramSettingsView.vue
new file mode 100644
index 0000000..f452864
--- /dev/null
+++ b/frontend/src/views/settings/AI/TelegramSettingsView.vue
@@ -0,0 +1,186 @@
+
+
+
+
×
+
Telegram: интеграция и настройки
+
+
+
+
Bot Token: ••••••••••••••••••••••••••••••••••••••••
+
Bot Username: {{ form.botUsername }}
+
Изменить
+
Закрыть
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/views/settings/AIProviderSettings.vue b/frontend/src/views/settings/AIProviderSettings.vue
index 8823c1f..ee534e4 100644
--- a/frontend/src/views/settings/AIProviderSettings.vue
+++ b/frontend/src/views/settings/AIProviderSettings.vue
@@ -15,10 +15,18 @@
- Модель:
+ Модель (LLM):
-
- {{ model.id || model }}
+
+ {{ model.id || model.name || model }}
+
+
+
+
+ Embeddings-модель:
+
+
+ {{ model.id || model.name || model }}
@@ -49,7 +57,9 @@ const props = defineProps({
const apiKey = ref('');
const baseUrl = ref('');
const selectedModel = ref('');
+const selectedEmbeddingModel = ref('');
const models = ref([]);
+const embeddingModels = ref([]);
const hasSettings = ref(false);
const verifying = ref(false);
const verifyStatus = ref(null);
@@ -65,9 +75,11 @@ async function loadSettings() {
apiKey.value = data.settings.api_key || '';
baseUrl.value = data.settings.base_url || '';
selectedModel.value = data.settings.selected_model || '';
+ selectedEmbeddingModel.value = data.settings.embedding_model || '';
hasSettings.value = true;
if (apiKey.value || props.provider === 'ollama') {
await loadModels();
+ await loadEmbeddingModels();
}
} else {
hasSettings.value = false;
@@ -82,13 +94,30 @@ async function loadModels() {
const { data } = await axios.get(`/api/settings/ai-settings/${props.provider}/models`);
models.value = data.models || [];
if (!selectedModel.value && models.value.length) {
- selectedModel.value = models.value[0].id || models.value[0];
+ const first = models.value[0];
+ selectedModel.value = first.id || first.name || first;
}
} catch (e) {
models.value = [];
}
}
+async function loadEmbeddingModels() {
+ try {
+ const { data } = await axios.get(`/api/settings/ai-settings/${props.provider}/models`);
+ embeddingModels.value = (data.models || []).filter(m => {
+ const name = m.id || m.name || m;
+ return name && name.toLowerCase().includes('embed');
+ });
+ if (!selectedEmbeddingModel.value && embeddingModels.value.length) {
+ const first = embeddingModels.value[0];
+ selectedEmbeddingModel.value = first.id || first.name || first;
+ }
+ } catch (e) {
+ embeddingModels.value = [];
+ }
+}
+
async function onVerify() {
verifying.value = true;
verifyStatus.value = null;
@@ -119,6 +148,7 @@ async function onSave() {
api_key: apiKey.value,
base_url: baseUrl.value,
selected_model: selectedModel.value,
+ embedding_model: selectedEmbeddingModel.value,
});
saveStatus.value = true;
hasSettings.value = true;
@@ -135,7 +165,9 @@ async function onDelete() {
apiKey.value = '';
baseUrl.value = '';
selectedModel.value = '';
+ selectedEmbeddingModel.value = '';
models.value = [];
+ embeddingModels.value = [];
hasSettings.value = false;
}
@@ -151,8 +183,9 @@ watch([apiKey, baseUrl], () => {
\ No newline at end of file
diff --git a/frontend/src/views/settings/AiSettingsView.vue b/frontend/src/views/settings/AiSettingsView.vue
index e75d64a..7b25a1e 100644
--- a/frontend/src/views/settings/AiSettingsView.vue
+++ b/frontend/src/views/settings/AiSettingsView.vue
@@ -1,46 +1,37 @@
-
+
+
×
Интеграции
OpenAI
Интеграция с OpenAI (GPT-4, GPT-3.5 и др.).
-
Подробнее
-
-
-
Anthropic
-
Интеграция с Anthropic Claude (Claude 3 и др.).
-
Подробнее
-
-
-
Google Gemini
-
Интеграция с Google Gemini (Gemini 1.5, 1.0 и др.).
-
Подробнее
+
Подробнее
Ollama
Локальные open-source модели через Ollama.
-
Подробнее
+
Подробнее
Telegram
Интеграция с Telegram-ботом для уведомлений и авторизации.
-
Подробнее
+
Подробнее
Email
Интеграция с Email для отправки писем и уведомлений.
-
Подробнее
+
Подробнее
База данных
Интеграция с PostgreSQL для хранения данных приложения и управления настройками.
-
Подробнее
+
Подробнее
ИИ-ассистент
Настройки поведения, языков, моделей и правил работы ассистента.
-
Подробнее
+
Подробнее
-
-
-
-
\ No newline at end of file
diff --git a/frontend/src/views/settings/DatabaseSettingsView.vue b/frontend/src/views/settings/DatabaseSettingsView.vue
deleted file mode 100644
index 4ecd60c..0000000
--- a/frontend/src/views/settings/DatabaseSettingsView.vue
+++ /dev/null
@@ -1,171 +0,0 @@
-
-
-
Настройки базы данных
-
-
-
Host: {{ form.dbHost }}
-
Port: {{ form.dbPort }}
-
Database: {{ form.dbName }}
-
User: {{ form.dbUser }}
-
Password: ••••••••••••••••••••••••••••••••
-
Изменить
-
Закрыть
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/frontend/src/views/settings/EmailSettingsView.vue b/frontend/src/views/settings/EmailSettingsView.vue
deleted file mode 100644
index cd9d90b..0000000
--- a/frontend/src/views/settings/EmailSettingsView.vue
+++ /dev/null
@@ -1,186 +0,0 @@
-
-
-
Настройки Email
-
-
-
SMTP Host: {{ form.smtpHost }}
-
SMTP Port: {{ form.smtpPort }}
-
SMTP User: {{ form.smtpUser }}
-
IMAP Host: {{ form.imapHost }}
-
IMAP Port: {{ form.imapPort }}
-
From Email: {{ form.fromEmail }}
-
Изменить
-
Закрыть
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/frontend/src/views/settings/InterfaceSettingsView.vue b/frontend/src/views/settings/InterfaceSettingsView.vue
index 5b8e667..b3983b5 100644
--- a/frontend/src/views/settings/InterfaceSettingsView.vue
+++ b/frontend/src/views/settings/InterfaceSettingsView.vue
@@ -1,5 +1,6 @@
-
+
+
×
Настройки Интерфейса
@@ -27,9 +28,13 @@
import { ref } from 'vue';
import { getFromStorage, setToStorage } from '../../utils/storage'; // Путь к utils может отличаться
import DomainConnectBlock from './DomainConnectBlock.vue';
+import { useRouter } from 'vue-router';
// TODO: Импортировать API для сохранения, если нужно
const selectedLanguage = ref(getFromStorage('userLanguage', 'ru'));
+const router = useRouter();
+
+const goBack = () => router.push('/settings');
// Функция сохранения
const saveLanguageSetting = () => {
@@ -87,6 +92,21 @@ h3 {
.btn-primary {
align-self: flex-start;
}
+.close-btn {
+ position: absolute;
+ top: 18px;
+ right: 18px;
+ background: none;
+ border: none;
+ font-size: 2rem;
+ cursor: pointer;
+ color: #bbb;
+ transition: color 0.2s;
+ z-index: 10;
+}
+.close-btn:hover {
+ color: #333;
+}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
diff --git a/frontend/src/views/settings/OllamaSettingsView.vue b/frontend/src/views/settings/OllamaSettingsView.vue
deleted file mode 100644
index 5574879..0000000
--- a/frontend/src/views/settings/OllamaSettingsView.vue
+++ /dev/null
@@ -1,63 +0,0 @@
-
-
-
Настройки Ollama
-
- Текущая модель:
- {{ currentModel }}
-
-
- Доступные модели для загрузки:
-
- {{ model }}
-
-
-
Закрыть
-
-
-
-
-
-
\ No newline at end of file
diff --git a/frontend/src/views/settings/PublishToIPFSBlock.vue b/frontend/src/views/settings/PublishToIPFSBlock.vue
deleted file mode 100644
index 3f56a0a..0000000
--- a/frontend/src/views/settings/PublishToIPFSBlock.vue
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/frontend/src/views/settings/SecuritySettingsView.vue b/frontend/src/views/settings/SecuritySettingsView.vue
index a464df5..a6490c1 100644
--- a/frontend/src/views/settings/SecuritySettingsView.vue
+++ b/frontend/src/views/settings/SecuritySettingsView.vue
@@ -1,5 +1,6 @@
+
×
Настройки безопасности и подключения к блокчейну
@@ -59,6 +60,7 @@ import useBlockchainNetworks from '@/composables/useBlockchainNetworks';
import eventBus from '@/utils/eventBus';
import RpcProvidersSettings from './RpcProvidersSettings.vue';
import AuthTokensSettings from './AuthTokensSettings.vue';
+import { useRouter } from 'vue-router';
// Состояние для отображения/скрытия дополнительных настроек
const showRpcSettings = ref(false);
@@ -288,6 +290,9 @@ provide('removeAuthToken', removeAuthToken);
provide('addAuthToken', addAuthToken);
provide('newAuthToken', newAuthToken);
provide('networks', networks);
+
+const router = useRouter();
+const goBack = () => router.push('/settings');
\ No newline at end of file
diff --git a/frontend/src/views/settings/SettingsIndexView.vue b/frontend/src/views/settings/SettingsIndexView.vue
new file mode 100644
index 0000000..011486d
--- /dev/null
+++ b/frontend/src/views/settings/SettingsIndexView.vue
@@ -0,0 +1,65 @@
+
+
+
+
ИИ
+
Настройки интеграций, моделей, ассистента и RAG.
+
Подробнее
+
+
+
Блокчейн
+
Интеграция с блокчейн-сетями, RPC, токены и смарт-контракты.
+
Подробнее
+
+
+
Безопасность
+
Управление доступом, токенами, аутентификацией и правами.
+
Подробнее
+
+
+
Интерфейс
+
Настройки внешнего вида, локализации и пользовательского опыта.
+
Подробнее
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/views/settings/TelegramSettingsView.vue b/frontend/src/views/settings/TelegramSettingsView.vue
deleted file mode 100644
index b31ff25..0000000
--- a/frontend/src/views/settings/TelegramSettingsView.vue
+++ /dev/null
@@ -1,147 +0,0 @@
-
-
-
Настройки Telegram
-
-
-
Bot Token: ••••••••••••••••••••••••••••••••
-
Bot Username: {{ form.botUsername }}
-
Изменить
-
Закрыть
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/md/RAG_ASSISTANT_INTEGRATION.md b/md/RAG_ASSISTANT_INTEGRATION.md
index baec673..0f49196 100644
--- a/md/RAG_ASSISTANT_INTEGRATION.md
+++ b/md/RAG_ASSISTANT_INTEGRATION.md
@@ -7,13 +7,14 @@
- Поддерживает фильтрацию по продуктам, сегментам клиентов (тегам), приоритету, дате и другим бизнес-полям.
- Интегрируется с LLM (Ollama/OpenAI) для генерации финального ответа на основе найденного контекста.
- Позволяет настраивать системный промт с плейсхолдерами для гибкой персонализации ответов.
+- Позволяет генерировать draft-ответ для администратора, который может быть отредактирован и отправлен вручную.
---
## Основные требования
-1. **Структура RAG-таблицы**
- - Каждая строка содержит:
+1. **Гибкая структура RAG-таблицы**
+ - Пользователь может создавать таблицы с произвольными столбцами и назначать им специальное значение (purpose) через выпадающий список:
- Вопрос (`question`)
- Ответ (`answer`)
- Ответ с уточняющим вопросом (`clarifyingAnswer`)
@@ -23,12 +24,13 @@
- Дополнительный контекст (`context`)
- Приоритет (`priority`)
- Дата (`date`)
- - Для каждого столбца указывается назначение (purpose) через выпадающий список при создании/редактировании.
+ - Для каждого столбца указывается назначение при создании/редактировании.
+ - Можно добавлять дополнительные столбцы для бизнес-атрибутов.
2. **Фильтрация и поиск**
- При поступлении вопроса пользователя:
- Фильтровать строки по продукту, тегам пользователя, приоритету, дате и другим полям.
- - Выполнять векторный поиск (embedding) только по релевантным строкам.
+ - Выполнять векторный поиск (embedding) только по релевантным строкам (по столбцу "Вопрос").
3. **Интеграция с LLM**
- После поиска по RAG-таблице формировать системный промт с подстановкой найденных данных (через плейсхолдеры).
@@ -59,6 +61,11 @@
- Логируются все этапы работы ассистента: запрос пользователя, найденный контекст, результат LLM, время ответа, id пользователя и т.д.
- Вся информация сохраняется для последующего анализа и улучшения качества ответов.
+7. **Автоответ и draft-ответ**
+ - Если найден релевантный вопрос (score > 0.95) — ассистент автоматически отвечает пользователю.
+ - Если нет — ассистент генерирует draft-ответ для администратора (через роут `/ai-draft`), который подставляется в поле ввода и ожидает отправки админом. Draft генерируется всегда, даже если нет точного совпадения в RAG.
+ - Draft-ответ строится с учётом промта, истории и всех доступных данных из RAG.
+
---
## Пример бизнес-сценария
@@ -69,6 +76,7 @@
- Векторный поиск проводится только по релевантным строкам.
- В системном промте используются плейсхолдеры для подстановки найденных данных.
- LLM генерирует финальный ответ с учётом контекста, уточняющих вопросов и ответов на возражения.
+- Если точного совпадения нет — draft-ответ для администратора формируется на основе промта и истории.
---
@@ -89,4 +97,5 @@
- Персонализированные, точные и масштабируемые ответы для разных продуктов и сегментов клиентов.
- Гибкая настройка ассистента через UI и системный промт.
-- Возможность расширения под любые бизнес-сценарии.
\ No newline at end of file
+- Возможность расширения под любые бизнес-сценарии.
+- Draft-ответы для администратора, которые можно редактировать и отправлять вручную.
\ No newline at end of file
diff --git a/md/RAG_SEARCH_SETUP_AND_TEST.md b/md/RAG_SEARCH_SETUP_AND_TEST.md
new file mode 100644
index 0000000..b79e037
--- /dev/null
+++ b/md/RAG_SEARCH_SETUP_AND_TEST.md
@@ -0,0 +1,51 @@
+# План настройки и тестирования поиска по таблице RAG
+
+## 1. Подготовка таблицы RAG
+- Убедиться, что таблица RAG создана и содержит пары "вопрос-ответ".
+- Добавить несколько тестовых записей через UI или напрямую в базу данных.
+
+## 2. Настройка провайдера эмбеддингов
+- В настройках ассистента выбрать нужного провайдера (OpenAI, Ollama и др.).
+- Ввести API-ключ и Base URL (например, для OpenAI: https://api.openai.com/v1).
+- Сохранить настройки.
+
+## 3. Проверка настроек ассистента
+- Убедиться, что выбран актуальный ID таблицы RAG.
+- Проверить выбранного провайдера эмбеддингов.
+- Установить порог релевантности (например, 0.95).
+
+## 4. Проверка backend-логики
+- Проверить, что в backend (например, в ragService.js) реализован поиск по RAG с использованием выбранного провайдера эмбеддингов.
+- Убедиться, что используется актуальный ID таблицы и динамический выбор провайдера.
+- Проверить возможность изменения порога релевантности.
+
+## 5. Тестирование через UI
+- Отправить ассистенту вопрос, который есть в RAG-таблице — убедиться, что ответ возвращается из базы.
+- Отправить вопрос, которого нет в таблице — убедиться, что ассистент либо не отвечает, либо использует LLM (по настройкам).
+
+## 6. Проверка логов backend
+- Проверить логи на наличие сообщений о поиске по RAG, найденных совпадениях и выбранном провайдере эмбеддингов.
+- В случае ошибок — проанализировать и устранить их.
+
+## 7. Тестирование через API (опционально)
+- Использовать Postman/curl для отправки запросов напрямую к backend.
+- Пример запроса:
+ ```http
+ POST /api/chat/message
+ {
+ "userId": 137,
+ "message": "Как зовут?"
+ }
+ ```
+
+## 8. Автоматизация тестирования (по желанию)
+- Написать автотесты (например, на Mocha/Jest), которые будут отправлять вопросы и сверять ответы с ожидаемыми из RAG.
+
+## 9. Рекомендации
+- Для тестов использовать уникальные, простые вопросы и ответы.
+- После каждого изменения настроек проводить тестовые запросы.
+- Добавить в UI индикатор источника ответа (из базы или сгенерирован).
+
+---
+
+**Если потребуется пример кода или помощь с конкретной реализацией — обращайтесь!**
\ No newline at end of file