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 IDialog
s 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
IDialog
s, 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.
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)
- How the framework save the dialog stack is as below:
BotDataStore > BotData > DialogStack
- BotFramework is using AutoFac as an DI container
- DialogModule is their Autofac module for dialog components
HOW TO DO
Knowing FACTS from above, my solution will be
- 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);
- 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;
}
}
- Load the BotData in the scope
- Load the DialogStack
- Reset the DialogStack
- 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));
}
Here is a horribly ugly hack that works. It basically deletes all user data (which you might actually need) and this causes the conversation to restart.
If someone knows a better way, without deleting user data, please please share.
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
try
{
if (activity.Type == ActivityTypes.Message)
{
//if the user types certain messages, quit all dialogs and start over
string msg = activity.Text.ToLower().Trim();
if (msg == "start over" || msg == "exit" || msg == "quit" || msg == "done" || msg =="start again" || msg == "restart" || msg == "leave" || msg == "reset")
{
//This is where the conversation gets reset!
activity.GetStateClient().BotState.DeleteStateForUser(activity.ChannelId, activity.From.Id);
}
//and even if we reset everything, show the welcome message again
BotUtils.SendTyping(activity); //send "typing" indicator upon each message received
await Conversation.SendAsync(activity, () => new RootDialog());
}
else
{
HandleSystemMessage(activity);
}
}
I know this is a little old, but I had the same problem and the posted solutions are no longer the best approaches.
I'm not sure since what version this is available, but on 3.8.1 you can register IScorable
services that can be triggered anywhere in the dialog.
There is a sample code that shows how it works, and it does have a "cancel" global command handler:
https://github.com/Microsoft/BotBuilder-Samples/tree/master/CSharp/core-GlobalMessageHandlers
A part of the code will look like this:
protected override async Task PostAsync(IActivity item, string state, CancellationToken token)
{
this.task.Reset();
}
Additional code that worked for someone else:
private async Task _reset(Activity activity)
{
await activity.GetStateClient().BotState
.DeleteStateForUserWithHttpMessagesAsync(activity.ChannelId, activity.From.Id);
var client = new ConnectorClient(new Uri(activity.ServiceUrl));
var clearMsg = activity.CreateReply();
clearMsg.Text = $"Reseting everything for conversation: {activity.Conversation.Id}";
await client.Conversations.SendToConversationAsync(clearMsg);
}
This code is posted by user mmulhearn here: https://github.com/Microsoft/BotBuilder/issues/101#issuecomment-316170517