Наконец-то мои руки дошли до обновления telegram бота и я сразу же решил написать статью на эту тему. Отдельная благодарность моим подписчикам за то, что вы готовы ждать мои подробные "бредни")
OpenAI представила уже 4 версию своего ChatGPT, но она доступна только для копаний на бета тестировании и владельцам подписки Plus за 10$ в месяц (в РФ не оплатишь).
Но зато нам доступна замечательная версия 3.5 turbo, которая очень сильно отличается от предыдущей 3 версии. Какие теперь генерируются ответы и насколько приятно стало работать с ChatGPT просто неописуемо.
По этому я и решил взяться за обновление бота на новый chatgpt-3.5-turbo, а также добавить немного от себя. Готовы получать код? Приступим!
Что же я добавил и изменил?
- Поменял библиотеку с aiogram на telebot
- Добавил reply кнопки для удобства
- Добавил админ меню (но не доработал😊)
- Добавил ЗАПОМИНАНИЕ КОНТЕКСТА
Какой же код?
Итак.... Расскажу немного о коде, который в итоге я использую в своём telegram боте. Буду разбирать подробно строки разбивая их на несколько частей.
import os
print('Бот запущен!')
admins = [1387454874]
NUMBERS_ROWS = 7
import openai
import telebot
from telebot.types import ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardButton, InlineKeyboardMarkup
openai.api_key = "Ключ Api от openai"
bot = telebot.TeleBot('Api ключ бота')
Эта часть кода отвечает за импортирование необходимых модулей и установление соединения с API для двух сервисов: OpenAI и Telegram Bot API.
Конкретно, мы импортируем модули os, openai, telebot и несколько классов из telebot.types, такие как ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardButton и InlineKeyboardMarkup, которые используются для создания разных типов кнопок в Telegram боте.
Затем мы устанавливаем ключ API для OpenAI и токен для Telegram Bot API. Это позволяет нашему боту взаимодействовать с сервисами OpenAI и Telegram.
Переменная admins содержит список id администраторов бота.
NUMBERS_ROWS - это константа, которая содержит количество строк в таблице, которая содержит в себе последние сообщения с пользователем. Здесь стоит "7", значит бот запоминает контекст только последних 7 сообщений.
После этого в консоль выводится сообщение "Бот запущен!", чтобы мы могли убедиться, что наш бот успешно запущен.
if not os.path.exists("users"):
os.mkdir("users")
os.path.exists("users") проверяет, существует ли папка "users" в текущей директории.
Если папки "users" не существует, то выполняется следующая строка кода: os.mkdir("users"). Она создает новую папку "users" в текущей директории, чтобы использовать ее для хранения данных о пользователях бота.
Если папка "users" уже существует, то строка кода os.mkdir("users") не будет выполнена, так как условие not os.path.exists("users") не будет выполнено.
@bot.message_handler(commands=['start'])
def start(message):
keyboard = ReplyKeyboardMarkup(resize_keyboard=True)
clear_button = KeyboardButton('Очистить историю')
instruction_button = KeyboardButton('Инструкция')
keyboard.add(clear_button, instruction_button)
bot.send_message(chat_id=message.chat.id, text='Привет! Меня зовут Джо! \n \nЯ твой персональный ассистент, который поможет тебе в любую минуту и ответит на интересующие тебя вопросы'
'\n\nЯ запоминаю то, о чём мы говорили, но для того чтобы мои системы не были перегружены я запоминаю только последние 7 сообщений'
'\n\nЧтобы очистить историю и начать новую тему введите "Очистить историю" или нажмите на кнопку', reply_markup=keyboard)
bot.send_chat_action(chat_id=message.chat.id, action='typing')
@bot.message_handler(commands=['start']) означает, что функция start будет вызываться, когда пользователь напишет команду "/start" в чате бота.
Функция start отправляет пользователю приветственное сообщение, используя метод bot.send_message() с параметрами chat_id, text и reply_markup.
chat_id - идентификатор чата, куда будет отправлено сообщение (в данном случае - идентификатор чата, откуда пришло сообщение).
text - текст сообщения, которое будет отправлено пользователю.
reply_markup - объект типа ReplyKeyboardMarkup, который содержит настройки для создания клавиатуры с кнопками в сообщении.
В данном случае, в ReplyKeyboardMarkup добавляются две кнопки: "Очистить историю" и "Инструкция". Когда пользователь нажимает на одну из этих кнопок, Telegram бот может обрабатывать сообщения соответствующим образом.
В конце функция отправляет сообщение о том, что бот начал набирать текст, используя метод bot.send_chat_action(), чтобы пользователь видел "набирает сообщение" в интерфейсе Telegram, в то время как бот генерирует ответ.
@bot.callback_query_handler(func=lambda call: call.data in ['send_message', 'list_users', 'blacklist', 'exit_admin'])
def admin_panel(call):
if call.from_user.id not in admins:
return
if call.data == 'send_message':
bot.send_message(chat_id=call.message.chat.id, text='Введите текст рассылки:')
bot.register_next_step_handler(call.message, send_message_to_all_users)
elif call.data == 'list_users':
users = os.listdir('users')
bot.send_message(chat_id=call.message.chat.id, text=f'Список пользователей:\n\n{len(users)} человек(а)')
elif call.data == 'blacklist':
bot.send_message(chat_id=call.message.chat.id, text='Введите id пользователя, которого хотите добавить в чёрный список:')
bot.register_next_step_handler(call.message, add_user_to_blacklist)
elif call.data == 'exit_admin':
keyboard = InlineKeyboardMarkup()
bot.send_message(chat_id=call.message.chat.id, text='Вы вышли из панели администратора', reply_markup=keyboard)
Этот кусок кода определяет обработчик колбеков для администраторской панели.
@bot.callback_query_handler(func=lambda call: call.data in ['send_message', 'list_users', 'blacklist', 'exit_admin']) означает, что функция admin_panel будет вызываться, когда пользователь нажмет на кнопку, у которой data будет соответствовать одному из значений: 'send_message', 'list_users', 'blacklist' или 'exit_admin'.
Функция admin_panel начинается с проверки, является ли пользователь администратором (проверка if call.from_user.id not in admins). Если пользователь не является администратором, функция завершается.
Если call.data равно 'send_message', то вызывается функция send_message_to_all_users для отправки сообщения всем пользователям бота.
Если call.data равно 'list_users', то функция listdir() модуля os используется для получения списка файлов в папке "users", которая была создана ранее. Затем отправляется сообщение с количеством пользователей.
Если call.data равно 'blacklist', то вызывается функция add_user_to_blacklist для добавления пользователя в черный список.
Если call.data равно 'exit_admin', то отправляется сообщение о выходе из администраторской панели.
В конце, bot.send_message() используется для отправки сообщения в чат бота, чтобы пользователь мог увидеть результат обработки колбека.
def send_message_to_all_users(message):
users = os.listdir('users')
for user_id in users:
try:
bot.send_message(chat_id=user_id, text=message.text)
except:
continue
bot.send_message(chat_id=message.chat.id, text=f'Сообщение отправлено {len(users)} пользователям')
Этот код представляет функцию send_message_to_all_users(), которая принимает сообщение message и используется для отправки сообщения всем пользователям, которые хранятся в папке users. Она получает список пользователей из этой папки и пытается отправить сообщение каждому пользователю. Если сообщение не может быть отправлено, она продолжает выполнение цикла и пробует отправить сообщение следующему пользователю. В конце она отправляет сообщение в чат от имени бота, указывая количество пользователей, которым было отправлено сообщение.
def add_user_to_blacklist(message):
try:
with open('blacklist.txt', 'a') as f:
f.write(message.text + '\n')
bot.send_message(chat_id=message.chat.id, text=f'Пользователь {message.text} добавлен в чёрный список')
except:
bot.send_message(chat_id=message.chat.id, text='Произошла ошибка при добавлении пользователя в чёрный список')
Этот код содержит функцию add_user_to_blacklist(), которая используется для добавления пользователей в "черный список". Функция записывает id пользователя, полученное из сообщения, в файл blacklist.txt и отправляет сообщение о том, что пользователь был добавлен в черный список. Если происходит ошибка, она отправляет сообщение об ошибке в чат от имени бота.
@bot.message_handler(commands=['admin'])
def msg(message):
if message.from_user.id in admins:
keyboard = InlineKeyboardMarkup()
send_message_button = InlineKeyboardButton(text='Рассылка', callback_data='send_message')
list_users_button = InlineKeyboardButton(text='Список пользователей', callback_data='list_users')
blacklist_button = InlineKeyboardButton(text='Чёрный список', callback_data='blacklist')
exit_button = InlineKeyboardButton(text='Выйти из админ панели', callback_data='exit_admin')
keyboard.add(send_message_button, list_users_button, blacklist_button, exit_button)
bot.send_message(chat_id=message.chat.id, text='Меню администратора:', reply_markup=keyboard)
if f"{message.chat.id}.txt" not in os.listdir('users'):
with open(f"users/{message.chat.id}.txt", "x") as f:
f.write('')
Эта часть кода отвечает за обработку команды /admin. Если пользователь, который отправил команду, находится в списке администраторов (список задан в переменной admins), то бот отправляет ему сообщение с клавиатурой, содержащей 4 кнопки: "Рассылка", "Список пользователей", "Чёрный список" и "Выйти из админ панели".
Если же отправивший команду пользователь не является администратором, бот ничего не отправляет.
Далее в этом же блоке кода проверяется, есть ли у пользователя файл с именем, соответствующим его ID в папке users. Если такого файла нет, создаётся новый файл с таким именем в этой папке. Это нужно для хранения истории диалога бота с пользователем, который можно будет использовать в дальнейшем.
@bot.message_handler(func=lambda message: message.text == 'Инструкция')
def instruction(message):
instructions = """
<b>Инструкция:</b>
<b>1.</b> Чтобы задать вопрос, просто отправьте сообщение.
<b>2.</b> Для очистки истории введите "Очистить историю" или нажмите на соответствующую кнопку.
<b>3.</b> Бот запоминает только последние 7 сообщений, поэтому если вы хотите перейти к новой теме, не забудьте очистить историю.
<b>4.</b> Для того, чтобы получить более качественный ответ от бота задайте ему сначала характер. Например <b>"Ты спортивный тренер"</b> или <b>"Ты юрист"</b> или <b>"Ты программист с опытом работы более 5-ти лет"</b> или <b>"Ты SMM специалист"</b>. Далее пишите свой запрос
<b>5.</b> Использование нецензурной лексики карается блокировкой
<b>6.</b> Если вы получили похожее сообщение: <b>This model's maximum context length is 4097 tokens. However, your messages resulted in **** tokens. Please reduce the length of the messages.</b>, значит вы привысили лимит текста. Следует очистить историю.
"""
bot.send_message(chat_id=message.chat.id, text=instructions, parse_mode='HTML')
Данный код отвечает на сообщение с текстом "Инструкция". При этом используется функция-обработчик сообщений bot.message_handler, которая вызывается, если сообщение от пользователя соответствует заданному условию.
Внутри функции instruction задана переменная instructions, которая содержит в себе текст инструкции для использования бота. Текст написан с использованием языка разметки HTML, чтобы выделить заголовки и сделать текст более читаемым.
После этого, функция вызывает метод bot.send_message(), который отправляет сообщение с текстом инструкции пользователю, который инициировал команду "Инструкция". В качестве параметров методу передаются идентификатор чата message.chat.id, текст сообщения text и режим разметки текста parse_mode, который указан как "HTML".
ФИНАЛЬНАЯ ЧАСТЬ
@bot.message_handler(content_types=['text'])
def msg(message):
if f"{message.chat.id}.txt" not in os.listdir('users'):
with open(f"users/{message.chat.id}.txt", "x") as f:
f.write('')
with open(f'users/{message.chat.id}.txt', 'r', encoding='utf-8') as file:
oldmes = file.read()
if message.text == 'Очистить историю':
with open(f'users/{message.chat.id}.txt', 'w', encoding='utf-8') as file:
file.write('')
return bot.send_message(chat_id=message.chat.id, text='История очищена!')
try:
send_message = bot.send_message(chat_id=message.chat.id, text='Обрабатываю запрос, пожалуйста подождите!')
completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "assistant", "content": oldmes},
{"role": "user","content": f'Предыдущие сообщения: {oldmes}; Запрос: {message.text}'}], presence_penalty=0.6)
bot.edit_message_text(text=completion.choices[0].message["content"], chat_id=message.chat.id, message_id=send_message.message_id)
with open(f'users/{message.chat.id}.txt', 'a+', encoding='utf-8') as file:
file.write(message.text.replace('\n', ' ') + '\n' + completion.choices[0].message["content"].replace('\n', ' ') + '\n')
with open(f'users/{message.chat.id}.txt', 'r', encoding='utf-8') as f:
lines = f.readlines()
if len(lines) >= NUMBERS_ROWS +1:
with open(f'users/{message.chat.id}.txt', 'w', encoding='utf-8') as f:
f.writelines(lines[2:])
except Exception as e:
bot.send_message(chat_id=message.chat.id, text=e)
bot.infinity_polling()
Самая важная часть кода! Это является обработчиком сообщений от пользователя и содержит следующие шаги:
- Проверяет, существует ли файл с историей сообщений пользователя (название файла имеет формат "id_пользователя.txt" и хранится в папке "users"). Если файл не существует, создает новый.
- Читает предыдущие сообщения пользователя из файла.
- Если пользователь отправляет сообщение "Очистить историю", то очищает историю в файле и отправляет сообщение пользователю о том, что история очищена.
- Отправляет запрос на создание ответа бота на основе предыдущих сообщений пользователя и нового сообщения. Для этого используется API OpenAI.
- Редактирует отправленное ранее сообщение "Обрабатываю запрос" и заменяет его на полученный ответ от OpenAI.
- Добавляет новое сообщение пользователя и ответ бота в файл с историей сообщений пользователя.
- Если количество сообщений в файле превышает заданное значение (задается константой NUMBERS_ROWS), то удаляет старые сообщения, чтобы файл не занимал слишком много места на диске.
- Если происходит ошибка при выполнении какого-либо из вышеперечисленных шагов, то отправляет сообщение об ошибке пользователю.
- Ожидает новых сообщений от пользователя и повторяет вышеописанный цикл для каждого нового сообщения.
ЗАКЛЮЧЕНИЕ
Максимально подробно мы разобрали код, на котором строится мой бот, но есть определённые недочёты, а именно:
- Добавление пользователя в чёрный список ничего не меняет для самого пользователя (отсутствует проверка файла blacklist и блокировка обработки новых сообщений от пользователя)
- Кнопка выхода из админ меню практически бесполезна (на неё есть свои планы, которые ещё не придумал как реализовать)
- При большом объёме информации 7 последних сообщений выходят за рамки openai api по символам (более 4096 символов) и бот выдаёт ошибку пользователю. Чтобы дальше пользоваться ботом необходимо очистить историю
Вот, кстати, ссылочка на мой бот. Можете протестировать его работу
Буду очень рад вашим лайкам и комментариям. Отвечаю я не так быстро и не так часто пишу статьи в связи с сильной загруженностью, но как только буду находить время обязательно напишу что-то новенькое! Подписывайтесь!