如何在单独的文件中继续嵌套对话

How can I continue a nested conversation in a separate file

我不是专业程序员,但我正在尝试使用 ConversationHandlers 构建一个 python-telegram-bot 用于工作。基本上,我为用户提供了一个选项菜单,总结为:

如果选择“完成调查”,则机器人会询问用户 ID。根据用户 ID,我为用户分配了 30 多个不同调查中的 1 个(我正在尝试使用 child 对话)。随着时间的推移,此调查列表将会增加,并且每个调查都有独特的问题和步骤。

考虑到调查的数量,我考虑将每个调查作为一个 child 对话来管理,它有自己的 ConversationHandler,并且 运行 从一个单独的 file/module 中管理它(以保持事情动态的,没有一个包含 n+ 个变量的大文件要考虑)。

问题是,如何从单独的文件继续 child 对话?还有另一种方法可以解决这个问题吗?我知道该机器人仍在 运行ning 从主文件并检查更新。我想 运行 每项调查,一旦完成,return 到 INITIAL bot 菜单(parent 对话)。

我发现了之前的讨论,但我的知识几乎没有超出 python-telegram-bot 示例,所以我很难理解:https://github.com/python-telegram-bot/python-telegram-bot/issues/2388

这是我正在尝试执行的示例摘要代码:

main_file.py

from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, InlineKeyboardMarkup, InlineKeyboardButton, Update, KeyboardButton, Bot, InputMediaPhoto
from telegram.ext import Updater, CommandHandler, MessageHandler, Filters, ConversationHandler, CallbackQueryHandler, CallbackContext
import surveys  # this file contains each survey as a function with its own ConversationHandler

token = ''

MENU, USER, CHAT_ID, USER_ID, FINISHED = map(chr, range(1,6))

END = ConversationHandler.END


def start(update: Update, context: CallbackContext) -> int:
    """Initialize the bot"""

    context.user_data[CHAT_ID] = update.message.chat_id
    text = 'Select an option:'
    reply_keyboard = [
        ['Complete Survey'],
        ['EXIT'],
    ]

    context.bot.send_message(
        context.user_data[CHAT_ID],
        text=text,
        reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
    )

    return MENU


def exit(update:Update, context:CallbackContext) -> None:
    """Exit from the main menu"""
    context.bot.send_message(
        context.user_data[CHAT_ID],
        text='OK bye!',
        reply_markup=ReplyKeyboardRemove()
    )
    return END


def abrupt_exit(update:Update, context:CallbackContext) -> None:
    """Exit the main conversation to enter the survey conversation"""

    return END


def survey_start(update:Update, context:CallbackContext) -> None:
    """Asks for the user_id in order to determine which survey to offer"""
    text = 'Please type in your company ID'
    context.bot.send_message(
        context.user_data[CHAT_ID],
        text=text,
        reply_markup=ReplyKeyboardRemove()
    )
    return USER


def survey_select(update:Update, context:CallbackContext) -> None:
    """Search database to find next survey to complete"""
    user = str(update.message.text)
    chat_id = context.user_data[CHAT_ID]
    context.user_data[USER_ID] = user

    """Search database with user_id and return survey to complete"""
    survey = 'survey_a'     # this value is obtained from the database

    runSurvey = getattr(surveys, survey)   # I used getattr to load functions in a different module
    runSurvey(Update, CallbackContext, user, chat_id, token)

    return FINISHED


def main() -> None:
    updater = Updater(token, use_context=True)

    # Get the dispatcher to register handlers
    dispatcher = updater.dispatcher

    # Survey conversation
    survey_handler = ConversationHandler(
        entry_points=[
            MessageHandler(Filters.regex('^Complete Survey$'), survey_start),
        ],

        states={
            USER: [
                MessageHandler(Filters.text, survey_select),
            ],
            FINISHED: [
                # I'm guessing here I should add something to exit the survey ConversationHandler
            ],
        },
        fallbacks=[
            CommandHandler('stop', exit),
        ],
    )


        # Initial conversation
    conv_handler=ConversationHandler(
        entry_points=[
            CommandHandler('start', start),
        ],
        states={
            MENU: [
                MessageHandler(Filters.regex('^Complete Survey$'), abrupt_exit),
                MessageHandler(Filters.regex('^EXIT$'), exit),
            ],
        },
        allow_reentry=True,
        fallbacks=[
            CommandHandler('stop', exit),
        ],
    )

    dispatcher.add_handler(conv_handler, group=0)  # I used separate groups because I tried ending
    dispatcher.add_handler(survey_handler, group=1)  # the initial conversation and starting the other

    # Start the Bot
    updater.start_polling()
    updater.idle()

if __name__ == '__main__':
    main()

surveys.py 这是每个调查都有自己的对话和调用函数的地方。基本上我输入 survey_A (先前选择)并尝试将其用作 main()

