Контекст диалога в pytelegrambotapi

от автора

Контекст в чат-ботах играет ключевую роль в создании удобных и интерактивных взаимодействий с пользователем. Без него бот теряет связь с предыдущими сообщениями, что усложняет диалог. В этой статье мы рассмотрим, как реализовать систему контекстных диалогов на Python с использованием библиотеки telebot. Мы покажем, как управлять состоянием диалога, сохранять контекст и обрабатывать несколько пользователей одновременно, делая бота более умным и персонализированным.

Зачем нужен контекст диалога?

Я думаю, вы часто, пользуясь телеграмм-ботами, вводите какую-то кастомную информацию, например цену товара, при этом не задумываясь, как сервис понимает, что ваше сообщение — это именно цена, а не имя или описание, очевидно, по предыдущему сообщению, где бот спрашивает о цене, спросил — ответили. Это и называют контекстом разговора. Другой пример — любой анонимный чат, боту нужно помнить, с каким пользователем вы говорите, он это знает, ведь он вас с ним и связал. Таким образом, без контекста диалога вам придётся каждый раз сообщать, что это такое, а далеко не все хотят разбираться, на кой чёрт вам 100500 команд с названием /add_name, /add_price, /add_description, и рано или поздно все запутаются.

Способы реализации

Я буду рассматривать именно библиотеку telebot, хотя в других, более продвинутых, есть крутые встроенные инструменты, например в python-telegram-bot, но в них чёрт ногу сломит, поскольку я немного ленивый низкоквалифицированный, я остановлюсь на pytelegrambotapi.
Первое, что придёт в голову большинству, это создать словарь для всех текущих диалогов, что-то по типу:

states = {user_id: "name"}

Если ваш бот состоит из больше чем пары команд и менюшек, скорее вы запутаетесь, как рулить, городя огород в универсальном хендлере, чем напишите что-то работающее.

Далее за ответом я полез к универсальному хранителю знаний — Чату ГПТ, который уже посоветовал более релевантное решение, встроенное в самого telebot’а: стейты, это уже более продвинутое решение, которое подходит для получения пользовательского ввода, но всё же не лишено минусов, например контекст всё ещё отсутствует, это просто стейты, хоть теперь мы и можем придерживаться технологии вопрос-ответ, но диалога выстроить нормально не получится без создания сторонних словарей.

3-й способ был собран мной из подручных материалов на коленке за 2 дня, аналогов я не нашёл, и поэтому заявляю, что это мой гриб и я его ем. Это библиотека telebot-dialogues, она позволяет создавать диалоги для пользователей с сохранением контекстных переменных, истории сообщений и приостановкой диалога.

Также хочу обратить внимание что message.from_user.id НЕ эквивалентно call.message.from_user.id, на деле message.from_user.id = call.message.chat.id

Теперь рассмотрим каждый способ подробней.

Метод в лоб

Смысл прост: создаём словарь, где ключи — это айдишники юзеров, а значения — их состояния, и погнали проверять их в хендлере всего, в общем виде реализация выглядит так:

классно, да? Вроде работает, но что если нам нужно чуть больше чем имя, возраст например. Пока логично, просто изменяем take_name и главный хендлер:

@bot.message_handler(content_types=["text"]) def handle_text(message):     state = states.get(message.from_user.id)     match state:         case 'name':             take_name(message)         case 'age':             take_age(message)              def take_name(message):     bot.send_message(message.chat.id, f'твоё имя: {message.text}! Введи свой возраст')     states[message.from_user.id] = 'age'

и соответственно добавляем:

def take_age(message):     bot.send_message(message.chat.id, f'твой возраст: {message.text} лет!')     states[message.from_user.id] = None

А сохранять эти данные как? Вроде логично прикурить сохранение в файл или БД, но что, если сохранить нужно всё разом, тогда придётся использовать ещё один костыль: временное хранилище.

temp_info_save = {}

и соответственно изменяем всё под новые суровые реалии:

