- Добавлен парсер запроса из текста команды с разбором имени персонажа и временного периода/количества сообщений - Внедрена кэшированная генерация и поиск промптов по персонажу для более живых и контекстных суммаризаций - Интегрирован внешний вызов API OpenRouter для разбора команд и генерации суммаризаций с учетом стиля персонажа - Обновлен основной класс TelegramHistoryBot для поддержки новой команды и вызова AI через requestAI.js - Добавлены хранилища кэша для команд и промптов с логированием загрузки, сохранения и ошибок - Созданы инструкции для генератора промптов с детальной структурой и правилами для разнообразных персонажей BREAKING CHANGE: Для корректной работы требуется добавить в .env ключи OPENROUTER_API_KEY, OPENROUTER_MODEL и OPENROUTER_CHEAP_MODEL
218 lines
12 KiB
JavaScript
218 lines
12 KiB
JavaScript
const logger = require("./logger");
|
||
const {searchInCache, saveInCache} = require("./commandResponser");
|
||
require('dotenv').config();
|
||
// request_parse_prompt= 'Получена строка, это команда ТГ бота. ' +
|
||
// 'Команда может содержать период или количество сообщений, а так же имя персонажа (вероятнее всего известная личность, возможно - название музыкальной группы).' +
|
||
// 'Необходимо разобрать.' +
|
||
// 'Если не удалось разобрать, то вернуть со значениями persona:"Карл Маркс",messages:22.' +
|
||
// 'Одновременно не может быть и количество сообщений, и количество часов, что-то одно.' +
|
||
// 'Если интервал не в часах - пересчитать в часы!' +
|
||
// 'Если имя принадлежит публичной или известной персоне - указать имя полностью! (например - кипелов - Валерий Кипелов, кюри - Мария Склодовская-Кюри' +
|
||
// 'Если это название музыкальной группы - указать, что это музыкальная группа (группа ХХХХ)' +
|
||
// 'Комментарии не оставлять' +
|
||
// 'Вывод - строго в формате json' +
|
||
// 'command={\n' +
|
||
// ' persona:\'Карл Маркс\', // имя персонажа\n' +
|
||
// ' messages:100, // сколько сообщений обработать или 0\n' +
|
||
// ' hours: 24 // за сколько часов, или 0\n' +
|
||
// '}' ;
|
||
//
|
||
//
|
||
request_parse_prompt= 'Ты - эксперт по анализу текстовых команд. Твоя задача - разобрать команду Telegram бота и извлечь из неё информацию о персоне и временном периоде.\n' +
|
||
'ВХОДНЫЕ ДАННЫЕ\n' +
|
||
'Получена строка команды, которая может содержать:\n' +
|
||
'\n' +
|
||
'Имя персонажа (известная личность, историческая фигура, или название музыкальной группы)\n' +
|
||
'Временной период (количество часов, дней, недель, месяцев) ИЛИ количество сообщений\n' +
|
||
'Дополнительные слова и предлоги\n' +
|
||
'\n' +
|
||
'ПРАВИЛА ОБРАБОТКИ\n' +
|
||
'1. ПЕРСОНА\n' +
|
||
'\n' +
|
||
'Если упомянута известная личность - указать полное имя (например: "кипелов" → "Валерий Кипелов", "кюри" → "Мария Склодовская-Кюри", "летов" → "Егор Николаевич Летов")\n' +
|
||
'Если это музыкальная группа - указать с пометкой "группа" (например: "битлз" → "группа The Beatles", "гражданская оборона" → "группа Гражданская оборона")\n' +
|
||
'Если персона не определена или неясна - использовать "Карл Маркс"\n' +
|
||
'Учитывать различные формы написания (сокращения, транслитерацию, опечатки)\n' +
|
||
'\n' +
|
||
'2. ВРЕМЕННОЙ ПЕРИОД\n' +
|
||
'\n' +
|
||
'Может быть указан в различных форматах: "2 часа", "3ч", "день", "неделю", "месяц", "за вчера"\n' +
|
||
'ВСЕ периоды пересчитывать в ЧАСЫ:\n' +
|
||
'\n' +
|
||
'1 день = 24 часа\n' +
|
||
'1 неделя = 168 часов\n' +
|
||
'1 месяц = 720 часов (30 дней)\n' +
|
||
'"вчера" = 24 часа\n' +
|
||
'"позавчера" = 48 часов\n' +
|
||
'\n' +
|
||
'\n' +
|
||
'Если период не указан - использовать 0\n' +
|
||
'\n' +
|
||
'3. КОЛИЧЕСТВО СООБЩЕНИЙ\n' +
|
||
'\n' +
|
||
'Может быть указано как число + "сообщений", "постов", "записей"\n' +
|
||
'Если не указано - использовать 0\n' +
|
||
'ВАЖНО: одновременно НЕ МОЖЕТ быть и количество сообщений, и временной период - только что-то одно!\n' +
|
||
'\n' +
|
||
'4. ПРИОРИТЕТЫ\n' +
|
||
'\n' +
|
||
'Если указаны и период, и количество сообщений - приоритет у количества сообщений (hours = 0)\n' +
|
||
'Если ничего не указано - messages = 22, hours = 0\n' +
|
||
'\n' +
|
||
'ПРИМЕРЫ ВХОДНЫХ КОМАНД И ОЖИДАЕМЫЙ РЕЗУЛЬТАТ\n' +
|
||
'Команда: "покажи последние 50 сообщений в стиле Высоцкого"\n' +
|
||
'Результат: {"persona": "Владимир Семёнович Высоцкий", "messages": 50, "hours": 0}\n' +
|
||
'Команда: "сгенерируй как Metallica за последний день"\n' +
|
||
'Результат: {"persona": "группа Metallica", "messages": 0, "hours": 24}\n' +
|
||
'Команда: "в стиле Маска за 3 часа"\n' +
|
||
'Результат: {"persona": "Илон Маск", "messages": 0, "hours": 3}\n' +
|
||
'Команда: "как Цой последние 100 постов"\n' +
|
||
'Результат: {"persona": "Виктор Робертович Цой", "messages": 100, "hours": 0}\n' +
|
||
'Команда: "покажи Бетховена"\n' +
|
||
'Результат: {"persona": "Людвиг ван Бетховен", "messages": 22, "hours": 0}\n' +
|
||
'ИНСТРУКЦИИ ПО ВЫВОДУ\n' +
|
||
'\n' +
|
||
'Комментарии НЕ оставлять\n' +
|
||
'Вывод СТРОГО в формате JSON без markdown блоков\n' +
|
||
'НЕ ИСПОЛЬЗОВАТЬ json или блоки\n' +
|
||
'Возвращать ТОЛЬКО чистый JSON объект\n' +
|
||
'Не добавлять дополнительные поля\n' +
|
||
'Использовать двойные кавычки для JSON\n' +
|
||
'Никаких объяснений до или после JSON\n' +
|
||
'ОБЯЗАТЕЛЬНО ВАЛИДНЫЙ JSON!!! ОЧЕНЬ ВАЖНО!!\n' +
|
||
'\n' +
|
||
'ФОРМАТ ОТВЕТА (возвращать БЕЗ markdown блоков):\n' +
|
||
'{"persona": "Полное имя персоны или группа Название (краткое описание в трех-пяти словах)", "messages": число_или_0, "hours": число_или_0}\n' +
|
||
'КРИТИЧЕСКИ ВАЖНО\n' +
|
||
'\n' +
|
||
'Твой ответ должен начинаться с { и заканчиваться }\n' +
|
||
'Никаких дополнительных символов, пробелов или переносов строк до и после JSON\n' +
|
||
'НЕ оборачивать в ```json блоки\n' +
|
||
'Возвращать ТОЛЬКО валидный JSON объект\n' +
|
||
'\n' +
|
||
'ОБРАБАТЫВАЙ КОМАНДУ СОГЛАСНО ВСЕМ ПРАВИЛАМ ВЫШЕ. АНАЛИЗИРУЙ КАЖДОЕ СЛОВО И КОНТЕКСТ. ВЕРНИ ТОЛЬКО JSON!'
|
||
|
||
|
||
// async function handleRequest(request) {
|
||
// // Попытка найти результат в кэше
|
||
// const cachedResult = await searchInCache(request);
|
||
//
|
||
// if (cachedResult) {
|
||
// return cachedResult; // Возвращаем найденное в кэше
|
||
// }
|
||
//
|
||
// // Если в кэше ничего нет — вызываем основную функцию
|
||
// const result = await someFunction(request);
|
||
//
|
||
// // Сохраняем результат в кэш
|
||
// await saveInCache(request, result);
|
||
//
|
||
// return result;
|
||
// }
|
||
|
||
async function callAI(prompt, data, type = 'history') {
|
||
// Если API ключ недоступен, используем fallback
|
||
if (!process.env.OPENROUTER_API_KEY ) {
|
||
logger.error('⚠️ OpenRouter API ключ недоступен');
|
||
return;
|
||
}
|
||
let workingPrompt = '';
|
||
switch (type) {
|
||
case 'history':workingPrompt=prompt;
|
||
break;
|
||
case 'request':{
|
||
workingPrompt=request_parse_prompt;
|
||
const cachedResult = await searchInCache(data);
|
||
if (cachedResult) {
|
||
return cachedResult; // Возвращаем найденное в кэше
|
||
}
|
||
}
|
||
|
||
break;
|
||
default:workingPrompt=prompt;
|
||
break;
|
||
}
|
||
try {
|
||
const formattedMessages = data;
|
||
// const formattedMessages = this.formatMessagesForAI(data);
|
||
let useModel
|
||
switch (type) {
|
||
case 'history':useModel=process.env.OPENROUTER_MODEL;
|
||
break;
|
||
case 'prompt':useModel=process.env.OPENROUTER_MODEL;
|
||
break;
|
||
case 'request':useModel=process.env.OPENROUTER_CHEAP_MODEL;
|
||
break;
|
||
default:useModel=process.env.OPENROUTER_MODEL;
|
||
break;
|
||
}
|
||
// = (type!==='history' ? process.env.OPENROUTER_MODEL: process.env.OPENROUTER_CHEAP_MODEL);
|
||
logger.info(`🤖 Отправляем запрос к OpenRouter (модель ${useModel})...`);
|
||
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
||
'Content-Type': 'application/json',
|
||
'HTTP-Referer': 'https://git.rockzo.ru/Rockzo_Develop/summy',
|
||
'X-Title': 'Telegram History Bot'
|
||
},
|
||
body: JSON.stringify({
|
||
model: useModel,
|
||
messages: [
|
||
{
|
||
role: 'system',
|
||
content: workingPrompt
|
||
},
|
||
{
|
||
role: 'user',
|
||
content: formattedMessages
|
||
}
|
||
],
|
||
max_tokens: 2000,
|
||
temperature: 0.8,
|
||
top_p: 0.9
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text();
|
||
throw new Error(`OpenRouter API error: ${response.status} - ${errorText}`);
|
||
}
|
||
|
||
const completion = await response.json();
|
||
const aiSummary = completion.choices?.[0]?.message?.content;
|
||
|
||
if (!aiSummary) {
|
||
logger.error(JSON.stringify(completion));
|
||
throw new Error('Пустой ответ от ИИ');
|
||
}
|
||
|
||
logger.info('✅ Получен ответ от OpenRouter');
|
||
if (type==='request') {
|
||
await saveInCache(data, aiSummary);
|
||
}
|
||
// return `🎬 <b>История чата</b>\n\n${aiSummary}`;
|
||
if (type==='request') {
|
||
console.log(typeof aiSummary, aiSummary)
|
||
return JSON.parse(aiSummary);
|
||
}
|
||
return aiSummary;
|
||
|
||
} catch (error) {
|
||
logger.error('❌ Ошибка при вызове OpenRouter API:', error);
|
||
|
||
// Детальная обработка различных типов ошибок
|
||
if (error.message.includes('401')) {
|
||
logger.error('❌ Неверный API ключ OpenRouter');
|
||
} else if (error.message.includes('429')) {
|
||
logger.error('❌ Превышен лимит запросов OpenRouter');
|
||
} else if (error.message.includes('402')) {
|
||
logger.error('❌ Недостаточно средств на счету OpenRouter');
|
||
} else if (error.code === 'ENOTFOUND') {
|
||
logger.error('❌ Проблемы с сетевым подключением');
|
||
}
|
||
|
||
return // this.generateFallbackSummary(data);
|
||
}
|
||
}
|
||
module.exports = callAI; |