from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, InlineKeyboardMarkup, InlineKeyboardButton, Update, \
    KeyboardButton, Bot, InputMediaPhoto
from telegram.ext import Updater, CommandHandler, MessageHandler, Filters, ConversationHandler, CallbackQueryHandler, \
    CallbackContext

NEXT_QUESTION, LAST_QUESTION, CHAT_ID = map(chr, range(1,4))

END = ConversationHandler.END

def exit(update:Update, context:CallbackContext) -> None:
    """Exit from the main menu"""
    context.bot.send_message(
        context.user_data[CHAT_ID],
        text='OK bye!',
        reply_markup=ReplyKeyboardRemove()
    )
    return END


def first_q(update:Update, context:CallbackContext, chat_id:str) -> None:
    """First survey_A question"""

    context.bot.send_message(
        chat_id,
        text='What is your name?',
        reply_markup=ReplyKeyboardRemove()
    )
    return NEXT_QUESTION


def last_q(update: Update, context: CallbackContext) -> None:
    """Last survey_A question"""

    update.message.reply_text(
        'How old are you?', reply_markup=ReplyKeyboardRemove()
    )
    return LAST_QUESTION

def survey_a(update:Update, context:CallbackContext, user:str, chat_id: str, token:str) -> None:
    """This function acts like the main() for the survey A conversation"""
    print(f'{user} will now respond survey_a')
    CHAT_ID = chat_id   # identify the chat_id to use
    updater = Updater(token, use_context=True)   # here I thought of calling the Updater once more

    survey_a_handler = ConversationHandler(
        entry_points=[
            MessageHandler(Filters.text, first_q),
        ],
        states={
            NEXT_QUESTION: [
                MessageHandler(Filters.text, last_q),
            ],
            LAST_QUESTION: [
                MessageHandler(Filters.text, exit),
            ],

        },
        allow_reentry=True,
        fallbacks=[
            CommandHandler('stop', exit),
        ],
    )

    updater.dispatcher.add_handler(survey_a_handler, group=0)  # I only want to add the corresponding
                                                               # survey conversation handler

    first_q(Update, CallbackContext, CHAT_ID)

我 运行 代码,它在 first_q 中的 surveys.py 第 23 行中断: context.bot.send_message( AttributeError: 'property' object 没有属性 'send_message'

我假设我与对话处理程序的逻辑有偏差。

感谢任何帮助

我已经开发电报机器人大约一年了,我希望最好的方法是先构建您的项目。让我详细解释一下。

“文件夹”

Folder structure

基本上,所有代码都在项目的src文件夹中。在 src 文件夹中有另一个名为 components 的 sub-folder,其中包含您想要处理的机器人的所有不同部分(即您的 quiz_1、quiz_2 , ...) 和 main.py 文件,其中包含机器人的 'core'。但是,在项目的根目录(就是您的项目文件夹)中,您可以看到 bot.py 文件,它就像一个 运行ner 文件。所以除了:

import src.main from main

if '__name__' == '__main__':
    main()

提示

关于你的问卷:

  • 我建议只使用字符串作为状态的键,而不是将它们映射到随机值。基本上你可以像 "MAIN_MENU", "STATE_ONE" , "STATE_TWO"等等,但是一定要return回调函数中的同一个字符串!
  • PTB库的整体逻辑是这样的: Telegram API 服务器 -> PTB Updater() class -> Dispatcher()(在你的代码中是 updater.dispatcher) -> 处理程序 -> 回调函数 -> <- 用户。 箭头指向用户并返回回调函数的原因是因为您的机器人逻辑和用户之间存在交互,因此用户的响应返回到您的回调函数代码。
  • 我建议不要选择像 'first_question' 或 'second_question' 这样的回调函数名称。而是将其命名为 get_question() 使用该函数从其他来源检索问题数据,以便它可以是动态的。因此,例如,您将有一个包含不同问题的字典,其中包含问题编号的键 - 很简单,对吧?然后您将编写一个函数,该函数将根据其状态向用户发送问题,并使用字典中的正确键选择正确的问题。通过这种方式,您可以向字典中添加更多问题,而无需更改函数中的代码,因为它将是动态的(只要您编写的函数正确即可)。
  • 在你的 main.py 文件中只有一个 main() 函数可以保存 Updater() 和给定的标记,因为你 不能 多个具有 相同令牌 的 Updater()。这就像一个机器人只能被一次轮询的一个应用程序访问。轮询 - visit here.

好消息!

为了支持您的机器人开发并遵循结构化项目创建,我创建了一个 repo on GitHub,其项目结构与我今天试图向您解释的项目结构几乎相同。随意检查它,克隆它并玩耍。只需将您的令牌添加到 .env 文件和 运行 机器人。

更多资源

同时查看这些项目:

正如您将在其中看到的那样,main.py 包含所有处理程序,src 文件夹包含所有不同的 'components',它们更像是机器人的不同部分。

如果您需要任何帮助,我很乐意回答您的任何问题。