summy/requestAI.js
Vufer 18611835cb feat: добавить обработку команды /summy с разбором персонажа и периода для суммаризации
- Добавлен парсер запроса из текста команды с разбором имени персонажа и временного периода/количества сообщений
- Внедрена кэшированная генерация и поиск промптов по персонажу для более живых и контекстных суммаризаций
- Интегрирован внешний вызов API OpenRouter для разбора команд и генерации суммаризаций с учетом стиля персонажа
- Обновлен основной класс TelegramHistoryBot для поддержки новой команды и вызова AI через requestAI.js
- Добавлены хранилища кэша для команд и промптов с логированием загрузки, сохранения и ошибок
- Созданы инструкции для генератора промптов с детальной структурой и правилами для разнообразных персонажей

BREAKING CHANGE: Для корректной работы требуется добавить в .env ключи OPENROUTER_API_KEY, OPENROUTER_MODEL и OPENROUTER_CHEAP_MODEL
2025-06-29 22:54:46 +03:00

218 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;