Managing state with Microsoft Bot Framework

In this article we are going to add state to our bot that we created earlier. We want that our bot asks for our name, remembers it and then greets us using the name. Without state management the bot would forget everything right after one conversation.

The concept

Here is a quick summary about what we will do. Don't get discouraged if it doesn't make any sense yet:

  1. We create a storage object in index.ts. A storage object represents the physical data storage layer. This can be any database. In this example we will use an In-Memory database so that everything we store is only stored until we restart our bot.
  2. Then we define two state management objects in index.ts, also called buckets: one object to store data for the user and one object to store data for the conversation.
  3. After that we define state property accessors on these buckets in bot.ts. We do this, so that we can simply call get and set to store and retrieve data.
  4. With all that in place we are able to handle state in the onTurn method located in bot.ts.

We will see how these three things (storage object, buckets and state property accessors) are connected. But first, let's make ourselves aware of the...

Challenges when managing state

What is it that makes state management a complex thing to deal with?

  1. There are many specific databases out there. We don't want to deal with the specifics for each of them
  2. We would like to cache our data somehow - for fast access
  3. We would like to reduce the amount of handling data asynchronously

That is why we need a solution to deal with all these challenges in an easy way. The simplest thing to do would be to just get and set our state values somewhere.

State Properties

That is where State Properties come into play. They are key-value pairs that your bot can read from (get) and write to (set) - without worrying about the specific underlying implementation. They even take care of caching and async data handling. The important question is "Where shall we call get or set?".

Buckets (State management objects)

Answer: On state management objects, also called "buckets". There are three: User state, Conversation state and Private conversation state.

User state

User state is available in any turn that the bot is conversing with that user on that channel, regardless of the conversation. Good for tracking information about the user: Non-critical user information, such as name and preferences, an alarm setting, or an alert preference. It is also good to store information about the last conversation a user had with the bot. For instance, a product-support bot might track which products the user has asked about.

Here is a basic implementation on how to create a State Property Accessor called 'userProfile'. This example also shows how to get, set and save values.

import { TurnContext, BotStatePropertyAccessor, UserState } from 'botbuilder';

class MyBot {
  private userProfile: BotStatePropertyAccessor;
  private userState: UserState;

  constructor(userState) {
    this.userProfile = userState.createProperty('userProfile');
    this.userState = userState;
  }
  
  public onTurn = async (turnContext: TurnContext) => {
    const userProfile = await this.userProfile.get(turnContext, {});
    await this.userProfile.set(turnContext, userProfile);
    // 'set' does not physically store the value, but saveChanges does.
    // We save it in-memory (more about it later)
    await this.userState.saveChanges(turnContext);
  }
}

Conversation state

Conversation state is available in any turn in a specific conversation, regardless of user (i.e. group conversations). Conversation state is good for tracking the context of the conversation, such as: Whether the bot asked the user a question, and which question that was or what the current topic of conversation is, or what the last one was. For example: The bot could aggregate and display student responses for a given question, aggregate each student's performance and then privately relay that back to them at the end of the session.

Private conversation state

Private conversation state is scoped to both the specific conversation and to that specific user. Good for channels that support group conversations, but where you want to track both user and conversation specific information.

Scope & Keys

A Scope defines the visibility of data to your bot. Both user and conversation state are scoped by channel. That means that the bot will treat the same person as a new separate user (with new user state) for every channel that person enters.

When setting the value of your state property, the key is defined for you internally with information contained on the turn context to ensure that each user or conversation gets placed in the correct bucket and property. The unique keys used for each of these predefined buckets are specific to the user and conversation, or both.

Type of stateUnique ID creation
User state{Activity.ChannelId}/users/{Activity.From.Id}#YourPropertyName
Conversation state{Activity.ChannelId}/conversations/{Activity.Conversation.Id}#YourPropertyName
private conversation state{Activity.ChannelId}/conversations/{Activity.Conversation.Id}/users/{Activity.From.Id}#YourPropertyName

