Skip to content

States, Transitions, and Context

Gwendal Daniel edited this page Oct 21, 2020 · 1 revision

Xatkit is relies on State Machines to define the bot execution logic. States and transitions are first class citizen in a Xatkit bot: they define the actions the bot performs, bot reactions to incoming events or user intents are defined using transitions. In the following we detail how to leverage the Xatkit DSL to create states and link them with expressive transitions.

States

A Xatkit state is a named entity that can contain a body section representing the action to perform. The state's body is set with a lambda expression that is executed when entering the state. This lambda expression takes a single StateContext parameter. We will come back to it later in this article, but for the moment we can just ignore it.

The Xatkit DSL provides the state(name) construct to create a state for your bot:

val myState = state("MyState")
    .body(context -> System.out.println("Hello World!");

ℹ The state method (and all the other methods of the Xatkit DSL) are defined in the class com.xatkit.dsl.DSL. You can statically import them using the following import: import static com.xatkit.dsl.DSL.*

But a body is not enough to create a valid state in Xatkit: we also need to define at least one transition to evaluate once the body has been executed.

val myState = state("MyState")
    .body(context -> System.out.println("Hello World!")
    .next()
        .moveTo(anotherState);

State transitions are defined after the next() construct. We detail in the next section the different type of transitions and how they are handled by the Xatkit execution engine.

ℹ The reason we enforce the definition of at least one transition is to make sure there isn't any final state in our state machine. From a user point of view a final state would mean that the bot doesn't react to anything, and doesn't do anything, and we are pretty sure this is something you definitely want to avoid!

Finally, a state can define an optional fallback section that is executed when none of its transitions can be navigated:

val myState = state("MyState")
    .body(context -> System.out.println("Hello World!")
    .next()
        .when(<Transition Condition>).moveTo(anotherState)
    .fallback(context -> System.out.println("Sorry I didn't get it");

The fallback(...) method is similar to body(...): it takes as parameter a lambda expression that describes the action(s) to perform as a fallback. Note that the bot stays in the same state after the execution of a fallback, and waits for new inputs to re-evaluate its transitions.

Transitions

Transitions are used to navigate from one state to another. Xatkit defines two types of transitions:

  • Guarded transitions: transitions that navigates to a state if a given condition is true
  • Automated transitions: transitions that do not contain a condition and directly navigates to a state

Guarded transitions are defined as lambda expressions (with again a StateContext argument):

val myState = state("MyState")
    .body(context -> System.out.println("Hello World!")
    .next()
        .when(context -> 1 == 2).moveTo(anotherState)

Automated transitions are defined using the following syntax:

val myState = state("MyState")
    .body(context -> System.out.println("Hello World!")
    .next()
        .moveTo(anotherState)

There is no limit in the number of transitions a state can contain. Once the state's body has been executed, the execution engine evaluates all the state's transitions following a XOR approach: if one solution is found the navigation is performed and the state machine moves to the next state. If the bot definition is ambiguous and there is more than one navigable transition the execution engine throws an error because it can't decide which one to apply. If no solution is found the execution engine executes the state's fallback (if it exists), and wait for a new input to re-evaluate the transitions.

❗ It's not possible to mix guarded transitions and automated transitions in the same state, as this would result in ambiguous navigations.

Xatkit also the intentIs(IntentDefinition) and eventIs(IntentDefinition) methods in the DSL class to define transition conditions matching a given intent or Event:

val s1 = state("S1")
                .body(context -> System.out.println("Hi there!"))
                .next()
                    .when(intentIs(Greetings)).moveTo(s2)
                    .when(intentIs(HowAreYou)).moveTo(s3);

The example above describes a bot that prints a welcome message, then waits for either a Greetings intent or a HowAreYou intent and move to s2/s3 respectively.

Note that intentIs and eventIs conditions conditions are blocking, meaning that the bot will wait for the next intent/event to evaluate which transition to navigate. This is not the case for context-based conditions like when(context -> 1 == 2) that are evaluated as soon as the state's body has been executed.

❗ While it's technically possible to create intent/event-based transitions without using intentIs/eventIs, we strongly recommend to use these constructs, as they are internally used to properly deploy the bot.

Context

The lambda expressions used to define the state's body, fallback, or transition's condition takes a StateContext argument. This argument is provided by the execution engine, and contains all the following contextual information relative to the bot:

Name Method Description
User Session getSession() The session with the current user. The session is user-managed Map you can use to persist and access information from your bot. This can be parameters that will drive the conversation, result of a previous computation, or a cached value that is expensive to compute
Current State getState() The state the bot is currently executing
Matched Intent getIntent() The RecognizedIntent that triggered the transition to arrive in the current state. This value is null if the transition was an automated transition, or if the transition was triggered by an event
Matched Event getEventInstance() The EventInstance that triggered the transition to arrive in the current state. This value is null if the transition was an automated transition. Note that if getIntent() != null then getEventInstance() == getIntent()
Bot Configuration getConfiguration() A Map containing the bot configuration

In addition, the EventInstance and RecognizedIntents interfaces (accessible with context.getEventInstance() and context.getIntent()) provide the following methods to access event-specific information:

Base Class Method Description
EventInstance getValue(name) Access the value of the name parameter from the event.
EventInstance getPlatformData() A Map containing platform-specific data (e.g. the identifier of a Slack channel)
RecognizedIntent getRecognitionConfidence() A float value between 0 and 1 representing the confidence of the NLP provider
RecognizedIntent getMatchedInput() A string containing the raw user input
RecognizedIntent getNlpData() A Map containing additional NLP data (e.g. sentiment analysis)

ℹ Note that RecognizedIntent extends EventInstance and thus also defines the getValue(name) and getPlatformDate() methods.

The example below shows how to use the context to store a city-name intent parameter in the user session:

val saveParameterState = state("SaveParameter")
    .body(context -> {
        if(context.getIntent() != null) {
            String cityName = (String) context.getIntent().getValue("city-name");
            context.getSession().put("persistent-city-name", cityName);
        }
    })
    .next()
        .moveTo(accessParameterState);

And the following state that retrieves the persisted value:

val accessParameterState = state("AccessParameter")
    .body(context -> {
        String cityName = (String) context.getSession().get("persistent-city-name");
        System.out.println("I remember that you live in " + cityName);
    })
    .next()
        .moveTo(...)

The StateContext argument works pretty much the same way in transitions, excepted that the getIntent()/getEvent() methods return the intent/event that is currently evaluated, and not the one that triggered the previous transition. This allows to check incoming intent/event parameters within transition's conditions, and decide if the bot should move to another state.

Apart from that, the context can be used to create complex transition conditions such as the example below that compares values persisted in the user session:

val s = state("MyState")
    .next()
        .when(context -> context.getSession().get("value1").equals(context.getSession().get("value2"))).moveTo(s2);
Clone this wiki locally