Контекст в чат-ботах играет ключевую роль в создании удобных и интерактивных взаимодействий с пользователем. Без него бот теряет связь с предыдущими сообщениями, что усложняет диалог. В этой статье мы рассмотрим, как реализовать систему контекстных диалогов на 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/
Добавить комментарий