-
Notifications
You must be signed in to change notification settings - Fork 470
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Flow #288
Comments
Please make it happen 🙏 |
this would be helpful |
You can use redis for that. For that functionality, you definitely need persistent storage. What will you do on: reboot, scaling? |
Nice issue. It is looks like scene in telegraf (node.js lib for telegram bots) - telegraf/telegraf#705. This would be helpful |
Any progress on this? |
This would be really cool. I really like the implementation of conversations in python-telegram-bot using finite-state machines: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/conversationbot.py |
@wildsurfer Hang in there. @and3rson Yeah you could call these things FSMs, but there isn't much sense in doing so when you're not exactly reasoning about these machines. For all intents and purposes, the state mechanics of a chatbot must be overall correct (e.g. not being able to produce invalid states) as well as fluid (being able to deduce state transitions from natural input) and what you have to see is that this particular set of constraints is much more nuanced than whatever fits the bill of an FSM in your book, probably. I say this because most of the time, out–of–the–box stock framework implementation for this sort of thing is completely unsound shit. This is part of the reason, too, why I insisted that flow–like functionality must be implemented in the executable. I've changed my mind though, but I don't intend to spew suboptimal solutions any time soon. Hang in there. (Flow will make v3.) |
@tucnak Did the concept of flows end up making it into V3? |
@nii236 No, not yet unfortunately it didn't. I'm overloaded with a bunch of stuff like The Stone Cross Foundation of Ukraine and the other FOSS projects on my neck. However, I've said this numerous times, I would happily lead whomever young engineering wanting to prove themselves with something like this. My approach to Flow had evolved significantly since the time I wrote this issue. My bad the evolution wasn't reflected here. Basically, instead of straight-up building the logic around something like looplab/fsm I suggest we do a more generic interface, because depending on the style the bot is implemented in, and the extent of i.e. possible back-and-forth logic to it, you might want something different to a plain generic FSM implementation, which has to be on the table one way or another. What if I want to collect metrics? And this is something truly interesting: imagine being able to record spans for your interactions like you do with prometheus. This would be a game-changer, but this would require if not some form of middleware, then a special implementation of the state machine. This is the API that I currently have in mind: // package tele
// var StateFunc func(*tele.State) error
// package yourbot
inb4 := func (prompt string) tele.StateFunc {
return func(state *tele.State) error {
return state.Bot.Send(prompt)
}
})
bot.Handle("/hello", tele.Flow(). // sets up Interaction interface-chain
Text(inb4("Enter username:"), getUsername).
Text(inb4("Enter password:"), getPassword).
Photo(inb4("Your avatar photo:"), getAvatar).
Then(createUser)) The idea is that there's a Another open question remains whether if the state itself should be some fixed struct or an interface. I worry making it a full-blown interface would make it a rather big one, and extending it would be problematic. Otherwise, how do you have the end-users implementing serialisation and tracing, which in case of tracing would for the most part be prometheus, but this is not a given— I've seen people implementing tracing manually in-memory with Bloom filters and the like. Hence if serialisation and the other specialities are not part of the state struct/interface, they must be "registered" with the bot instance somehow. But at this point it might as well be of benefit to make it interface, so the end-users would embed and override the default state interface with their custom implementations like they do in labstack/echo.*Context |
I am really looking forward to this FSM support. I have a bot that needs this as soon as we can get it but I'm sure we will get there. The implementation outlined above is just perfect. |
I'd like to try to do it, but I have my own idea of how it should be done. So, let's start from the beginning:
In detail, if we want to utilize the flow, the first step is to create an instance of FlowBus.
After that, we need to register all global handlers that we use in our flow process. This step might seem like overhead, but it's necessary to ensure that the flow can appropriately handle various actions. However, consider a situation where a user needs a handler for any text input. How can they solve this problem if the flow process also requires that handler? It can be challenging to manage. To address this issue, I've introduced the FlowBus entity. Typically, user code might look like this: bot.Handle(telebot.OnText, flowBus.Handle)
bot.Handle(telebot.OnMedia, flowBus.Handle) But in cases where a user needs a custom handler like ours, they can do the following: bot.Handle(telebot.OnText, flowBus.ProcessUserToFlowOrCustom(func (c telebot.Context) error {
// Called only if the user hasn't begun the flow.
return nil
})) Let's mention the Flow. I believe we need to have the following capabilities:
type NonEmptyValidator struct{}
func (NonEmptyValidator) Validate(state State, c *telebot.Context) error { return nil }
type IsEmailValidator struct{}
func (IsEmailValidator) Validate(state State, c *telebot.Context) error { return nil }
func Example() {
flowBus := Handler{}
bot.Handle(telebot.OnText, flowBus.Handle)
var loggingMessage string
loggingDecorator := func(state *State) {
if state.Machine.Step() == 0 {
loggingMessage += fmt.Sprintf("[FLOW] [USER: (%d)", state.context.Message().Sender.ID)
return
}
loggingMessage += fmt.Sprintf("[STEP (%d)] [DATA: (%s)]", state.Machine.Step(), state.context.Message().Text)
}
sendUserMessage := func(message string) func(*State) {
return func(state *State) {
state.context.Reply(message)
}
}
// Our greeting serves as the initial step
startStep := func(state *State) {
state.context.Reply("Hello there!")
}
var startDto struct {
email string
password string
}
flowBus.Flow("/start").
Step(BeginStep().Start(startStep).OnSuccess(loggingDecorator)).
Step(BeginStep().
Start(sendUserMessage("Enter email:")).
Validate(NonEmptyValidator{}, IsEmailValidator{}).
Assign(func(state *State) {
startDto.email = state.context.Message().Text
}).
OnSuccess(loggingDecorator),
).Step(BeginStep().
Start(sendUserMessage("Enter password:")).
// TextAssigner - is already an implemented function that sets a text from a user to the variable
Assign(TextAssigner(&startDto.password)),
).Step(BeginStep().
Start(func(state *State) {
state.Machine.Back()
state.Machine.ToStep(2)
// Global fail
state.Machine.Fail()
}),
).Success(createUser).Fail(failHandler)
} But please, don't worry about a lot of code. I'm confident that it will typically look something like this: func Example() {
flowBus := Handler{}
bot.Handle(telebot.OnText, flowBus.Handle)
sendUserMessage := func(message string) func(*State) {
return func(state *State) {
state.context.Reply(message)
}
}
var email, password string
flowBus.Flow("/start").
Step(BeginStep().Start(sendUserMessage("Enter email:")).Assign(TextAssigner(&email))).
Step(BeginStep().Start(sendUserMessage("Enter password:")).Assign(TextAssigner(&password))).
Success(createUser)
} |
The API above isn't the final idea. I'm going to think about it, and I'm pretty sure I can make it simpler and shorter. |
I will make a pull request tomorrow. The code may not be production-ready, of course, and without tests, but it should be sufficient for manual testing. Unfortunately, I haven't worked with a Telegram bot api for the last two years, so I may not be able to cover all cases. I'm asking for help from anyone who wants to contribute. Just pull my branch, try to describe your own flows, and provide feedback. |
#657 Let's discuss that |
@demget Is this solution planned for version 4.X? |
hi please implement this part, it so important for many developers |
Introduction
There are multiple missing pieces from the current Telebot implementation.
One of these pieces is a feature, known as the context. I don't believe that it should be included in the framework itself, as the state machine and the logic associated with it—is largely dependant on the implemention, therefore it must be implemented in the user code.
That said, we repeatedly end up implementing the context one way, or another. From the perspective of the code, it's trivial to do so. What is much more complicated is implementing complex multi-step interactions. This requires a large number of states, which adds to unnecessary verbosity and complicates the logic even further. To make things easier, I propose to introduce the concept of the flow.
What is the flow?
The flow is simply a high-level algorithm, a series of actions and branches, which naturally occur in the logic of any sophisticated bot.
Please, consider the following algorithm:
/start
A greeting message is shown, accompanied with a list of commands and an inline keyboard with key actions.In order to implement the aforementioned algorithm, currently you would have to create a state machine of your own, and laboriously spell out each and every state, alongside with the numerous transition rules. Principally speaking, this is trivial, but as the interactions require multiple kinds of media and have many intermittent requirements, the implementation would have to be spread out across different handlers. The code will quickly grow uncontrollably.
Proposal
The approach spelled out below is only but a first impression, much of it is open to discussion. I should fix a few principles to consider, when discussing it: (a) the state machine must not be naively exposed from the point of the flow, (b) the interaction must be functionally described by its steps, not the other way around, (c) interactions are always happening one-on-one between the bot and the user, (d) the flow is controlled via errors, handled by the interactions from the inner to the outer. Keep this in mind.
I will now walk though the key building blocks.
State
State of the interaction is implementation-dependant.
Path
Path is an interaction queue.
This struct maintains the snapshots of all states at any given interaction, so the rollback can be done from any point. The state is each and different for any given
Interaction
This is the logical building block, a state machine interface.
Interaction implements the state of its own, while the path maintains the global state. Before the first message is pushed,
Enter
is called to validate the enter condition; if nil is returned, the interaction is added to the tail of the path along with the path state at the time of entrance.When
Back
is called, it can either return nil, in which case the tail is simply rolled back to the last occurance of the interaction in the path, or aRollback
, which may specify the exact position and/or override the the state of the interaction at that point.Interactions must be implemented to be valid Telebot handlers.
Flow
Flow is an interaction that outlines the high-level series of actions (interactions) in the builder fashion.
(I'm not sure whether if it should be a struct or interface, but it probably should be an interface.)
Flow can be used to set up a series of data fetches and validations for all the supported types of updates. Essentially, it's the high-level representation of the algorithm, where everything is supposed to come together using custom fetches,
Or
andThen
to compose,Check
to validate in-between steps.Flows implement regular interactions and can be created with
Begin(step string)
.Rollback
Rollback is an error that can override the current position and the state of the path.
Conclusion
This is it, for now. Hopefully, the API mentioned above is sufficient to build most, if not any of the bot interactions. Take a look at the actual code that is roughly implementing the algorithm outlined in the introduction.
Please feel free to ask questions, as well as point out features and imperfections.
Cheers,
Ian
The text was updated successfully, but these errors were encountered: