Terminate all dialogs and exit conversation in MS Bot Framework when the user types "exit", "quit" etc

user6269864 picture user6269864 · Aug 24, 2016 · Viewed 9.5k times · Source

I can't figure out how to do the a very simple thing in MS Bot Framework: allow the user to break out of any conversation, leave the current dialogs and return to the main menu by typing "quit", "exit" or "start over".

Here's the way my main conversation is set up:

    public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
    {
        try
        {
            if (activity.Type == ActivityTypes.Message)
            {
                UserActivityLogger.LogUserBehaviour(activity);

                if (activity.Text.ToLower() == "start over")
                {
                    //Do something here, but I don't have the IDialogContext here!
                }
                BotUtils.SendTyping(activity); //send "typing" indicator upon each message received
                await Conversation.SendAsync(activity, () => new RootDialog());
            }
            else
            {
                HandleSystemMessage(activity);
            }
        }

I know how to terminate a dialog with context.Done<DialogType>(this);, but in this method, I do not have access to the IDialogContext object, so I cannot call .Done().

Is there any other way to terminate the whole dialog stack when the user types a certain message, other than adding a check for that in each step of all dialogs?

Posted bounty:

I need a way to terminate all IDialogs without using the outrageous hack that I've posted here (which deletes all user data, which I need, e.g. user settings and preferences).

Basically, when the user types "quit" or "exit", I need to exit whatever IDialog is currently in progress and return to the fresh state, as if the user has just initiated a conversation.

I need to be able to do this from MessageController.cs, where I still do not have access to IDialogContext. The only useful data I seem to have there is the Activity object. I will be happy if someone points out to other ways to do that.

Another way to approach this is find some other way to check for the "exit" and "quit" keywords at some other place of the bot, rather than in the Post method.

But it shouldn't be a check that is done at every single step of the IDialog, because that's too much code and not even always possible (when using PromptDialog, I have no access to the text that the user typed).

Two possible ways that I didn't explore:

  • Instead of terminating all current IDialogs, start a new conversation with the user (new ConversationId)
  • Obtain the IDialogStack object and do something with it to manage the dialog stack.

The Microsoft docs are silent on this object so I have no idea how to get it. I do not use the Chain object that allows .Switch() anywhere in the bot, but if you think it can be rewritten to use it, it can be one of the ways to solve this too. However, I haven't found how to do branching between various types of dialogs (FormFlow and the ordinary IDialog) which in turn call their own child dialogs etc.

Answer

Kien Chu picture Kien Chu · Aug 26, 2016

PROBLEM BREAKDOWN

From my understanding of your question, what you want to achieve is to reset the dialog stack without completely destroy the bot state.


FACTS (from what I read from github repository)

  1. How the framework save the dialog stack is as below:

BotDataStore > BotData > DialogStack

  1. BotFramework is using AutoFac as an DI container
  2. DialogModule is their Autofac module for dialog components

HOW TO DO

Knowing FACTS from above, my solution will be

  1. Register the dependencies so we can use in our controller:

// in Global.asax.cs
var builder = new ContainerBuilder();
builder.RegisterModule(new DialogModule());
builder.RegisterModule(new ReflectionSurrogateModule());
builder.RegisterModule(new DialogModule_MakeRoot());

var config = GlobalConfiguration.Configuration;
builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
builder.RegisterWebApiFilterProvider(config);
var container = builder.Build();
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
  1. Get the Autofac Container (feel free to put anywhere in your code that you're comfortable with)

private static ILifetimeScope Container
{
    get
    {
        var config = GlobalConfiguration.Configuration;
        var resolver = (AutofacWebApiDependencyResolver)config.DependencyResolver;
        return resolver.Container;
    }
}
  1. Load the BotData in the scope
  2. Load the DialogStack
  3. Reset the DialogStack
  4. Push the new BotData back to BotDataStore

using (var scope = DialogModule.BeginLifetimeScope(Container, activity))
{
    var botData = scope.Resolve<IBotData>();
    await botData.LoadAsync(default(CancellationToken));
    var stack = scope.Resolve<IDialogStack>();
    stack.Reset();
    await botData.FlushAsync(default(CancellationToken));
}

Hope it helps.


UPDATE 1 (27/08/2016)

Thanks to @ejadib to point out, Container is already being exposed in conversation class.

We can remove step 2 in the answer above, in the end the code will look like

using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, activity))
{
    var botData = scope.Resolve<IBotData>();
    await botData.LoadAsync(default(CancellationToken));
    var stack = scope.Resolve<IDialogStack>();
    stack.Reset();
    await botData.FlushAsync(default(CancellationToken));
}