How to approach bot state management?

Hello :slight_smile:

I’m building a chat bot. Basically, I have a phoenix api server that exposes a webhook endpoint that handles updates/requests on a chat app; e.g. WhatsApp. The Bot needs to parse the request payload, perform DB operations, and optionally do some other side effects.

The code handling the request has grown more complex as more conversational states are handled. A conversational state often spans multiple requests; e.g. the first request is where a user says “Hi”, the Bot then responds and waits for a following request from the user. The seconds request is usually based on the first.

I’m using hierarchical finite state machines HFSM to handle the state transitions. However, most libraries that support state machines are purists. They handle the transition from one state to the other without worrying to much about side-effects. I’m using machinist. I’m also aware that other alternative libraries support hooks that allow for side-effects. However, I’m now questioning the use of HFSM. A better model, in my mind, would be something similar to the Elm architecture. The reason for this is that I want to be able to specify actions in the side-effect handling code: Action A causes a state update and kicks off a some side-effect. The side-effect handling code could then optionally fire action B. How would you do this in an Elixir/Phoenix app?

Or perhaps you’d approach the entire problem differently?

Thanks

One thing that jumps immediately to mind based on the idea of “returning actions” is the gen_statem module that’s already included in the BEAM.

An event handler in gen_statem can return results like next_event, which will trigger other event handlers before checking the mailbox again. This is useful for situations where many handlers need the same effects - I’ve used it to persist the FSM state to the database, for instance. It would look something like this:

def some_state({:call, from}, {:some_event, payload}, data) do
  result = do_something_with(payload, data)

  {:next_state, :some_other_state, data, [{:reply, from, result}, {:next_event, :internal, :persist_data}]}
end

def some_state(:internal, :persist_data, data) do
  persist_data(data)

  :keep_state_and_data
end
1 Like

Thanks @al2o3cr and apologies for the late reply.

I have now tried gen_statem and it works beautifully. I was also pleasantly surprised by the quality of the Erlang documentation. This was my first venture into trying to understand Erlang documentations.