Repository navigation
[Discussion:] ConversationHandler #2770
Copy link
Copy link
Open
Description
Several ideas for changes and/or additions have been gathered in the v14 project, mentioned in the user group or issues or dwelled in the back of my head. So I'm writing this issue to keep track of them better.
The following list of ideas should be discussed before release of v14 because at least some of them are easier to implement if we do them non-backwards compatible. I'd like to emphasize that most of these are very rough first thoughts and I'm fully aware that we may scrape many of them.
Related: #2802
- Make
CH.fallbacksoptional. I've seen a lot of people (including me) setting at to an empty list for some use cases … Poolitzer already expressed support for this idea ConverstanionHandler + run_async: Problem is that we can't persist futures/promises. But we should be able to guarantee that all futures are done on (clean) shutdown. One could add something likeThis already get's handled on v20BasePersistence.resolve_conversation_promisesthat persistence classes can call before flushing on shutdown.- [FEATURE] ConversationHandler: Allow special handlers/callbacks for entering states #2390 (closed just for better overview, not actually implemented)
- add a "prefallbacks" list, i.e. the state-handlers only trigger, if the "prefallback" handlers don't trigger. The current alternative is to add those handlers to the
fallbackslist, which requires to make all state handlers ignore updates that should be handled by thefallbacks. see also Added impementation of "prefallbacks" list. #2764 #2764; Implemented "prefallbacks" feature #2960 - Include
entry_points,fallbacks(and eventual "prefallbacks") into thestatesdict. We already have special states forTIMEOUTandWAITING, so I think it would make the interface cleaner if we used special states(PRE)FALLBACKandENDas keys for these lists rather than making them standalone kwargs of__init__ - Think about "conversation related data", e.g.
context.conversation_data. It should be an object unique per conversation key andConversationHandlerinstance. It may even be automatically deleted when the conversation ends . See also [FEATURE] Have per-conversation data (or at least an identifier) in ConversationHandler #4136 - Rethink which parts of
ConversationHandlerwe consider public.- e.g. the
conversationsis currently public, although undocumented and basically part of the internals. If we make it private, we can rework the setters ofpersistenceandconversationsto something likeget_persisted_data(persistence)orinitialize_persistence(persistence) - We also rather regularly get questions like "how do I access the current state of the conversation" and "how do I check if a user has an active conversation". Making the logic of how the keys are build and how
conversationsworks public may help with some of these and also with the abovecontext.conversation_data - One could take this one step further and let the user customize how the key is generated. This could be useful e.g. in situations where one wants to handle 3rd-party updates (e.g. login into an external service) within the conversation, which are not of type
telegram.Update. see also ConversationHandler do not support per_message_thread_id conversations #4768 for a request to make the CH state per message thread ID
- e.g. the
- think about
conversation_timeout:- Is it worth to allow timeout handlers to put the conversation in a non-end state?
- it it worth to try & allow a different timeout duration per state?
- Can we make them work for nested conversations?
- Can we ensure that the timeout jobs can be persisted?
- [FEATURE] class-based nested conversations #2827 to make CH be based more on a class-like structure
- nested conversations:
- Check behavior of the
WATINGstate, especially with nested conversations - Check behavior of async handlers with nested conversations
- Once a child conversation has started one may want the next update to be processed exclusively by that child conversation, even if it's not the first handler in the state. Maybe this can be made possible. See also https://stackoverflow.com/questions/78143788
- Check behavior of the
- Think about a mechanism to lazy-load persisted conversation states to allow to reduce load on startup. would be especially useful for stateless server designs. See here for some initial discussion. Note also that in stateless setups, a
refresh_conversation_datawould be useful, see [QUESTION] How to work with persistent conversation data across multiple processes #4452. - When the conversation has ended, pass
CH.ENDtopersistence.update_conversationinstead ofNone - We currently issue a warning for
per_message=True, per_chat=False- this is not necessarily valid in caseinline_message_idsare used entry_pointsshould be checked after the states are checked - that way, state handlers may trigger before anyentry_pointstrigger viaallow_reentry. See https://t.me/pythontelegrambotgroup/606782?thread=606781 for a question on that. Edit: On second thought, theallow_reentrymay not be needed at all - the user can just repeat theentry_pointsin thefallbacks. If "prefallbacks" (see above) are added, that even allows the user to customize which of state handlers andentry_pointsare checked first- A commonly requested feature is to track a "conversation" between two (or more) users who each chat with the bot in the respective private chat. If we could make something like this feasible, that would be awesome.
- discuss possibility to make job timeouts persistent: [FEATURE] Persist timeouts of ConversationHandler #3522
- Improve type hinting where possible - see Sub-Optiomal type hints in
ConversationHandler#3665 (closed only to track this here) - Errors in the timeout state should be handled like errors in handlers, not like errors in jobs - see https://t.me/pythontelegrambotgroup/675226
- In case built-in states are used, they should either not be easy to reproduce or we should warn if the user tries to use them manually. See [FEATURE] warn if
ConversationHandler.stateshas a key -1 #3834 - Rethink if we want to allow for some error handling on CH or even state level. See From error state to other state on a Conversational bot #2277 for some discussion
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Type
Fields
Priority
None yet
Effort
None yet
Activity
kirsanium commentedon Jun 20, 2022
I request for a tree-like behaviour on reentering nested
ConversationHandlers - it seems reasonable toConversationHandler.ENDall child handlers when you reenter the root one. Found no intended way to do it and my current solution is extremely hacky.Poolitzer commentedon Aug 1, 2022
The grammy dev recently contacted me and pointed towards their implementation of conversation handlers. Since they are pure async as well, this might be interesting to consider. May be too much for either V20 or PTB in general and rather ptbcontrib, just wanted to write it down.
https://t.me/grammyjs_news/36
Bibo-Joshi commentedon Aug 1, 2022
Interesting! Thanks for mentioning. A similar idea has come up in #3042 before. While my comments on that PR still hold, one can ofc still keep the concept in mind. Redesigning CH as a whole can probably come to many different results.
Bibo-Joshi commentedon Jan 6, 2023
I've read through the grammy docs a bit and have a few comments on what I learned:
Most important IMO is the observation, that the async conversation-function is not just simply executed. Instead on every
await conversation.wait(), the function is basically ended and on the next incoming update the function is re-run up to that line. This is e.g. important for persistency. This mechanism makes it necessary toTL;DR: While this approach allows you to write easy-to-read code in a straightforward way at first glance, it's not quite so easy on second glance and the implementation is everything but easy.
A second thing that I want to point out is the usage of
conversation.wait/waitFor(…)and the usage of filter quries inwaitFor. As far as I see by quickly skimming through the grammy docs, these filter queries are grammys analogue ofBaseHandler.check_update(while in PTB filters are used exclusively forMessage-handlers). With this background,wait(For)is a vary natural way to specify what update to wait for in the conversation. In PTB, the background is different and a similar approach would probably mean to eather abandon theBaseHandlersetup or duplicate it into a filter-like setup.Grammys approach definitely has several perks, surely works well in grammys ecosystem and I have huge respect for whoever built it. But my summary is that grammys approach is not systematically superior to a classic FSM.
KnorpelSenf commentedon Jan 6, 2023
Glad to see that these ideas are spreading! It took a fair bit of research and many iterations and brainstorming to come up with the concept. And yes, it was a bit challenging to build, too :)
In case you're going to consider something similar for PTB, note that you have control over the event loop in Python (in contrast to JS), which greatly simplifies the way how conversation builder functions are run. You can simply stop running the loop, while grammY conversations sort of have to implicitly intercept its execution via a special promise and then hack ourselves back into control.
On the question why I personally dislike FSMs and similar abstractions, there's a good high-level summary of the ambition behind conversations in this interview starting at 26:34 min: https://podrocket.logrocket.com/grammy
Looking forward to seeing what you'll come up with! Good luck!
Bibo-Joshi commentedon Jan 12, 2023
@KnorpelSenf thanks for your comment! I did a bit more reading and thinking on the topic, but haven't made any significant progress so far :D TBH I also don't quite understand your comment on controlling (and stopping) the event loop in Python. If I stop the event loop, then how will I process new updates?
If you're interested you can also contact me on Telegram. I'd be happy to have a more in-depth discussion in private chat :)
Bibo-Joshi commentedon Jan 22, 2023
A few insights into the grammyjs conversation setup/how something like this could look like in python from offline discussion with KnorpelSenf:
var = await conversation.external(some_function_that_must_not_be_replayed(…)), the return value ofsome_function_that_must_not_be_replayedmust be persisted for the replay-mechanism to work properly. This can be an issue with general objects, e.g. sychronization primitives like locks are generally not serializable.await conversation.external(some_coroutine_function(…))can be removed by implementing a custom event loop. I.e.await some_coroutine_function(…)automatically takes care of storing the return value and the replay-logic. See here for a reference. However, this still only works for coroutine functions and for other functions that must only be run once (e.g.print(…),random), one would still need something likeconversation.externalrejedai commentedon Apr 3, 2023
I apologize for the possible duplication, I did not find this in the branch.
I think it is necessary to add a simple way to get the conversation by name and get the current state from the conversation.
Example:
Maybe I have bad notification design, but I don't need to send a notification when a person has passed a certain state. I used to bypass this with the help of a persist, I took data from the database, now when the persist is not realtime, I have to bypass it with the help of cyclic checking of handlers, which I think is not correct.
my current user state code:
4 remaining items
Poolitzer commentedon Mar 9, 2024
Recently the idea came up in chat to allow custom classes in CH, because the subclass restriction of Update potentially forces one to input a lot of dummy values in the class.
So if we ask them to e.g. subclass ConversationHandlerRequirement which only has user and/or chat id as attribute, we can decrease that dummy data amount
Bibo-Joshi commentedon Mar 9, 2024
@Poolitzer see also the 3rd bullet point in
Bibo-Joshi commentedon May 27, 2024
I've been brooding on an idea for a few weeks now and I don't think that I'll be able to publish a
polished draft. So here's what I've got so far, including several open questions and discussion
points.
Abstract
We introce an absract base class
FSMManagerthat's used asApplication.fsm_manager. This classesjob is it to determine which handlers should be available for a given update by maintaining internal
state information. Functionality to access and set the states is exposed via this class and
corresponding shortcuts in the
CallbackContextclass.Motivation
Introducing stateful logic effectively makes the complete
Applicationstateful. Encapsuling thestate-handling inside a single handler that has no direct interaction with
Applicationleads to anunnatural cut. Granting explicit read and write access to states has been a frequently requested
feature. Making this functionality accessible is not a technical problem if the corresponding
interfaces cover eventual internal concurrency protection. Proper design and documentation can
minimize the risk of users running into concurrency problems by accident.
Implementation draft
Here is some pseudo-code to give you an idea of how this could look like in terms of interfaces.
New FSM specific classes
Adaptions of exitisting interfaces
Adressing known questions
How can existing functionality be covered?
JobQueue.run_onceto schedule calling ofset_next_state. Optionallywe can provide a convenience method for that. Since these jobs will require only simple input,
persistence of timeout jobs won't be a problem.
dedicated states. You can
set_next_stateto them from where ever you like to trigger a "nested"behavior
per_message: No longer necessary as user can implement a customFSMKey&FSMManager. In theexample above, one could simply add
update.effective_message.idas additional level.How does this solve existing problems and feature requests?
for (groups of) chats and users, making it possible to track conversations across chats.
make
FSMManagertrack these. That way,FSMManager.set_statecan automatically call them.FSMManager.set_statereturns before the callbacks have finished)
like
context.bot_data.setdefault(context.fsm_key, {}). Automatic initialization andde-initialization on conversation end could possibly be implemneted by using on-entry states
for
FSMState.IDLEparticular, choosing if only the nested conversation is active or if "parent" states are still
available can be selected as needed
loaded. Best we can do is outsource the responsibility for that to the persistency implementation.
If we send a proper "IDLE" state to the presistency and document that this means that the
conversation for that key is now no longer tracked in-memory, that should be sane.
set_next_statestate-history
FSMManager.do_set_statecan be implemented to keep track of thatto
telegram.UpdateWhat new problems does this approach bring?
conditions if the user isn't careful
FSMManager.get_semaphoreprovides easy way to access semaphore per key to protectcrucial logic
BaseUpdateProcessorcan ensure sequential processing per keywhere necessary via the semaphores
async with context.as_fsm_state(WAITING_STATE):tomake custom concurrency handling more accessible.
Implemented the requirement to return CH.END instead of None to persi…
github-actions commentedon Oct 16, 2025
This issue has been automatically closed due to inactivity. Feel free to comment in order to reopen or ask again in our Telegram support group at https://t.me/pythontelegrambotgroup.
KnorpelSenf commentedon Oct 16, 2025
This reminds me: A few months ago, I completely rewrote both grammY conversations and the documentation for it from scratch. The concepts outlined above stayed fundamentally the same, but the implementation is very straightforward now, the explanation of it is much better, and I was able to add tons of new cool features (like waiting for several types of updates at the same time, and essentially concurrency within the same function etc) that weren't possible before.
If you want to check it out again, now might be a good time. The repo is still at https://github.com/grammyjs/conversations