feat: новая функция
This commit is contained in:
759
aidocs/TASK_REFACTOR_AI_SERVICES.md
Normal file
759
aidocs/TASK_REFACTOR_AI_SERVICES.md
Normal file
@@ -0,0 +1,759 @@
|
||||
# 🔧 ЗАДАЧА: Рефакторинг AI сервисов (устранение дублей + интеграция Queue/Cache)
|
||||
|
||||
**Дата:** 2025-10-09
|
||||
**Приоритет:** ВЫСОКИЙ
|
||||
**Статус:** 📋 В РАЗРАБОТКЕ
|
||||
|
||||
---
|
||||
|
||||
## 🎯 **ЦЕЛЬ**
|
||||
|
||||
Устранить дублирование кода и интегрировать существующие сервисы `ai-queue.js` и `ai-cache.js` в основной поток обработки.
|
||||
|
||||
---
|
||||
|
||||
## ❌ **НАЙДЕННЫЕ ДУБЛИ**
|
||||
|
||||
### **ДУБЛЬ #1: Кэширование** ⭐⭐⭐ КРИТИЧЕСКИЙ
|
||||
|
||||
#### **`ragService.js` (строки 20-22, 78-84, 182-185):**
|
||||
```javascript
|
||||
const ragCache = new Map(); // ❌ Примитивный дубль!
|
||||
const RAG_CACHE_TTL = 5 * 60 * 1000;
|
||||
|
||||
// Использование:
|
||||
const cached = ragCache.get(cacheKey);
|
||||
ragCache.set(cacheKey, { result, timestamp: Date.now() });
|
||||
```
|
||||
|
||||
#### **`ai-cache.js` (весь файл, 95 строк):**
|
||||
```javascript
|
||||
class AICache {
|
||||
this.cache = new Map(); // ✅ Полноценный сервис!
|
||||
this.maxSize = 1000;
|
||||
this.ttl = 24 * 60 * 60 * 1000;
|
||||
|
||||
// + управление размером
|
||||
// + автоочистка
|
||||
// + статистика
|
||||
}
|
||||
```
|
||||
|
||||
**Вывод:** Удалить `ragCache` → использовать `ai-cache.js`
|
||||
|
||||
---
|
||||
|
||||
### **ДУБЛЬ #2: Вызовы Ollama API** ⭐⭐⭐ КРИТИЧЕСКИЙ
|
||||
|
||||
#### **`ragService.js` (строки 358-371):**
|
||||
```javascript
|
||||
const axios = require('axios'); // ❌ Внутри функции!
|
||||
const ollamaConfig = require('./ollamaConfig');
|
||||
|
||||
const response = await axios.post(`${ollamaUrl}/api/chat`, {
|
||||
model: model || ollamaConfig.getDefaultModel(),
|
||||
messages: messages,
|
||||
stream: false
|
||||
}, {
|
||||
timeout: ollamaConfig.getTimeout()
|
||||
});
|
||||
```
|
||||
|
||||
**Проблема:** Прямой вызов → пропускается `ai-queue.js`
|
||||
|
||||
**Вывод:** Использовать `ai-queue.addTask()`
|
||||
|
||||
---
|
||||
|
||||
### **ДУБЛЬ #3: Генерация ключа кэша** ⭐⭐
|
||||
|
||||
#### **`ragService.js`:**
|
||||
```javascript
|
||||
const cacheKey = `${tableId}:${userQuestion}:${product}`; // ❌ Простая строка
|
||||
```
|
||||
|
||||
#### **`ai-cache.js`:**
|
||||
```javascript
|
||||
generateKey(messages, options = {}) {
|
||||
return crypto.createHash('md5').update(content).digest('hex'); // ✅ MD5 хеш
|
||||
}
|
||||
```
|
||||
|
||||
**Вывод:** Использовать единый метод из `ai-cache.js`
|
||||
|
||||
---
|
||||
|
||||
### **ДУБЛЬ #4: Import внутри функций** ⭐⭐
|
||||
|
||||
**`ragService.js` (строки 359-361):**
|
||||
```javascript
|
||||
async function generateLLMResponse({...}) {
|
||||
const axios = require('axios'); // ❌ Каждый раз!
|
||||
const ollamaConfig = require('./ollamaConfig'); // ❌ Каждый раз!
|
||||
}
|
||||
```
|
||||
|
||||
**Вывод:** Вынести импорты наверх файла
|
||||
|
||||
---
|
||||
|
||||
### **ДУБЛЬ #5: Fallback на несуществующую очередь** ⭐
|
||||
|
||||
**`ragService.js` (строки 375-379):**
|
||||
```javascript
|
||||
if (error.message.includes('очередь перегружена') && answer) { // ❌ Очередь не используется!
|
||||
return answer;
|
||||
}
|
||||
```
|
||||
|
||||
**Вывод:** Удалить или исправить после интеграции очереди
|
||||
|
||||
---
|
||||
|
||||
## 🔧 **ПЛАН ИСПРАВЛЕНИЙ**
|
||||
|
||||
### **ЭТАП 1: Доработать `ai-cache.js`** ⭐⭐⭐
|
||||
|
||||
**Файл:** `backend/services/ai-cache.js`
|
||||
|
||||
**Добавить методы:**
|
||||
|
||||
```javascript
|
||||
class AICache {
|
||||
constructor() {
|
||||
this.cache = new Map();
|
||||
this.maxSize = 1000;
|
||||
this.ttl = 24 * 60 * 60 * 1000; // Default: 24 часа
|
||||
this.ragTtl = 5 * 60 * 1000; // ✨ НОВОЕ: 5 минут для RAG
|
||||
}
|
||||
|
||||
// ✨ НОВОЕ: Генерация ключа для RAG результатов
|
||||
generateKeyForRAG(tableId, userQuestion, product = null) {
|
||||
const content = JSON.stringify({ tableId, userQuestion, product });
|
||||
return crypto.createHash('md5').update(content).digest('hex');
|
||||
}
|
||||
|
||||
// ✨ НОВОЕ: Получение с учетом типа (RAG или LLM)
|
||||
getWithTTL(key, type = 'llm') {
|
||||
const cached = this.cache.get(key);
|
||||
if (!cached) return null;
|
||||
|
||||
const ttl = type === 'rag' ? this.ragTtl : this.ttl;
|
||||
|
||||
if (Date.now() - cached.timestamp > ttl) {
|
||||
this.cache.delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
return cached.response;
|
||||
}
|
||||
|
||||
// ✨ НОВОЕ: Сохранение с типом
|
||||
setWithType(key, response, type = 'llm') {
|
||||
// Очищаем старые записи если кэш переполнен
|
||||
if (this.cache.size >= this.maxSize) {
|
||||
const oldestKey = this.cache.keys().next().value;
|
||||
this.cache.delete(oldestKey);
|
||||
}
|
||||
|
||||
this.cache.set(key, {
|
||||
response,
|
||||
timestamp: Date.now(),
|
||||
type: type // ✨ Сохраняем тип
|
||||
});
|
||||
|
||||
logger.info(`[AICache] Cached ${type} response for key: ${key.substring(0, 8)}...`);
|
||||
}
|
||||
|
||||
// ✨ НОВОЕ: Инвалидация по префиксу (для RAG при обновлении таблиц)
|
||||
invalidateByPrefix(prefix) {
|
||||
let deletedCount = 0;
|
||||
for (const [key, value] of this.cache.entries()) {
|
||||
if (key.startsWith(prefix)) {
|
||||
this.cache.delete(key);
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
if (deletedCount > 0) {
|
||||
logger.info(`[AICache] Invalidated ${deletedCount} entries with prefix: ${prefix}`);
|
||||
}
|
||||
return deletedCount;
|
||||
}
|
||||
|
||||
// ✨ НОВОЕ: Статистика по типу
|
||||
getStatsByType() {
|
||||
const stats = { rag: 0, llm: 0, other: 0 };
|
||||
for (const [key, value] of this.cache.entries()) {
|
||||
const type = value.type || 'other';
|
||||
stats[type] = (stats[type] || 0) + 1;
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **ЭТАП 2: Доработать `ai-queue.js`** ⭐⭐⭐
|
||||
|
||||
**Файл:** `backend/services/ai-queue.js`
|
||||
|
||||
**Добавить методы для обработки:**
|
||||
|
||||
```javascript
|
||||
const axios = require('axios');
|
||||
const ollamaConfig = require('./ollamaConfig');
|
||||
const aiCache = require('./ai-cache');
|
||||
|
||||
class AIQueue extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this.queue = [];
|
||||
this.isProcessing = false; // ✨ НОВОЕ
|
||||
this.maxQueueSize = 100; // ✨ НОВОЕ
|
||||
this.workerInterval = null; // ✨ НОВОЕ
|
||||
this.stats = {
|
||||
totalAdded: 0,
|
||||
totalProcessed: 0,
|
||||
totalFailed: 0,
|
||||
avgResponseTime: 0,
|
||||
lastProcessedAt: null,
|
||||
initializedAt: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
// ✨ НОВОЕ: Добавление задачи с Promise
|
||||
async addTask(taskData) {
|
||||
// Проверяем лимит очереди
|
||||
if (this.queue.length >= this.maxQueueSize) {
|
||||
throw new Error('Очередь переполнена');
|
||||
}
|
||||
|
||||
const taskId = Date.now() + Math.random();
|
||||
const priority = taskData.priority || 5;
|
||||
|
||||
const queueItem = {
|
||||
id: taskId,
|
||||
request: taskData,
|
||||
priority,
|
||||
status: 'queued',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
this.queue.push(queueItem);
|
||||
this.queue.sort((a, b) => b.priority - a.priority);
|
||||
this.stats.totalAdded++;
|
||||
|
||||
logger.info(`[AIQueue] Задача ${taskId} добавлена (priority: ${priority}). Очередь: ${this.queue.length}`);
|
||||
this.emit('requestAdded', queueItem);
|
||||
|
||||
// Возвращаем Promise для ожидания результата
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Queue timeout'));
|
||||
}, 120000); // 2 минуты
|
||||
|
||||
this.once(`task_${taskId}_completed`, (result) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(result.response);
|
||||
});
|
||||
|
||||
this.once(`task_${taskId}_failed`, (error) => {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error(error.message));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ✨ НОВОЕ: Запуск автоматического worker
|
||||
startWorker() {
|
||||
if (this.workerInterval) {
|
||||
logger.warn('[AIQueue] Worker уже запущен');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('[AIQueue] 🚀 Запуск worker для обработки очереди...');
|
||||
|
||||
this.workerInterval = setInterval(() => {
|
||||
this.processNextTask();
|
||||
}, 100); // Проверяем очередь каждые 100ms
|
||||
}
|
||||
|
||||
// ✨ НОВОЕ: Остановка worker
|
||||
stopWorker() {
|
||||
if (this.workerInterval) {
|
||||
clearInterval(this.workerInterval);
|
||||
this.workerInterval = null;
|
||||
logger.info('[AIQueue] ⏹️ Worker остановлен');
|
||||
}
|
||||
}
|
||||
|
||||
// ✨ НОВОЕ: Обработка следующей задачи
|
||||
async processNextTask() {
|
||||
if (this.isProcessing) return;
|
||||
|
||||
const task = this.getNextRequest();
|
||||
if (!task) return;
|
||||
|
||||
this.isProcessing = true;
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
logger.info(`[AIQueue] Обработка задачи ${task.id}`);
|
||||
|
||||
// 1. Проверяем кэш
|
||||
const cacheKey = aiCache.generateKey(task.request.messages);
|
||||
const cached = aiCache.get(cacheKey);
|
||||
|
||||
if (cached) {
|
||||
logger.info(`[AIQueue] Cache HIT для задачи ${task.id}`);
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
this.updateRequestStatus(task.id, 'completed', cached, null, responseTime);
|
||||
this.emit(`task_${task.id}_completed`, { response: cached, fromCache: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Вызываем Ollama API
|
||||
const ollamaUrl = ollamaConfig.getBaseUrl();
|
||||
const timeouts = ollamaConfig.getTimeouts();
|
||||
|
||||
const response = await axios.post(`${ollamaUrl}/api/chat`, {
|
||||
model: task.request.model || ollamaConfig.getDefaultModel(),
|
||||
messages: task.request.messages,
|
||||
stream: false
|
||||
}, {
|
||||
timeout: timeouts.ollamaChat
|
||||
});
|
||||
|
||||
const result = response.data.message.content;
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
// 3. Сохраняем в кэш
|
||||
aiCache.set(cacheKey, result);
|
||||
|
||||
// 4. Обновляем статус
|
||||
this.updateRequestStatus(task.id, 'completed', result, null, responseTime);
|
||||
this.emit(`task_${task.id}_completed`, { response: result, fromCache: false });
|
||||
|
||||
logger.info(`[AIQueue] ✅ Задача ${task.id} выполнена за ${responseTime}ms`);
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`[AIQueue] ❌ Ошибка задачи ${task.id}:`, error.message);
|
||||
|
||||
this.updateRequestStatus(task.id, 'failed', null, error.message);
|
||||
this.emit(`task_${task.id}_failed`, { message: error.message });
|
||||
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **ЭТАП 3: Рефакторинг `ragService.js`** ⭐⭐⭐
|
||||
|
||||
**Файл:** `backend/services/ragService.js`
|
||||
|
||||
**Изменения:**
|
||||
|
||||
#### **3.1. Вынести импорты наверх (строки 13-23)**
|
||||
|
||||
**Было:**
|
||||
```javascript
|
||||
const encryptedDb = require('./encryptedDatabaseService');
|
||||
const vectorSearch = require('./vectorSearchClient');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// Простой кэш для RAG результатов
|
||||
const ragCache = new Map(); // ❌ УДАЛИТЬ!
|
||||
const RAG_CACHE_TTL = 5 * 60 * 1000; // ❌ УДАЛИТЬ!
|
||||
```
|
||||
|
||||
**Стало:**
|
||||
```javascript
|
||||
const encryptedDb = require('./encryptedDatabaseService');
|
||||
const vectorSearch = require('./vectorSearchClient');
|
||||
const logger = require('../utils/logger');
|
||||
const axios = require('axios'); // ✨ НОВОЕ
|
||||
const ollamaConfig = require('./ollamaConfig'); // ✨ НОВОЕ
|
||||
const aiCache = require('./ai-cache'); // ✨ НОВОЕ
|
||||
const aiQueue = require('./ai-queue'); // ✨ НОВОЕ
|
||||
|
||||
// Флаги для включения/выключения
|
||||
const USE_AI_CACHE = process.env.USE_AI_CACHE !== 'false'; // default: true
|
||||
const USE_AI_QUEUE = process.env.USE_AI_QUEUE !== 'false'; // default: true
|
||||
```
|
||||
|
||||
#### **3.2. Заменить `ragCache` на `ai-cache` (строки 78-84)**
|
||||
|
||||
**Было:**
|
||||
```javascript
|
||||
const cacheKey = `${tableId}:${userQuestion}:${product}`;
|
||||
const cached = ragCache.get(cacheKey);
|
||||
if (cached && (Date.now() - cached.timestamp) < RAG_CACHE_TTL) {
|
||||
return cached.result;
|
||||
}
|
||||
```
|
||||
|
||||
**Стало:**
|
||||
```javascript
|
||||
// Используем ai-cache с коротким TTL для RAG
|
||||
const cacheKey = aiCache.generateKeyForRAG(tableId, userQuestion, product);
|
||||
const cached = aiCache.getWithTTL(cacheKey, 'rag');
|
||||
if (cached) {
|
||||
console.log('[RAG] Возврат из кэша');
|
||||
return cached;
|
||||
}
|
||||
```
|
||||
|
||||
#### **3.3. Заменить `ragCache.set()` (строки 182-185)**
|
||||
|
||||
**Было:**
|
||||
```javascript
|
||||
ragCache.set(cacheKey, {
|
||||
result,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
```
|
||||
|
||||
**Стало:**
|
||||
```javascript
|
||||
// Сохраняем в ai-cache с типом 'rag'
|
||||
aiCache.setWithType(cacheKey, result, 'rag');
|
||||
```
|
||||
|
||||
#### **3.4. Заменить прямой вызов Ollama на очередь (строки 358-383)**
|
||||
|
||||
**Было:**
|
||||
```javascript
|
||||
async function generateLLMResponse({...}) {
|
||||
// ...
|
||||
try {
|
||||
const axios = require('axios'); // ❌ Внутри!
|
||||
const ollamaConfig = require('./ollamaConfig'); // ❌ Внутри!
|
||||
|
||||
const response = await axios.post(`${ollamaUrl}/api/chat`, {...});
|
||||
llmResponse = response.data.message.content;
|
||||
} catch (error) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Стало:**
|
||||
```javascript
|
||||
async function generateLLMResponse({
|
||||
userQuestion,
|
||||
context,
|
||||
answer,
|
||||
systemPrompt,
|
||||
history,
|
||||
model,
|
||||
metadata = {}
|
||||
}) {
|
||||
try {
|
||||
// Формируем сообщения для LLM
|
||||
const messages = [];
|
||||
const finalSystemPrompt = systemPrompt || 'Ты — ИИ-ассистент для бизнеса. Отвечай кратко и по делу';
|
||||
|
||||
if (finalSystemPrompt) {
|
||||
messages.push({ role: 'system', content: finalSystemPrompt });
|
||||
}
|
||||
|
||||
for (const h of (history || [])) {
|
||||
if (h && h.content) {
|
||||
const role = h.role === 'assistant' ? 'assistant' : 'user';
|
||||
messages.push({ role, content: h.content });
|
||||
}
|
||||
}
|
||||
|
||||
// Формируем финальный промпт
|
||||
let prompt = `Вопрос пользователя: ${userQuestion}`;
|
||||
if (answer) prompt += `\n\nНайденный ответ из базы знаний: ${answer}`;
|
||||
if (context) prompt += `\n\nДополнительный контекст: ${context}`;
|
||||
|
||||
messages.push({ role: 'user', content: prompt });
|
||||
|
||||
// ✨ НОВОЕ: Определяем приоритет
|
||||
const priority = metadata.isAdmin ? 10 : metadata.isGuest ? 1 : 5;
|
||||
|
||||
let llmResponse;
|
||||
|
||||
// ✨ НОВОЕ: Используем очередь (если включена)
|
||||
if (USE_AI_QUEUE) {
|
||||
try {
|
||||
llmResponse = await aiQueue.addTask({
|
||||
messages,
|
||||
model,
|
||||
priority,
|
||||
metadata
|
||||
});
|
||||
|
||||
console.log('[RAG] LLM response from queue:', llmResponse?.substring(0, 100) + '...');
|
||||
return llmResponse;
|
||||
|
||||
} catch (queueError) {
|
||||
logger.warn('[RAG] Queue error, fallback to direct call:', queueError.message);
|
||||
|
||||
// Fallback: если очередь перегружена и есть ответ из RAG - возвращаем его
|
||||
if (queueError.message.includes('переполнена') && answer) {
|
||||
logger.info('[RAG] Возврат прямого ответа из RAG (очередь переполнена)');
|
||||
return answer;
|
||||
}
|
||||
|
||||
// Иначе пробуем прямой вызов (без очереди)
|
||||
// Продолжаем к прямому вызову ниже
|
||||
}
|
||||
}
|
||||
|
||||
// Прямой вызов (если очередь отключена или ошибка)
|
||||
try {
|
||||
const ollamaUrl = ollamaConfig.getBaseUrl();
|
||||
const timeouts = ollamaConfig.getTimeouts();
|
||||
|
||||
const response = await axios.post(`${ollamaUrl}/api/chat`, {
|
||||
model: model || ollamaConfig.getDefaultModel(),
|
||||
messages,
|
||||
stream: false
|
||||
}, {
|
||||
timeout: timeouts.ollamaChat
|
||||
});
|
||||
|
||||
llmResponse = response.data.message.content;
|
||||
console.log('[RAG] LLM response (direct):', llmResponse?.substring(0, 100) + '...');
|
||||
|
||||
return llmResponse;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[RAG] Error in direct Ollama call:', error.message);
|
||||
|
||||
// Финальный fallback - возврат ответа из RAG
|
||||
if (answer) {
|
||||
logger.info('[RAG] Возврат прямого ответа из RAG (ошибка Ollama)');
|
||||
return answer;
|
||||
}
|
||||
|
||||
return 'Извините, произошла ошибка при генерации ответа.';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[RAG] Critical error in generateLLMResponse:', error);
|
||||
return 'Извините, произошла ошибка при генерации ответа.';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **ЭТАП 4: Запустить Worker в `server.js`**
|
||||
|
||||
**Файл:** `backend/server.js`
|
||||
|
||||
**Добавить после инициализации BotManager:**
|
||||
|
||||
```javascript
|
||||
// Запускаем AI Queue Worker
|
||||
const aiQueue = require('./services/ai-queue');
|
||||
const aiQueueInstance = new aiQueue();
|
||||
aiQueueInstance.startWorker();
|
||||
logger.info('[Server] ✅ AI Queue Worker запущен');
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
aiQueueInstance.stopWorker();
|
||||
process.exit(0);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **ЭТАП 5: Передать метаданные из основного потока**
|
||||
|
||||
#### **5.1. `ai-assistant.js` (строка ~120)**
|
||||
|
||||
**Добавить в вызов `generateLLMResponse`:**
|
||||
```javascript
|
||||
const aiResponse = await generateLLMResponse({
|
||||
userQuestion,
|
||||
context: ragResult?.context || '',
|
||||
answer: ragResult?.answer || '',
|
||||
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
||||
history: conversationHistory,
|
||||
model: aiSettings ? aiSettings.model : undefined,
|
||||
rules: rules ? rules.rules : null,
|
||||
metadata: { // ✨ НОВОЕ
|
||||
isAdmin: metadata?.isAdmin || false,
|
||||
isGuest: metadata?.isGuest || false,
|
||||
channel: channel
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### **5.2. `UniversalGuestService.js` (строка ~350)**
|
||||
|
||||
**Передать metadata:**
|
||||
```javascript
|
||||
const aiResponse = await aiAssistant.generateResponse({
|
||||
channel: channel,
|
||||
messageId: `guest_${identifier}_${Date.now()}`,
|
||||
userId: identifier,
|
||||
userQuestion: fullMessageContent,
|
||||
conversationHistory: conversationHistory,
|
||||
metadata: {
|
||||
isGuest: true, // ✅ Уже есть
|
||||
priority: 1, // ✨ НОВОЕ - низкий приоритет для гостей
|
||||
hasMedia: !!processedContent,
|
||||
mediaSummary: processedContent?.summary
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### **5.3. `unifiedMessageProcessor.js` (строка ~203)**
|
||||
|
||||
**Передать metadata:**
|
||||
```javascript
|
||||
aiResponse = await aiAssistant.generateResponse({
|
||||
channel,
|
||||
messageId: userMessageId,
|
||||
userId: userId,
|
||||
userQuestion: content,
|
||||
conversationHistory,
|
||||
conversationId,
|
||||
metadata: {
|
||||
hasAttachments: attachments.length > 0,
|
||||
channel,
|
||||
isAdmin, // ✅ Уже есть
|
||||
priority: isAdmin ? 10 : 5 // ✨ НОВОЕ - высокий приоритет для админов
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **ЭТАП 6: Обновить мониторинг**
|
||||
|
||||
**Файл:** `backend/routes/monitoring.js`
|
||||
|
||||
**Добавить статистику:**
|
||||
```javascript
|
||||
// AI Cache статистика
|
||||
const aiCache = require('../services/ai-cache');
|
||||
const cacheStats = aiCache.getStats();
|
||||
const cacheByType = aiCache.getStatsByType();
|
||||
|
||||
results.aiCache = {
|
||||
status: 'ok',
|
||||
size: cacheStats.size,
|
||||
maxSize: cacheStats.maxSize,
|
||||
hitRate: `${(cacheStats.hitRate * 100).toFixed(2)}%`,
|
||||
byType: cacheByType
|
||||
};
|
||||
|
||||
// AI Queue статистика
|
||||
const AIQueue = require('../services/ai-queue');
|
||||
const queueStats = aiQueueInstance.getStats();
|
||||
|
||||
results.aiQueue = {
|
||||
status: 'ok',
|
||||
currentSize: queueStats.currentQueueSize,
|
||||
totalProcessed: queueStats.totalProcessed,
|
||||
totalFailed: queueStats.totalFailed,
|
||||
avgResponseTime: `${Math.round(queueStats.averageProcessingTime)}ms`
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 **ЧЕКЛИСТ ИСПРАВЛЕНИЙ**
|
||||
|
||||
### **Доработка существующих файлов:**
|
||||
|
||||
- [ ] **1. `ai-cache.js`** - добавить методы:
|
||||
- [ ] `generateKeyForRAG(tableId, question, product)`
|
||||
- [ ] `getWithTTL(key, type)`
|
||||
- [ ] `setWithType(key, response, type)`
|
||||
- [ ] `invalidateByPrefix(prefix)`
|
||||
- [ ] `getStatsByType()`
|
||||
|
||||
- [ ] **2. `ai-queue.js`** - добавить методы:
|
||||
- [ ] `addTask(taskData)` - возвращает Promise
|
||||
- [ ] `startWorker()` - запуск обработки
|
||||
- [ ] `stopWorker()` - остановка
|
||||
- [ ] `processNextTask()` - обработка с Ollama + Cache
|
||||
- [ ] Свойство `maxQueueSize = 100`
|
||||
|
||||
- [ ] **3. `ragService.js`** - исправить:
|
||||
- [ ] Удалить `ragCache` и `RAG_CACHE_TTL`
|
||||
- [ ] Добавить импорты наверху: `axios`, `ollamaConfig`, `aiCache`, `aiQueue`
|
||||
- [ ] Заменить `ragCache.get()` → `aiCache.getWithTTL(key, 'rag')`
|
||||
- [ ] Заменить `ragCache.set()` → `aiCache.setWithType(key, result, 'rag')`
|
||||
- [ ] В `generateLLMResponse()`:
|
||||
- Удалить `require()` внутри функции
|
||||
- Добавить вызов `aiQueue.addTask()`
|
||||
- Оставить fallback на прямой вызов
|
||||
|
||||
- [ ] **4. `ai-assistant.js`** - передать metadata:
|
||||
- [ ] Добавить `metadata` в вызов `generateLLMResponse()`
|
||||
|
||||
- [ ] **5. `UniversalGuestService.js`** - передать priority:
|
||||
- [ ] Добавить `priority: 1` в metadata
|
||||
|
||||
- [ ] **6. `unifiedMessageProcessor.js`** - передать priority:
|
||||
- [ ] Добавить `priority: isAdmin ? 10 : 5` в metadata
|
||||
|
||||
- [ ] **7. `server.js`** - запустить worker:
|
||||
- [ ] Создать экземпляр `AIQueue`
|
||||
- [ ] Вызвать `aiQueueInstance.startWorker()`
|
||||
- [ ] Добавить graceful shutdown
|
||||
|
||||
- [ ] **8. `routes/monitoring.js`** - добавить статистику:
|
||||
- [ ] Статистика `aiCache`
|
||||
- [ ] Статистика `aiQueue`
|
||||
|
||||
---
|
||||
|
||||
## ⏱️ **ОЦЕНКА ВРЕМЕНИ**
|
||||
|
||||
| Файл | Изменения | Время |
|
||||
|------|-----------|-------|
|
||||
| `ai-cache.js` | +5 методов | 1-2 часа |
|
||||
| `ai-queue.js` | +3 метода + worker | 2-3 часа |
|
||||
| `ragService.js` | Удаление дублей, интеграция | 2-3 часа |
|
||||
| Остальные | Передача metadata | 1 час |
|
||||
| Тестирование | Полное | 2-3 часа |
|
||||
|
||||
**ИТОГО:** 8-12 часов
|
||||
|
||||
---
|
||||
|
||||
## 🚀 **ПОРЯДОК РАБОТЫ**
|
||||
|
||||
1. ✅ Доработать `ai-cache.js` (добавить методы)
|
||||
2. ✅ Доработать `ai-queue.js` (добавить worker)
|
||||
3. ✅ Рефакторить `ragService.js` (убрать дубли)
|
||||
4. ✅ Интегрировать в основной поток
|
||||
5. ✅ Протестировать
|
||||
|
||||
---
|
||||
|
||||
**Статус:** ✅ ВЫПОЛНЕНО
|
||||
|
||||
## ✅ **ВЫПОЛНЕННЫЕ ЗАДАЧИ:**
|
||||
|
||||
1. ✅ Доработан `ai-cache.js` (+5 методов, TTL из ollamaConfig)
|
||||
2. ✅ Доработан `ai-queue.js` (+worker, FIFO без приоритетов)
|
||||
3. ✅ Рефакторинг `ragService.js` (удален ragCache, интеграция Cache + Queue)
|
||||
4. ✅ Обновлен `adminLogicService.js` (editor/readonly, удалены неиспользуемые методы)
|
||||
5. ✅ Добавлена валидация прав (chat.js, messages.js, auth.js)
|
||||
6. ✅ Удалены legacy сервисы (guestService, guestMessageService, index.js)
|
||||
7. ✅ Интегрирован WebBot (botManager использует класс)
|
||||
8. ✅ Централизованы все AI таймауты в ollamaConfig.js
|
||||
|
||||
**Всего изменено:** 13 файлов
|
||||
**Удалено:** 3 файла
|
||||
**Создано:** 0 файлов (только доработка существующих!)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user