def take_name(message):     bot.send_message(message.chat.id, f'твоё имя: {message.text}!')     temp_info_save[message.from_user.id] = {'name': message.text}     states[message.from_user.id] = 'age'  def take_age(message):     bot.send_message(message.chat.id, f'твой возраст: {message.text} лет!')     temp_info_save[message.from_user.id]['age'] = message.text     states[message.from_user.id] = None 

Конечно, можно добавить и сохранение истории сообщений, и состояния и т. д., но я думаю, вы скорее головой тронетесь, чем напишите это в более-менее адекватном виде, так что этот способ можно хоронить.

Окончательный код:

from telebot import TeleBot  bot = TeleBot('YOUR_BOT_TOKEN')  states = {} temp_info_save = {}  @bot.message_handler(commands=['start']) def start_command(message):     states[message.user_from.id] = 'name'     bot.send_message(message.chat.id, 'Привет! Введи своё имя.')  @bot.message_handler(content_types=["text"]) def handle_text(message):     state = states.get(message.from_user.id)     match state:         case 'name':             take_name(message)             bot.send_message(message.chat.id, f'Привет, {message.text}! Введи свой возраст.')          case 'age':             take_age(message)             bot.send_message(message.chat.id, 'Теперь ты можешь отправлять мне сообщения.')   def take_name(message):     bot.send_message(message.chat.id, f'твоё имя: {message.text}!')     temp_info_save[message.from_user.id] = {'name': message.text}     states[message.from_user.id] = 'age'  def take_age(message):     bot.send_message(message.chat.id, f'твой возраст: {message.text} лет!')     temp_info_save[message.from_user.id]['age'] = message.text     states[message.from_user.id] = None   if __name__ == '__main__':     bot.polling()

Уже имеющийся контроллер состояний

По сути этот метод не многим отличается от предыдущего, но он хотя-бы выглядит прилично.

import telebot from telebot import types from telebot.handler_backends import State, StatesGroup  bot = telebot.TeleBot('YOUR_BOT_TOKEN')  class UserState(StatesGroup):     waiting_for_name = State()  # Состояние ожидания имени  @bot.message_handler(commands=['start']) def start_command(message):     bot.set_state(message.chat.id, UserState.waiting_for_name)     bot.send_message(message.chat.id, 'Привет! Введи своё имя.')  @bot.message_handler(content_types=["text"], state=UserState.waiting_for_name) def take_name(message):     user_name = message.text     bot.send_message(message.chat.id, f'Твоё имя: {user_name}!')          bot.delete_state(message.chat.id)  # Запуск бота if __name__ == '__main__':     bot.polling() 

Великолепно, очевидным плюсом будет отсутствие центрального хендлера, на этом плюсы кончились, в остальном это такой же словарь, только оформленный в виде класса, а вместо строк состояний стейты, но для сохранения данных между сообщениями также понадобится создавать словарь, вот пример реализации:

import telebot from telebot import types from telebot.handler_backends import State, StatesGroup  bot = telebot.TeleBot('YOUR_BOT_TOKEN')  temp_info_save = {}  class UserState(StatesGroup):     waiting_for_name = State()  # Состояние ожидания имени     waiting_for_age = State()   # Состояние ожидания возраста  @bot.message_handler(commands=['start']) def start_command(message):     # Устанавливаем состояние ожидания имени     bot.set_state(message.chat.id, UserState.waiting_for_name)     bot.send_message(message.chat.id, 'Привет! Введи своё имя.')  @bot.message_handler(content_types=["text"], state=UserState.waiting_for_name) def take_name(message):     user_name = message.text     bot.send_message(message.chat.id, f'Твоё имя: {user_name}!')          temp_info_save[message.chat.id] = {'name': user_name}          bot.set_state(message.chat.id, UserState.waiting_for_age)     bot.send_message(message.chat.id, f'Привет, {user_name}! Введи свой возраст.')  @bot.message_handler(content_types=["text"], state=UserState.waiting_for_age) def take_age(message):     user_age = message.text     bot.send_message(message.chat.id, f'Твой возраст: {user_age} лет!')          temp_info_save[message.chat.id]['age'] = user_age          bot.delete_state(message.chat.id)          bot.send_message(message.chat.id, 'Теперь ты можешь отправлять мне сообщения.')  if __name__ == '__main__':     bot.polling()

