Manage conversation and user state

Note

This topic is for the latest release of the SDK (v4). You can find content for the older version of the SDK (v3) here.

A key to good bot design is to track the context of a conversation, so that your bot remembers things like the answers to previous questions. Depending on what your bot is used for, you may even need to keep track of state or store information for longer than the lifetime of the conversation. A bot's state is information it remembers in order to respond appropriately to incoming messages. The Bot Builder SDK provides two classes for storing and retrieving state data as an object associated with a user or a conversation.

  • Conversation state help your bot keep track of the current conversation the bot is having with the user. If your bot needs to complete a sequence of steps or switch between conversation topics, you can use conversation properties to manage steps in a sequence or track the current topic.

  • User state can be used for many purposes, such as determining where the user's prior conversation left off or simply greeting a returning user by name. If you store a user's preferences, you can use that information to customize the conversation the next time you chat. For example, you might alert the user to a news article about a topic that interests her, or alert a user when an appointment becomes available.

The ConversationState and UserState are state classes that are specializations of BotState class with policies that control the lifetime and scope of the objects stored in them. Components that need to store state create and register a property with a type, and use property accessor to access the state. The bot state manager can use memory storage, CosmosDB, and Blob Storage.

Note

Use bot state manager to store preferences, user name, or the last thing they ordered, but do not use it to store critical business data. For critical data, create your own storage components or write directly to storage. In-memory data storage is intended for testing only. This storage is volatile and temporary. The data is cleared each time the bot is restarted.

Using conversation state and user state to direct conversation flow

In designing a conversation flow, it is useful to define a state flag to direct the conversation flow. The flag can be a simple boolean type or a type that includes the name of the current topic. The flag can help you track where in a conversation you are. For example, a boolean type flag can tell you whether you are in a conversation or not, while a topic name property can tell you which conversation you are currently in.

Conversation and User state

You can use Echo Bot With Counter sample as the start point for this how-to. First, create TopicState class to manage the current topic of conversation in TopicState.cs as shown below:

public class TopicState
{
   public string Prompt { get; set; } = "askName";
}

Then create UserProfile class in UserProfile.cs to manage user state.

public class UserProfile
{
    public string UserName { get; set; }
    public string TelephoneNumber { get; set; }
}

The TopicState class has a flag to keep track of where we are in the conversation and uses conversation state to store it. The Prompt is initialized as "askName" to initiate the conversation. Once the bot receives response from the user, Prompt will be redefined as "askNumber" to initiate the next conversation. UserProfile class tracks user name and phone number and stores it in user state.

Property accessors

The EchoBotAccessors class in our example is created as a singleton and passed into the class EchoWithCounterBot : IBot constructor through dependency injection. The EchoBotAccessors class constructor initializes a new instance of the EchoBotAccessors class. It contains the ConversationState, UserState, and associated IStatePropertyAccessor. The conversationState object stores the topic state and userState object that stores the user profile information. The ConversationState and UserState objects are created in the Startup.cs file. The conversation and user state objects are where we persist anything at the conversation and user scope.

Updated the constructor to include UserState as shown below:

public EchoBotAccessors(ConversationState conversationState, UserState userState)
{
    ConversationState = conversationState ?? throw new ArgumentNullException(nameof(conversationState));
    UserState = userState ?? throw new ArgumentNullException(nameof(userState));
}

Create unique names for TopicState and UserProfile accessors.

public static string UserProfileName { get; } = $"{nameof(EchoBotAccessors)}.UserProfile";
public static string TopicStateName { get; } = $"{nameof(EchoBotAccessors)}.TopicState";

Next, define two accessors. The first one stores the topic of the conversation, and the second stores user's name and phone number.

public IStatePropertyAccessor<TopicState> TopicState { get; set; }
public IStatePropertyAccessor<UserProfile> UserProfile { get; set; }

Properties to get the ConversationState is already defined, but you'll need to add UserState as shown below:

public ConversationState ConversationState { get; }
public UserState UserState { get; }

After making the changes, save the file. Next, we will update the Startup class to create UserState object to persist anything at the user-scope. The ConversationState is already exists.


services.AddBot<EchoWithCounterBot>(options =>
{
    ...

    IStorage dataStore = new MemoryStorage();
    
    var conversationState = new ConversationState(dataStore);
    options.State.Add(conversationState);
        
    var userState = new UserState(dataStore);  
    options.State.Add(userState);
});

