Контекст в чат-ботах играет ключевую роль в создании удобных и интерактивных взаимодействий с пользователем. Без него бот теряет связь с предыдущими сообщениями, что усложняет диалог. В этой статье мы рассмотрим, как реализовать систему контекстных диалогов на 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 имеет всё, что нужно при работе со сценариями, но очень громоздкий, из-за чего писать с ним маленькие приложения становится очень сложно, каждый имеет место быть, а вам желаю писать поменьше костылей и не писать как я. Спасибо за прочтение.
Комментарии (10)
gfiopl8
01.01.2025 16:40А есть что-нибудь для телебота для меню выбора из большого списка или дерева?
tiver Автор
01.01.2025 16:40Не совсем понял, для выбора из большого списка можно использовать инлайн клавиатуру, или если он очень большой обозначаешь каждое значение числом, вот так например
1) выбор 1
2) выбор 2
и дальше запрашиваешь у пользователя число.
А что вы подразумеваете под деревом я не понял, можете уточнить?gfiopl8
01.01.2025 16:40Большой список это когда листать приходится, вправо влево, или может как то получше, по 10 страниц вперед/назад не знаю. Может такое лучше как то через @ делать.
Дерево - то же самое но не список а дерево Ж)
gfiopl8
01.01.2025 16:40Ну например надо выбрать 1 язык из 100.
Текстовое сообщение Выберите язык, и к нему прикреплена клавиатура с кнопками на которых названия языков написаны.
Все 100 вариантов сделать нельзя, у телеги есть глюк, если так много кнопок сделать то бот помрет, надо будет токен перевыпускать, но даже если бы можно было всё равно они не вместятся ни в какой экран. Обычно в таких случаях список делят на небольшие куски и внизу добавляют кнопки для навигации по кускам(страницам).
Такой себе вариант но хотя бы такой бы найти, готовый.
Есть еще "инлайн" боты, это когда в ты в строке сообщения начинаешь писать @имя_бота и дальше запрос. Типа @gifмем с котятами, и у тебя вылезает особое окно с результатами, возможно такой вариант можно будет адаптировать под выбор из большого списка.
tiver Автор
01.01.2025 16:40Вот что-то подобное для списка:
import telebot bot = telebot.TeleBot('YOUT_TOKEN') # список языков languages = ['русский', 'английский', 'немецкий', 'очень много языков', 'другие языки', 'ещё языки'] # я поставлю два на страницу, мне лень много перечеслять @bot.message_handler(commands=['start']) def start_command(message): keyboard = get_keyboard(0, languages, 2) # первая страница с двумя языками # меню где нужно выбрать bot.send_message(message.chat.id, 'привет, выбери язык:', reply_markup=keyboard) def get_keyboard(index, list_of_languages, languages_in_page): languages_on_page = languages[index:index+languages_in_page] print(languages_on_page) print(index) keyboard = telebot.types.InlineKeyboardMarkup(row_width=2) # замени взависимости от размера текста for language in languages_on_page: print(language) keyboard.add(telebot.types.InlineKeyboardButton(text=language, callback_data=language+"_"+"language_choose")) row = [] if index > 0: row.append(telebot.types.InlineKeyboardButton(text='назад', callback_data='prev'+"_"+str(index-languages_in_page))) if len(list_of_languages) > index + languages_in_page: row.append(telebot.types.InlineKeyboardButton(text='далее', callback_data='next'+"_"+str(index+languages_in_page))) keyboard.row(*row) return keyboard @bot.callback_query_handler(func=lambda call: 'next' in call.data) def next_page(call: telebot.types.CallbackQuery) -> None: bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, reply_markup=get_keyboard(int(call.data.split('_')[1]), languages, 2)) @bot.callback_query_handler(func=lambda call: 'prev' in call.data) def next_page(call: telebot.types.CallbackQuery) -> None: bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, reply_markup=get_keyboard(int(call.data.split('_')[1]), languages, 2)) @bot.callback_query_handler(func=lambda call: 'language_choose' in call.data) def language_choose(call: telebot.types.CallbackQuery) -> None: bot.send_message(call.message.chat.id, f'Вы выбрали язык: {call.data.split("_")[0]}') bot.polling()
получается что-то такое:
насчёт инлайн ботов не уверен, я никогда не сталкивался и вообще не уверен что такое возможно в telebot.
slair
хотелось бы ясности
tiver Автор
словарь
states = {user_id: "state"}
, user_id: int, а стейт - строка, и далее в хендлере сверять с каждым возможным и вызываю соответствующую функцию, напримерif states[message.from_user.id] == "test": test(message)
, также можно создавать пустые класс как состояния, но тогда как по мне легче второй способ использовать.starwalkn
Возможно, комментарий был по поводу ключей. Ключи - идентификаторы, и снова ключи - стейты. Перепутали со значением вместо ключа.