Да, это ровно то же самое, что мы делали в предыдущем способе, только по-умному, подводя итог: способ сильно лучше, но по сути является тем же самым.

Мой вариант

Потратив два дня не очень активной мыслительной деятельности я сообразил библиотеку telebot-dialoguesустановка как обычно:

pip install telebot-dialogue

И да, это такой же словарь, но с красивой обёрткой, но моя обёртка — приемлемый интерфейс для взаимодействия, всё строится на двух слонах и одном слонёнке: Dialogue, DialogueManager и DialogueUpdater, первый, как следует из названия, является объектом диалога, второй — менеджером, а третий — это подкласс менеджера, чтобы не делать кучу функций под обновление переменных, вы просто редактируете информацию в контекстом менеджера.

from telebot import TeleBot from telebot_dialogue import DialogueManager, Dialogue  bot = TeleBot("Your_BOT_TOKEN") dialogue_manager = DialogueManager()  @bot.message_handler(commands=['start']) def start_command(message):     user_id = message.from_user.id     dialogue = Dialogue(user_id, take_name, end_func=end_dialogue)     dialogue_manager.add_dialogue(dialogue)      # если диалог с этим пользователем уже есть то новый не начнётся, force=True заменяет диалог     bot.send_message(message.chat.id, 'Привет! напиши своё имя:')  def end_dialogue(dialogue):     bot.send_message(dialogue.user_id,                      f'Завершение диалога, вот твоя инфа имя: {dialogue.get_context('name')}, '                      f'возраст: {dialogue.get_context('age')}')   def take_age(message, dialogue):     age = int(message.text)     with dialogue_manager.update(dialogue.user_id) as update_dialogue:         update_dialogue.update_context('age', age)         bot.send_message(message.chat.id, 'Спасибо! Тебе {} лет.'.format(age))         context = update_dialogue.get_context('name')          print(context) # типа сохранение в дб         update_dialogue.delete_dialogue()   def take_name(message, dialogue):     name = message.text     with dialogue_manager.update(dialogue.user_id) as update_dialogue:         update_dialogue.update_context('name', name)         bot.send_message(message.chat.id, 'Доброго утра, {}! Сколько тебе лет?'.format(name))         update_dialogue.handler = take_age  @bot.message_handler(content_types=['text']) def handle_text(message):     if not dialogue_manager.handle_message(message): # Если диалога нет, то отправляем приветствие         bot.send_message(message.chat.id, 'Привет! Это бот для общения с пользователем. напиши /start, чтобы начать.')   if __name__ == '__main__':     bot.polling()

Громоздко? Очень, зато потенциал куда больше, чем у предыдущих способов, это может быть использовано при публикации объявления, когда нужно заполнить форму, и хранить всё отдельно неудобно. А теперь можно перейти к более сложным функциям этой библиотеки:

История

Все сообщения сохраняются в истории сообщений, в виде обычного списка, вы можете получить к ним доступ как-то так:

def end_dialogue(dialogue):     bot.send_message(dialogue.user_id,                      f'Завершение диалога, вот твоя инфа имя: {dialogue.get_context('name')}, '                      f'возраст: {dialogue.get_context('age')} твоё первое сообщение - {dialogue.history[0].text}.')

Статус

например если в анонимном чате пользователь решит приостановить общение, то вызывается dialogue.stop_dialogue(), вызывающая функцию pause_func, dialogue.continue_dialogue() делает ровно противоположное, вызывает функцию continue_func, dialogue.delete_dialogue() вызывает end_func и завершает диалог. Функции pause_func, continue_func и end_func передаются при создании диалога. При приостановке и возобновлении меняется статус. Хендлер работает только с state=true