The line options.State.Add(ConversationState); and options.State.Add(userState); add the conversation state and user state, respectively. Next, create and register state accesssors. Acessors created here are passed into the IBot-derived class on every turn. Modift the code to inclue user state as shown below:

services.AddSingleton<EchoBotAccessors>(sp =>
{
   ...
    var userState = options.State.OfType<UserState>().FirstOrDefault();
    if (userState == null)
    {
        throw new InvalidOperationException("UserState must be defined and added before adding user-scoped state accessors.");
    }
   ...
 });

Next, create the two accessors using TopicState and UserProfile and pass it into the class EchoWithCounterBot : IBot class through dependency injection.

services.AddSingleton<EchoBotAccessors>(sp =>
{
   ...
    var accessors = new BotAccessors(conversationState, userState)
    {
        TopicState = conversationState.CreateProperty<TopicState>("TopicState"),
        UserProfile = userState.CreateProperty<UserProfile>("UserProfile"),
     };

     return accessors;
 });

The conversation and user state are linked to a singleton via the services.AddSingleton code block and saved via a state store accessor in the code starting with var accessors = new BotAccessor(conversationState, userState).

Use conversation and user state properties

In the OnTurnAsync handler of the EchoWithCounterBot : IBot class, modify the code to prompt for user name and then phone number. To track where we are in the conversation, we use the Prompt property defined in the TopicState. This property was initialized a "askName". Once we get the user name, we set it to "askNumber" and set the UserName to the name user typed in. After the phone number is received, you send a confirmation message and set the prompt to 'confirmation' because you are at the end of the conversation.

if (turnContext.Activity.Type == ActivityTypes.Message)
{
    // Get the conversation state from the turn context.
    var convo = await _accessors.TopicState.GetAsync(turnContext, () => new TopicState());
    
    // Get the user state from the turn context.
    var user = await _accessors.UserProfile.GetAsync(turnContext, () => new UserProfile());
    
    // Ask user name. The Prompt was initialiazed as "askName" in the TopicState.cs file.
    if (convo.Prompt == "askName")
    {
        await turnContext.SendActivityAsync("What is your name?");
        
        // Set the Prompt to ask the next question for this conversation
        convo.Prompt = "askNumber";
        
        // Set the property using the accessor
        await _accessors.TopicState.SetAsync(turnContext, convo);
        
        //Save the new prompt into the conversation state.
        await _accessors.ConversationState.SaveChangesAsync(turnContext);
    }
    else if (convo.Prompt == "askNumber")
    {
        // Set the UserName that is defined in the UserProfile class
        user.UserName = turnContext.Activity.Text;
        
        // Use the user name to prompt the user for phone number
        await turnContext.SendActivityAsync($"Hello, {user.UserName}. What's your telephone number?");
        
        // Set the Prompt now that we have collected all the data
        convo.Prompt = "confirmation";
                 
        await _accessors.TopicState.SetAsync(turnContext, convo);
        await _accessors.ConversationState.SaveChangesAsync(turnContext);

        await _accessors.UserProfile.SetAsync(turnContext, user);
        await _accessors.UserState.SaveChangesAsync(turnContext);
    }
    else if (convo.Prompt == "confirmation")
    { 
        // Set the TelephoneNumber that is defined in the UserProfile class
        user.TelephoneNumber = turnContext.Activity.Text;

        await turnContext.SendActivityAsync($"Got it, {user.UserName}. I'll call you later.");

        // initialize prompt
        convo.Prompt = ""; // End of conversation
        await _accessors.TopicState.SetAsync(turnContext, convo);
        await _accessors.ConversationState.SaveChangesAsync(turnContext);
    }
}

Start your bot

Run your bot locally.

Start the emulator and connect your bot

Next, start the emulator and then connect to your bot in the emulator:

  1. Click the Open Bot link in the emulator "Welcome" tab.
  2. Select the .bot file located in the directory where you created the Visual Studio solution.

Interact with your bot

Send a message to your bot, and the bot will respond back with a message. Emulator running

If you decide to manage state yourself, see manage conversation flow with your own prompts. An alternative is to use the waterfall dialog. The dialog keeps track of the conversation state for you so you do not need to create flags to track your state. For more information, see manage a simple conversation with dialogs.

Next steps

Now that you know how to use state to help you read and write bot data to storage, lets take a look at how you can read and write directly to storage.