Practical Dialog State in large Bot Framework v4 projects

State seems to be large topic in v4. I’ve written posts about it before, including; Waterfall Step Patterns and Changes in State Handling, but after more exposure to v4 I coming around to the decision that for more complex projects I’m going to recommend reverting back to how v3 manages state…mostly.

The v4 way, or not?

Using the Accessor Pattern for state is great, it provides a sense of concrete design. You create a dialog, you create one or more state accessors that will be passed to it. The use of the state is very explicit. This is all good. So what’s the problem? The problem is that for complex bot projects your state is going to be filled with little islands of seemingly unrelated data. Consider a simple scenario of a New Contact dialog. You invoke the dialog, which in-turn (pun not intended) invokes a New Address dialog which can also be used from other dialogs. You would write something like;

IStatePropertyAccessor contactStatePropertyAccessor;
IStatePropertyAccessor addressStatePropertyAccessor;

But in a complex Bot Project you may have tens or hundreds of dialogs;

IStatePropertyAccessor s1StatePropertyAccessor;
IStatePropertyAccessor s2StatePropertyAccessor;
...
IStatePropertyAccessor sNStatePropertyAccessor;

That’s not cool.

What about a Generic State Accessor?

So given we want to mitigate all these state islands, could we have a generic accessor? Short answer is, “yes”, actual answer is, “too dirty”.

IStatePropertyAccessor activeDialogStatePropertyAccessor;

Every dialog gets passed the same single accessor and they store their data in it. Ah but what our chain of dialogs, when we invoke our New Address won’t that overwrite the current New Contact state? Yes. So we’ll have to look at creating some form of dictionary/hash to avoid…hang on, this seems a lot like the Active Dialog state from v3. Is that still available?

Active Dialog State

Active Dialog State is the framework supplied state that follows the dialog stack. Or to put it another way, we don’t have to worry about the dialog stack. So how do we gain access to this in v4? Overrides;

public override Task BeginDialogAsync(DialogContext outerDc, object options = null, CancellationToken cancellationToken = default(CancellationToken))
{
    this.dialogContext = outerDc;
    this.SaveState();
    return base.BeginDialogAsync(outerDc, options, cancellationToken);
}

public override Task ContinueDialogAsync(DialogContext outerDc, CancellationToken cancellationToken = default(CancellationToken))
{
    this.dialogContext = outerDc;
    this.InitializeFromState();
    return base.ContinueDialogAsync(outerDc, cancellationToken);
}

In this example I’ve duplicated the handling of the state per class, but making this generic with clones or actions would be the way forward;

private void SaveState()
{
    // EDIT: DO NOT use 'this', its like creating a mini sub stack
    // and confuses the dialog rehydration. Use smaller struct/class
    this.dialogContext.ActiveDialog.State["ObjectState"] = this;
}

private void InitializeFromState()
{
    if (this.dialogContext.ActiveDialog.State.TryGetValue("ObjectState", out object rawObject))
    {
        var dialog = contactDialogObject as NewContactDialog;
        if (dialog != null)
        {
            if (dialog != null)
            {
                this.FirstName = dialog.FirstName;
                this.Telephone = dialog.Telephone;
            }
        }
    }
}

So now when the dialog starts it fetches its own state from the Active Dialog (itself), and updates its properties. So we are now at roughly the same point we would be in v3, i.e. we’ve been re-hydrated. We now need to update the state when a values changes;