Example

Step 1: Defining state store

We start by defining the state store: This is either Memory storage (for testing purpose, because everything gets deleted upon bot restart), Azure Blob Storage, Azure Cosmos DB storage (NoSQL) or any other physical storage service. We use MemoryStorage in our example:

// define data store in index.ts
import { MemoryStorage } from 'botbuilder';
const memoryStorage = new MemoryStorage();

Step 2: Creating buckets

// define buckets (state management objects) in index.ts
const conversationState = new ConversationState(memoryStorage);
const userState = new UserState(memoryStorage);

const myBot = new MyBot(conversationState, userState);

Step 3: Creating Property Accessors

// bot.ts
import { ActivityTypes, TurnContext } from 'botbuilder';

// The accessor names for the conversation data and user profile state property accessors.
const CONVERSATION_DATA_PROPERTY = 'conversationData';
const USER_PROFILE_PROPERTY = 'userProfile';

export class MyBot {
    private conversationData: any;
    private userProfile: any;
    private conversationState: any;
    private userState: any;

    constructor(conversationState, userState) {
        // Create the state property accessors for the conversation data and user profile.
        this.conversationData = conversationState.createProperty(CONVERSATION_DATA_PROPERTY);
        this.userProfile = userState.createProperty(USER_PROFILE_PROPERTY);

        // The state management objects for the conversation and user state.
        this.conversationState = conversationState;
        this.userState = userState;
    }

    // ...
    }
}

Step 4: Using state in onTurn

/**
 * Use onTurn to handle an incoming activity, received from a user, process it, and reply as needed
 *
 * @param {TurnContext} turnContext on turn context object.
 */
public onTurn = async (turnContext: TurnContext) => {
    if (turnContext.activity.type === ActivityTypes.Message) {
        // Get the state properties from the turn context.
        const userProfile = await this.userProfile.get(turnContext, {});
        const conversationData = await this.conversationData.get(
            turnContext, { promptedForUserName: false });

        if (!userProfile.name) {
            // First time around this is undefined, so we will prompt user for name.
            if (conversationData.promptedForUserName) {
                // Set the name to what the user provided.
                userProfile.name = turnContext.activity.text;

                // Acknowledge that we got their name.
                await turnContext.sendActivity(`Thanks ${userProfile.name}.`);

                // Reset the flag to allow the bot to go though the cycle again.
                conversationData.promptedForUserName = false;
            } else {
                // Prompt the user for their name.
                await turnContext.sendActivity('What is your name?');

                // Set the flag to true, so we don't prompt in the next turn.
                conversationData.promptedForUserName = true;
            }
            // Save user state and save changes.
            await this.userProfile.set(turnContext, userProfile);
            await this.userState.saveChanges(turnContext);
        } else {
            // Add message details to the conversation data.
            conversationData.timestamp = turnContext.activity.timestamp.toLocaleString();
            conversationData.channelId = turnContext.activity.channelId;

            // Display state data.
            await turnContext.sendActivity(`${userProfile.name} sent: ${turnContext.activity.text}`);
            await turnContext.sendActivity(`Message received at: ${conversationData.timestamp}`);
            await turnContext.sendActivity(`Message received from: ${conversationData.channelId}`);
        }
        // Update conversation state and save changes.
        await this.conversationData.set(turnContext, conversationData);
        await this.conversationState.saveChanges(turnContext);
    }
}
[forminator_quiz id="2486"]

About Author

Mathias Bothe To my job profile

I am Mathias, born 40 years ago in Heidelberg, Germany. Today I am living in Munich and Stockholm. I am a passionate IT freelancer with more than 16 years experience in programming, especially in developing web based applications for companies that range from small startups to the big players out there. I am founder of bosy.com, creator of the security service platform BosyProtect© and initiator of several other software projects.

No comments yet.

Leave a comment