GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Initial FSM PoC by Bibo-Joshi · Pull Request #4669 · python-telegram-bot/python-telegram-bot

Conversation

@Bibo-Joshi

@Bibo-Joshi Bibo-Joshi commented Feb 4, 2025 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

Works on #2770

"It has been a while since we openend #2770" would be an understatement. Yet here we are :D
Finally, I got around to draft up a somewhat proper PoC for my ideas outlined in #2770 (comment). I also attached an example that showcases how the FSM setup can be used. You'll need ≥2 accounts to test it.

To be clear: as of 2025-02-04 this PR does by far not include everything mentioned in #2770 (comment). I hope to work on it in the nearish-future. Continued work on this PR could be greatly supported by intermediate reviews, comments & discussions :)


ToDos / Ideas / Thoughts

(Listing here only things that came to my mind explicitly while working on this PR, not copying everything from #2770)

  • Using context.job_queue.run_once for conversation timeouts is relatively easy, but would have to repeated everywhere which also holds for the cancellation. Maybe we can think of a clever way to abstract that for the user.
  • Having both groups and states makes the "who handles what" logic more involved. Do we do "for state in states: for group in state" or "for group in groups: for state in group"? Also State.ANY and things like StateA | StateB make it necessary that one update can be handled by multiple states. All this at least needs proper documentation about what's going on. Additionally, we should think carefully about what we want here.
  • I'm unsure about if get_active_key_state should be async or not:
    • I feel like it should not make too much async operations - since it's part of the logic in App.process_update, it should be as quick as possible
    • If we want to support lazy-loading, this would probably be the place were it comes into play - depending on how we design the persistence connection
    • If states are set within get_active_key_state (as is done in the fsm example I added), then I'm wondering if one should use a semaphore for that. If one would do that, this could lead to major delays in process_update
  • Instead of BoundedSemaphore(1), we could probably just use Lock as synchronization primitive

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

Palaptin and clot27 reacted with hooray emoji artem-sitd reacted with heart emoji
All reactions
  • 🎉 2 reactions
  • ❤️ 1 reaction
@Bibo-Joshi
Initial FSM PoC
0c06ba0
@Bibo-Joshi Bibo-Joshi added 🛠 refactor change type: refactor 🔌 enhancement pr description: enhancement labels Feb 4, 2025
@Bibo-Joshi
Bibo-Joshi requested review from Poolitzer, clot27 and harshil21 February 4, 2025 20:44
@codecov

This comment was marked as outdated.

Sign in to view
Bibo-Joshi added 8 commits February 5, 2025 10:52
@Bibo-Joshi
Add State.ANY for fallbacks and allow handling multiple states for on…
07225b9
…e update
@Bibo-Joshi
Temporarily Disable Tests on this branch
34832d9
@Bibo-Joshi
Add Some Abstractions for Timeout Jobs
434cbfa
@Bibo-Joshi
Disable tests harder …
817b71d
@Bibo-Joshi
Move internal state storage to FiniteStateMachine and add state history
646ba37
@Bibo-Joshi
Try setting up infrastructure for optimistic locking. Example will fo…
4441543
…llow
@Bibo-Joshi
Add an example on concurrency in FSM
31af1a9
@Bibo-Joshi
Use Lock instead of semaphore
b1fff6d
Poolitzer
Poolitzer reviewed Feb 8, 2025
View reviewed changes

@Poolitzer Poolitzer left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just some first impressions, once I gain a further understanding I can give more

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread examples/fsmbot.py
self.admin_id = admin_id
super().__init__()

def _get_admin_state(self) -> tuple[State, int]:

@Poolitzer Poolitzer Feb 8, 2025

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure I like this being private/protected/however the underscore is called

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@Bibo-Joshi Bibo-Joshi Feb 8, 2025

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far I see no reason why someone should be allowed to all USM(…).get_admin_state … then again this is only an example, so I don't care :D

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread examples/fsmbot.py
super().__init__()

def _get_admin_state(self) -> tuple[State, int]:
return self._states[self.admin_id]

@Poolitzer Poolitzer Feb 8, 2025

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should not be the private function right

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

Bibo-Joshi reacted with thumbs up emoji
All reactions
  • 👍 1 reaction
Comment thread examples/fsmbot.py
def _get_admin_state(self) -> tuple[State, int]:
return self._states[self.admin_id]

def get_state_info(self, update: object) -> StateInfo[Optional[int]]:

@Poolitzer Poolitzer Feb 8, 2025

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type hint here is linked to the FSM class type hint above right? I can't have one be a string one be an integer.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@Bibo-Joshi Bibo-Joshi Feb 8, 2025

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, the type variable specifying the type of keys you use.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread telegram/ext/_application.py
Comment on lines +1518 to +1519
if not state_handlers[group]:
del state_handlers[group]

@Poolitzer Poolitzer Feb 8, 2025

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this remove the group if its empty? Can we comment it

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@Bibo-Joshi Bibo-Joshi Feb 8, 2025

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, that's basically the same as state_handlers.pop(group, None), I think. It will call state_handlers.__del__(group). IMHO this is basic python syntax 😬

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread telegram/ext/_fsm/machine.py

from telegram.ext import JobQueue

_KT = TypeVar("_KT", bound=Hashable)

@Poolitzer Poolitzer Feb 8, 2025

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Im back with my thinking about private stuff. Why do we private it here, not in other files; should we. Hm.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@Bibo-Joshi Bibo-Joshi Feb 8, 2025

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel you :D if machine was a public module like telegram.constants, it would have to be protected IMO. Since it's not and we expose the relevant elements only via telegram.ext, it's not so important. Making it clearer what the content exposed by this module is still doesn't hurt.
If you want to go deeper into the rabbit hole: https://discuss.python.org/t/add-the-export-keyword-to-python/28444/6

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread examples/fsmbot.py
Comment on lines +165 to +166
application.fsm = UserSupportMachine(admin_id=123456)
application.fsm.set_job_queue(application.job_queue)

@Poolitzer Poolitzer Feb 8, 2025

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these should be part of the builder and the second one set by default imo

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@Bibo-Joshi Bibo-Joshi Feb 8, 2025

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, absolutely. #PoC 👼

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread telegram/ext/_fsm/machine.py
return f"FSM_Job_{'_'.join(str(hash(k)) for k in keys)}"

def set_job_queue(self, job_queue: "JobQueue") -> None:
self.__job_queue = weakref.ref(job_queue)

@Poolitzer Poolitzer Feb 8, 2025

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why weakref wont it/the user experience break if the supplied job queue gets destroyed

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@Bibo-Joshi Bibo-Joshi Feb 8, 2025

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is job_queue.application. that's also weakref b/c we have a cyclic reference: application is application.job_queue.application. Now we also have application is application.fsm.job_queue.application. I'm not 100% sure if this is strictly necessary, but it seemed safer to me 🤔 I can at very least add a comment.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread telegram/ext/_fsm/machine.py
Comment on lines +166 to +167
if raise_exception:
raise RuntimeError("JobQueue was garbage collected")

@Poolitzer Poolitzer Feb 8, 2025

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ha here see. why.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread telegram/ext/_fsm/states.py
def __init__(self, uid: Optional[str] = None):
effective_uid = uid or uuid4().hex
if effective_uid in self.__knows_uids:
raise ValueError(f"Duplicate UID: {effective_uid} already registered")

@Poolitzer Poolitzer Feb 8, 2025

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the error saying UID correct? I would understand the optional type hint to be anything hashable

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@Bibo-Joshi Bibo-Joshi Feb 8, 2025

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I don't get you. What do you think is wrong? the phrasing of the error message?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread telegram/ext/_fsm/states.py


class State(abc.ABC):
__knows_uids: ClassVar[set[str]] = set()

@Poolitzer Poolitzer Feb 8, 2025

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait I must really misunderstand smth, how does this work across states

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@Bibo-Joshi Bibo-Joshi Feb 8, 2025

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uh … that's how class variables work? :D In the init self.__knows_uids is basically the same as State.__knows_uids …

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@Poolitzer

Poolitzer commented Feb 8, 2025

Copy link
Copy Markdown
Member

A thought I had: Since this is such a huge change for something so integral to our project, should we provide a beta version/implementation after we are satisfied with the status and announce it to get people to try it out and provide feedback before we make a first release?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@Bibo-Joshi

Bibo-Joshi commented Feb 8, 2025

Copy link
Copy Markdown
Member Author

A thought I had: Since this is such a huge change for something so integral to our project, should we provide a beta version/implementation after we are satisfied with the status and announce it to get people to try it out and provide feedback before we make a first release?

That is indeed a very good idea 👍
My thoughts on this are roughly like this:

  • the overall change should (hopefully) be such that nothing changes if you just don't use the new states.
  • that way we can introduce the refactoring & deprecate the existing CH without braking changes.
  • We can mark the status as "public beta feature" and at some point we can say "well, starting with version 25.0, states are now marked as stable".

that way we wouldn't have to do any additional actual beta releases.
If we prefer to have a proper beta release, I'm open to that. We could e.g. do states update on a 23.0bx line that gets only updates for states while other stuff (especially API updates only go into 22.x) . We shouldn't do that for too long IMO, we saw how much effort that takes with v20.
Encouraging people to try out a branch is ofc an easy option.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@Poolitzer

Poolitzer commented Feb 8, 2025

Copy link
Copy Markdown
Member

I dont get the first point, what do you mean with not using new states. In conjunction with ConvHandler? I thought this is supposed to fully replace it. In that case I would like a big announcement at least (maybe also in readme or so).

We might not really need an extra release now that I think of it, just make a big fuss about it before calling it stable and doing the deprecation error warning.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@Bibo-Joshi

Bibo-Joshi commented Feb 8, 2025

Copy link
Copy Markdown
Member Author

I dont get the first point, what do you mean with not using new states. In conjunction with ConvHandler? I thought this is supposed to fully replace it. In that case I would like a big announcement at least (maybe also in readme or so).

Yes, this is supposed to replace CH. But, since this is acting on a different level than handlers, the conversationhandlerbot.py example will still run as before. We should throw hella-big warnings if states & CH are used in conjunction, true.
All other features that are independent of CH will also all run the same. all keys will always be in state "IDLE".

We might not really need an extra release now that I think of it, just make a big fuss about it before calling it stable and doing the deprecation error warning.

👍

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@Poolitzer

Poolitzer commented Feb 8, 2025

Copy link
Copy Markdown
Member

Ah yes you mean in the transition period alright. Okay so lets do a release when we are happy and announce that its there and see if we get feedback, and then do a deprecation later. Perfect.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@OnceUponATimeInAmerica

OnceUponATimeInAmerica commented Jul 11, 2025 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

I'm not sure if this is the right place to mention this but:

Is there any plans or ongoing work for a declarative but an extensible (e.g. with hooks) FSM syntax? like State1 ---(on some input or trigger) ---> State2 (produce or calculate some output or perform a set of actions) ?

I have a fairly complex stateful bot in PTB (and ConversationHandler) and have written a very rudimentary function that parses a generically defined (but limited) transition table (declarative) and performs the transitions given the inputs and also produces some (a set of) outputs or actions.

I did it for a subset of states (relatively simple logic) in my application, because in the current PTB framework adding states and extending the FSM logic is somewhat a pain. Code re-usability was another consideration (e.g. several states might want to transition to a very specific state and so using a common transition function or hook for setting up the UI and then entering that state is useful for code reuse).

A declarative approach to complex FSM logic specification would both accelerate and simplify development, IMHO.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

fred231084g
fred231084g approved these changes Dec 22, 2025
View reviewed changes

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@Poolitzer Poolitzer Poolitzer left review comments
@harshil21 harshil21 Awaiting requested review from harshil21
@clot27 clot27 Awaiting requested review from clot27
+1 more reviewer
@fred231084g fred231084g fred231084g approved these changes
Reviewers whose approvals may not affect merge requirements

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

🔌 enhancement pr description: enhancement 🛠 refactor change type: refactor

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

4 participants

@Bibo-Joshi @Poolitzer @OnceUponATimeInAmerica @fred231084g
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.