private async Task PromptForTelephoneStepAsync(
                                        WaterfallStepContext stepContext,
                                        CancellationToken cancellationToken)
{
    // we've passed the validation so save the result
    this.Name = stepContext.Result.ToString();
    this.SaveState();
    ...

That’s it. We now have a fully working state engine for dialogs that doesn’t leave us with lots of data islands. Note, the above can be tidied up through the use of inheritance, generics, etc. Also note that you would need to implement other overrides to support restarting the flow, etc.

I should caution that I don’t know about the limits of state, perhaps I will discover a size restriction, we’ll see.

Advertisements

Changes to handling state from V3 to V4 of the Bot Framework

One of the crucial areas for a Bot development is how to handle state, v4 has a new approach to it

V3 State Handling

Probably the easiest way to access state data is from the context object, or more specifically the IBotData aspect of IDialogContext. This provides access to the three main state bags; Conversation, Private Conversation and User.

context.PrivateConversationData.SetValue("SomeBooleanState", true);

If you do not have access to the context then you can load the state directly from the store. The implementation of the store is left open, this example is using a CosmoDB store;

 var stateStore = new CosmosDbBotDataStore(uri, key, storeTypes: types);

 builder.Register(c => store)
        .Keyed(AzureModule.Key_DataStore)
        .AsSelf()
        .SingleInstance();

IBotDataStore botStateDataStore;
...
var address = new Address(activity.From.Id, activity.ChannelId, activity.Recipient.Id, activity.Conversation.Id, activity.ServiceUrl);
var botState = await botStateDataStore.LoadAsync(address, BotStoreType.BotPrivateConversationData, CancellationToken.None);

It’s an okay solution, but v4 decided to go in a slightly different direction.

V4 State Handling

v4 encourages us to be more upfront about what our state looks like rather than simply hiding it in property bags. Start off by creating a class to hold the state properties you want to access. This requires using IStatePropertyAccessor

public IStatePropertyAccessor FavouriteColourChoice { get; set; }

At practically the earliest point of the life-cycle, configure the state in the startup;

public void ConfigureServices(IServiceCollection services)
{
   services.AddBot(options =>
   {
...

// Provide a 'persistent' store implementation
IStorage dataStore = new MemoryStorage();

// Register which of the 3 store types we want to use
var privateState = new PrivateConversationState(dataStore);
options.State.Add(privateState);

var conversationState = new ConversationState(dataStore);
options.State.Add(conversationState);

var userState = new UserState(dataStore);
options.State.Add(userState);
...

Now for the real v4 change. On each execution of the bot, or ‘Turn’, we register the specialized state that we are going to expose;

// Create and register state accesssors.
// Acessors created here are passed into the IBot-derived class on every turn.
services.AddSingleton(sp =>
{
// get the options we've just defined from the configured services
var options = 
    sp.GetRequiredService<IOptions<BotFrameworkOptions>>().Value;
...

// get the state types we registered
var privateConversationState =
    options.State.OfType(PrivateConversationState).First();

var conversationState = options.State.OfType(ConversationState).First();

var userState = options.State.OfType(UserState).First();

// now expose our specialized state via an 'StateAccessor'
var accessors = new MySpecialisedStateAccessors(
                privateConversationState, 
                conversationState, 
                userState)
{
FavouriteColourChoice = userState.CreateProperty("BestColour"),
MyDialogState = conversationState.CreateProperty("DialogState"),
...
};

The state accessor is then made available to each Turn via the constructor;

public MainBotFeature(MyUserStateAccessors statePropertyAccessor)
{
     _userStateAccessors = statePropertyAccessor;
}
...
public async Task OnTurnAsync(ITurnContext turnContext, 
                  CancellationToken cancellationToken = 
                    default(CancellationToken))
{
...
var favouriteColour = await _userStateAccessors.FavouriteColourChoice.GetAsync(
                      turnContext, 
                      () => FavouriteColour.None);
...
// Set the property using the accessor.
await _userStateAccessors.CounterState.SetAsync(
                          turnContext, 
                          favouriteColourChangedState);

// Save the new turn count into the user state.
await _userStateAccessors.UserState.SaveChangesAsync(turnContext);
...

Migrating data

If you are already running a v3 Bot then migrating state data will depend on how you implemented your persistent state store, which is a good thing. It’s still basically a property bag under the covers so with a little bit of testing you should be able to migrate without too many issues – should 😉