Контекст и сброс

update_context(key, value) добавляется в контекст данные в формате ключ значение,
get_context(key, default=None) получает данные из контекста по ключу
clear_context и clear_history делают ровно то, что вы от них ожидаете
reset_dialogue сбрасывает диалог до заводских(уничтожает историю и контекст)

Менеджер

continue_dialogue, finish_dialogue и stop_dialogue являются оболочками и просто вызывают соответствующие функции у диалога по user_id

Контекстный updater

Вы можете менять любые параметры диалога через контекстный менеджер и функцию update у менеджера,

with manager.update(user_id) as dialogue:   # любые действия с дилогом   dialogue.handler = new_handler  print(manager.find_dialogue(user_id).handler) # new_handler

Пример анонимного чата со всеми функциями

from telebot import TeleBot from telebot_dialogue import DialogueManager, Dialogue  bot = TeleBot("YOUR_BOT_TOKEN") dialogue_manager = DialogueManager()   @bot.message_handler(commands=['start']) def find_conversation(message):     user_id = message.from_user.id     partner = '123' # поиск собеседника     if not dialogue_manager.find_dialogue(user_id):         dialogue = Dialogue(user_id, conversate, end_func=end_dialogue, context={'partner': partner})         dialogue_manager.add_dialogue(dialogue)         bot.send_message(message.chat.id, 'Привет! мы нашли тебе собеседника, можешь с ним поговорить:')              with dialogue_manager.update(user_id) as update_dialogue:         update_dialogue.clear_context()         update_dialogue.update_context('partner', partner)   def end_dialogue(dialogue):     bot.send_message(dialogue.user_id, 'Диалог завершен.')     bot.send_message(dialogue.get_context('partner'), 'Твой собеседник завершил диалог.')   def conversate(message, dialogue):     bot.send_message(dialogue.get_context('partner'), message.text)   @bot.message_handler(commands=['pause']) def pause_dialogue_handler(message):     user_id = message.from_user.id     if dialogue_manager.stop_dialogue(user_id):         return      bot.send_message(message.chat.id, 'у тебя нет активного диалога. Напиши /start, чтобы начать.')  @bot.message_handler(commands=['continue']) def continue_dialogue_handler(message):     user_id = message.from_user.id     if dialogue_manager.continue_dialogue(user_id):         return      bot.send_message(message.chat.id, 'у тебя нет активного диалога. Напиши /start, чтобы начать.')  @bot.message_handler(content_types=['text']) def handle_text(message):     if not dialogue_manager.handle_message(message): # Если диалога нет, то отправляем приветствие         bot.send_message(message.chat.id, 'У тебя сейчас нет диалога. Напиши /start, чтобы начать.')   def pause_dialogue(dialogue):     bot.send_message(dialogue.user_id, 'Диалог приостановлен.')     bot.send_message(dialogue.get_context('partner'), 'Твой собеседник приостановил диалог.')   def continue_dialogue(dialogue):     bot.send_message(dialogue.user_id, 'Диалог продолжен.')     bot.send_message(dialogue.get_context('partner'), 'Твой собеседник продолжил диалог.')   if __name__ == '__main__':     bot.polling()

Вывод

По сути, это всё, с тонкостями разберетесь сами. Все 3 метода не идеальны и имеют свои плюсы и минусы: словарь быстро развернуть, но в нем легко запутаться при масштабировании, встроенные стейты выглядят красивее и понятнее, но всё ещё не имеют многих функций из коробки, telebot-dialogue имеет всё, что нужно при работе со сценариями, но очень громоздкий, из-за чего писать с ним маленькие приложения становится очень сложно, каждый имеет место быть, а вам желаю писать поменьше костылей и не писать как я. Спасибо за прочтение.


ссылка на оригинал статьи https://habr.com/ru/articles/871180/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *