I started building a dialog in Microsoft's Bot Framework V4 and for that I want to use the custom validation of prompts. A couple of month ago, when version 4.4 was released, a new property "AttemptCount" was added to the PromptValidatorContext. This property gives information on how many times a user gave an answer. Obviously, it would be nice to end the current dialog if a user was reprompted several times. However, I did not find a way to get out of this state, because the given PromptValidatorContext does not offer a way to replace the dialog, unlike a DialogContext (or WaterfallStepContext). I asked that question on github, but didn't get an answer.
public class MyComponentDialog : ComponentDialog
{
readonly WaterfallDialog waterfallDialog;
public MyComponentDialog(string dialogId) : (dialogId)
{
// Waterfall dialog will be started when MyComponentDialog is called.
this.InitialDialogId = DialogId.MainDialog;
this.waterfallDialog = new WaterfallDialog(DialogId.MainDialog, new WaterfallStep[] { this.StepOneAsync, this.StepTwoAsync});
this.AddDialog(this.waterfallDialog);
this.AddDialog(new TextPrompt(DialogId.TextPrompt, CustomTextValidatorAsync));
}
public async Task<DialogTurnResult> StepOneAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var promptOptions = new PromptOptions
{
Prompt = MessageFactory.Text("Hello from text prompt"),
RetryPrompt = MessageFactory.Text("Hello from retry prompt")
};
return await stepContext.PromptAsync(DialogId.TextPrompt, promptOptions, cancellationToken).ConfigureAwait(false);
}
public async Task<DialogTurnResult> StepTwoAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// Handle validated result...
}
// Critical part:
public async Task<bool> CustomTextValidatorAsync(PromptValidatorContext<string> promptContext, CancellationToken cancellationToken)
{
if (promptContext.AttemptCount > 3)
{
// How do I get out of here? :-/
}
if (promptContext.Context.Activity.Text.Equals("password")
{
// valid user input
return true;
}
// invalid user input
return false;
}
}
If this feature is actually missing, I could probably do a workaround by saving the information in the TurnState and checking it in my StepTwo. Something like this:
promptContext.Context.TurnState["validation"] = ValidationEnum.TooManyAttempts;
But this doesn't really feel right ;-)
Does anyone has an idea?
Cheers,
Andreas
You have a few options depending on what you want to do in the validator function and where you want to put the code that manages the dialog stack.
Option 1: return false
Your first opportunity to pop dialogs off the stack will be in the validator function itself, like I mentioned in the comments.
if (promptContext.AttemptCount > 3)
{
var dc = await BotUtil.Dialogs.CreateContextAsync(promptContext.Context, cancellationToken);
await dc.CancelAllDialogsAsync(cancellationToken);
return false;
}
You were right to be apprehensive about this, because this actually can cause problems if you don't do it correctly. The SDK does not expect you to manipulate the dialog stack within a validator function, and so you need to be aware of what happens when the validator function returns and act accordingly.
Option 1.1: send an activity
You can see in the source code that a prompt will try to reprompt without checking to see if the prompt is still on the dialog stack:
if (!dc.Context.Responded)
{
await OnPromptAsync(dc.Context, state, options, true, cancellationToken).ConfigureAwait(false);
}
This means that even if you clear the dialog stack inside your validator function, the prompt will still try to reprompt after that when you return false. We don't want that to happen because the dialog has already been cancelled, and if the bot asks a question that it won't be accepting answers to then that will look bad and confuse the user. However, this source code does provide a hint about how to avoid reprompting. It will only reprompt if TurnContext.Responded is false. You can set it to true by sending an activity.
Option 1.1.1: send a message activity
It makes sense to let the user know that they've used up all their attempts, and if you send the user such a message in your validator function then you won't have to worry about any unwanted automatic reprompts:
await promptContext.Context.SendActivityAsync("Cancelling all dialogs...");
Option 1.1.2: send an event activity
If you don't want to display an actual message to the user, you can send an invisible event activity that won't get rendered in the conversation. This will still set TurnContext.Responded to true:
await promptContext.Context.SendActivityAsync(new Activity(ActivityTypes.Event));
Option 1.2: nullify the prompt
We may not need to avoid having the prompt call its OnPromptAsync if the specific prompt type allows a way to avoid reprompting inside OnPromptAsync. Again having a look at the source code but this time in TextPrompt.cs, we can see where OnPromptAsync does its reprompting:
if (isRetry && options.RetryPrompt != null)
{
await turnContext.SendActivityAsync(options.RetryPrompt, cancellationToken).ConfigureAwait(false);
}
else if (options.Prompt != null)
{
await turnContext.SendActivityAsync(options.Prompt, cancellationToken).ConfigureAwait(false);
}
So if we don't want to send any activities to the user (visible or otherwise), we can stop a text prompt from reprompting simply by setting both its Prompt and RetryPrompt properties to null:
promptContext.Options.Prompt = null;
promptContext.Options.RetryPrompt = null;
Option 2: return true
The second opportunity to cancel dialogs as we move up the call stack from the validator function is in the next waterfall step, like you mentioned in your question. This may be your best option because it's the least hacky: it doesn't depend on any special understanding of the internal SDK code that could be subject to change. In this case your whole validator function could be as simple as this:
private Task<bool> ValidateAsync(PromptValidatorContext<string> promptContext, CancellationToken cancellationToken)
{
if (promptContext.AttemptCount > 3 || IsCorrectPassword(promptContext.Context.Activity.Text))
{
// valid user input
// or continue to next step anyway because of too many attempts
return Task.FromResult(true);
}
// invalid user input
// when there haven't been too many attempts
return Task.FromResult(false);
}
Note that we're using a method called IsCorrectPassword to determine if the password is correct. This is important because this option depends on reusing that functionality in the next waterfall step. You had mentioned needing to save information in TurnState but this is unnecessary since everything we need to know is already in the turn context. The validation is based on the activity's text, so we can just validate that same text again in the next step.
Option 2.1: use WaterfallStepContext.Context.Activity.Text
The text that the user entered will still be available to you in WaterfallStepContext.Context.Activity.Text so your next waterfall step could look like this:
async (stepContext, cancellationToken) =>
{
if (IsCorrectPassword(stepContext.Context.Activity.Text))
{
return await stepContext.NextAsync(null, cancellationToken);
}
else
{
await stepContext.Context.SendActivityAsync("Cancelling all dialogs...");
return await stepContext.CancelAllDialogsAsync(cancellationToken);
}
},
Option 2.2: use WaterfallStepContext.Result
Waterfall step contexts have a builtin Result property that refers to the result of the previous step. In the case of a text prompt, it will be the string returned by that prompt. You can use it like this:
if (IsCorrectPassword((string)stepContext.Result))
Option 3: throw an exception
Going further up the call stack, you can handle things in the message handler that originally called DialogContext.ContinueDialogAsync by throwing an exception in your validator function, like CameronL mentioned in the deleted portion of their answer. While it's generally considered bad practice to use exceptions to trigger intentional code paths, this does closely resemble how retry limits worked in Bot Builder v3, which you mentioned wanting to replicate.
Option 3.1: use the base Exception type
You can throw just an ordinary exception. To make it easier to tell this exception apart from other exceptions when you catch it, you can optionally include some metadata in the exception's Source property:
if (promptContext.AttemptCount > 3)
{
throw new Exception(BotUtil.TooManyAttemptsMessage);
}
Then you can catch it like this:
try
{
await dc.ContinueDialogAsync(cancellationToken);
}
catch (Exception ex)
{
if (ex.Message == BotUtil.TooManyAttemptsMessage)
{
await turnContext.SendActivityAsync("Cancelling all dialogs...");
await dc.CancelAllDialogsAsync(cancellationToken);
}
else
{
throw ex;
}
}
Option 3.2: use a derived exception type
If you define your own exception type, you can use that to only catch this specific exception.
public class TooManyAttemptsException : Exception
You can throw it like this:
throw new TooManyAttemptsException();
Then you can catch it like this:
try
{
await dc.ContinueDialogAsync(cancellationToken);
}
catch (TooManyAttemptsException)
{
await turnContext.SendActivityAsync("Cancelling all dialogs...");
await dc.CancelAllDialogsAsync(cancellationToken);
}
Declare a flag variable in user state class and update the flag inside the if block:
if (promptContext.AttemptCount > 3)
{
\\fetch user state object
\\update flag here
return true;
}
After returning true you will be taken to the next dialog in waterfall step, where you can check the flag value, display an appropriate message and terminate dialog flow. You can refer to microsoft docs to know how to use the User state data
The prompt validator context object is a more specific object only concerned with passing or failing the validator.
** removed incorrect answer **
You can create a class with a WaterfallStep and a PromptValidator. That class would (i) handle the logic to exit the PromptValidator and (ii) handle the logic to cancel/end/proceed the dialog after that. This solution is category of Kyle Delaney answer which returns true in the PromptValidator.
I called that class WaterfallStepValidation:
private readonly Func<string, Task<bool>> _validator;
private readonly int _retryCount;
private bool _isInputValid = false;
public WaterfallStepValidation(Func<string, Task<bool>> validator, int retryCount)
{
_validator = validator;
_retryCount = retryCount;
}
public async Task<DialogTurnResult> CheckValidInputStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
if (!_isInputValid)
{
await stepContext.Context.SendActivityAsync("Could not proceed...");
// Here you could also end all dialogs or just proceed to the next step
return await stepContext.EndDialogAsync(false);
}
return await stepContext.NextAsync(stepContext.Result, cancellationToken);
}
public async Task<bool> PromptValidatorAsync(PromptValidatorContext<string> promptContext, CancellationToken cancellationToken)
{
_isInputValid = await _validator(promptContext.Recognized.Value);
if (!_isInputValid && promptContext.AttemptCount >= _retryCount)
{
_isInputValid = false;
return true;
}
return _isInputValid;
}
And then you call it like this:
var ageStepValidation = new WaterfallStepValidation(AgeValidator, retryCount: 3);
AddDialog(new TextPrompt("AgeTextPromptId", ageStepValidation.PromptValidatorAsync));
var waterfallSteps = new List<WaterfallStep>()
{
PromptNameStepAsync,
PromptAgeStepAsync,
ageStepValidation.CheckValidInputStepAsync,
PromptChoicesStepAsync
};
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), waterfallSteps));
I think this is an elegant workaround for this problem.
The weak points of this aproch are:
you have to place CheckValidInputStepAsync right after the step with the validation
is still a kind of hack, since the PromptValidator returns true if fails the validation
However, the strong points are:
it doesn't make big hacks, just that return true
most of the logic is encapsulated in the WaterfallStepValidation class
Related
I've submitted this as a bug report but also filing it here in case I'm doing something wrong and this isn't really a bug.
Bot framework version
3.16.1.38846
Describe the issue
I'm trying to create a localised formflow that can be proactively triggered. I'm able to create the form and trigger it through an API call using the proactive dialog trigger. However, the first question is always in English, despite the locale not being English. Nonetheless, it expects an answer in the locale in play (Mandarin in this case, (zh-SG)).
If I were to not trigger it through my API, all my questions are localised based on whatever locale I send in through the bot framework emulator. I tested this by setting up a keyword check in the root dialog, and I'm able to get all my formflow questions asked in the language specified. I've attached screenshots of how this seems to play out too.
To Reproduce
Steps to reproduce the behavior:
Create a simple form
Localise the form using the guide in the documentation
Call the form using the bot framework emulator using a simple keyword check in the root dialog. Use the default locale of en-US (Sample below)
Call the form using the bot framework emulator using a simple keyword check in the root dialog. Use the other language's locale (in this case, zh-SG)
Call the form using a proactive dialog trigger through a WebAPI. Method looks like this. Parameters such as the activity object have been previously seralised to a database. I've obscured certain parameters to protect some confidential information
Sample trigger
if (activity.Text.Equals("Trigger"))
{
var form = new FormDialog<Form1>(new Form1(), Form1.BuildForm, FormOptions.PromptInStart, null);
context.Call(form, formCompleteAsync);
}
WebAPI method
public IHttpActionResult Post([FromBody]Model Model)
{
if (ModelState.IsValid)
{
try
{
StartProactiveDialogAsync(model.someId, model.anotherId)
return Ok();
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
else
{
return BadRequest(ModelState);
}
}
StartProactiveDialogAsync
public async Task StartProactiveDialogAsync(someId, anotherId )
{
try
{
// Recreate the message from the conversation reference that was saved previously.
Activity activity = JsonConvert.DeserializeObject<Activity>(BotUserData.ConversationReference);
MicrosoftAppCredentials.TrustServiceUrl(activity.ServiceUrl);
var client = new ConnectorClient(new Uri(activity.ServiceUrl));
// Create a scope that can be used to work with state from bot framework.
using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, activity))
{
var botData = scope.Resolve<IBotData>();
await botData.LoadAsync(CancellationToken.None);
// This is the dialog stack.
var stack = scope.Resolve<IDialogTask>();
// Create the new dialog and add it to the stack.
var dialog = new CallDialog(parameter1, parameter2);
stack.Call(dialog.Void<object, IMessageActivity>(), null);
await stack.PollAsync(CancellationToken.None);
// Flush the dialog stack back to its state store.
await botData.FlushAsync(CancellationToken.None);
}
}
catch (Exception e)
{
await ProprietaryDiagnosticsTool.SendDiagnostic(e);
}
}
CallDialog
public class CallDialog : IDialog<object>
{
Parameter1 param1;
Parameter2 param2;
public CallDialog(Parameter1 param1, Parameter2 param2)
{
this.param1 = param1;
this.param2 = param2;
}
public async Task StartAsync(IDialogContext context)
{
switch (param1.Id)
{
case 1:
{
var form = new FormDialog<Form1>(new Form1(), Form1.BuildForm, FormOptions.PromptInStart, null);
context.Call(form, formComplete);
break;
}
case 2:
{
var form = new FormDialog<Form2>(new Form2(), Form2.BuildForm, FormOptions.PromptInStart, null);
context.Call(form, formComplete);
break;
}
case 3:
{
var form = new FormDialog<Form3>(new Form3(), Form3.BuildForm, FormOptions.PromptInStart, null);
context.Call(form, formComplete);
break;
}
}
}
private async Task formComplete(IDialogContext context, IAwaitable<FormParent> result)
{
var ans = await result;
await context.PostAsync("Result received");
context.Done(this);
}
}
Expected behavior
When calling the proactive dialog which calls the form in a different locale, the form should be presented in the locale specified
Screenshots
English formflow triggered through keyword - correct
English formflow triggered through API - correct
Mandarin formflow triggered through keyword - correct
Mandarin formflow triggered through API - incorrect
The error message says
"Yes" is not an option for question 1.
Additional information
I've traced the context.activity object through the various methods, from StartProactiveDialogAsync to CallDialog all the way till the formComplete method. The locale does tend to be correct, its simply the display of the first question of the proactive dialog calling the formflow that happens to be in the wrong language.
Eric from Microsoft helped to resolve this.
His full answer can be found here: https://github.com/Microsoft/BotBuilder-V3/issues/82
Simply put the locale needs to be pulled out of context.activity.privateconversationdata and sent to the form itself as it does not pick up the locale on its own when resuming a conversation.
Essentially, I'd really like some specific commands for my Discord Bot, but I have very little experience with C#. My main goals are:
A welcome message to newcomers. (Privately, to the new user. Nobody else sees it but the newcomer via server)
Wait for a specific message to be typed. Once this message is typed, the bot adds a role to the user, and delete's the user's message that was typed (so that following newcomer won't see the message and using it instead of reading the rules like I intend for them to see first.)
A timer function that enables as soon as a new member joins. This timer is to go on for 3 days and if the new user does not fulfill the 2nd task in that time period, they will be kicked until they find a new invite.
Anything other than the specific message typed will be tried 3 attempts. if the user does not type the correct word into the input, they will be warned each time, until the attempt number reaches 3. After it exceeds 3, they will be kicked until they find a new invite.
Here is my current list of commands:
using System.Threading.Tasks;
using Discord;
using Discord.Commands;
using Discord.WebSocket;
namespace BooleanBot.Modules
{
public class commands : ModuleBase<SocketCommandContext>
{
[Command("help")]
public async Task help()
{
var a = new Discord.EmbedBuilder();
a.WithTitle("Commands");
a.WithDescription("General Commands\n-- .help // Gives list of commands to use");
Discord.IDMChannel gencom = await Context.Message.Author.GetOrCreateDMChannelAsync();
await gencom.SendMessageAsync("", false, a);
await gencom.CloseAsync();
}
[Command("kick")]
[RequireBotPermission(Discord.GuildPermission.KickMembers)]
[RequireUserPermission(Discord.GuildPermission.KickMembers)]
public async Task KickAsync(Discord.IGuildUser user, [Remainder] string reason)
{
if (user.GuildPermissions.KickMembers)
{
var b = new Discord.EmbedBuilder();
b.WithTitle("User Kicked");
b.WithDescription(user.Username + "was kicked.");
await Context.Channel.SendMessageAsync("", false, b);
await user.KickAsync();
}
}
[Command("postwelcome")]
public async Task welcome()
{
var b = new Discord.EmbedBuilder();
b.WithTitle("Welcome to the Anthamation Server! I'm Antha-bot, the housekeeper! My server prefex is !Yo. Let's get started!");
b.WithDescription("Before you can do ANYTHING, you must go to #rules-and-access channel and read through the rules first! You will also find instructions on how to access the channels! PLEASE NOTE: If you are not a verified member within 3 days or type in something OTHER than the desired answer, you will be kicked automatically. See you on the other side!");
await Context.Channel.SendMessageAsync("", false, b);
}
}
}
Very sorry if this seems too lengthy for Stackoverflow. I have no where else to turn to.
With your welcome message, you need to subscribe to the UserJoined action. Somewhere else in your code I assume you have something along the lines of client.MessageReceived += client_MessageReceived;. To subscribe to the UserJoined action you need to add client.UserJoined += client.UserJoined;. Then once you have done that to it you can execute the client_UserJoined() code.
private async Task client_UserJoined(SocketGuildUser arg)
{
// whatever you put here will execute when a user joins.
}
I am currently experimenting with Roslyn and Code Actions, more specific Code Refactorings.
It feels kind of easy, but I have a difficulty I cannot solve.
Code actions are executed once against a dummy workspace as a "preview" option, so that you can see the actual changes before you click the action and execute it against the real workspace.
Now I am dealing with some things Roslyn can't really do (yet), so I am doing some changes via EnvDTE. I know, it's bad, but I couldn't find another way.
So the issue here is:
When I hover over my code action, the code gets executed as preview, and it should NOT do the EnvDTE changes. Those should only be done when the real execute happens.
I have created a gist with a small example of my code. It doesn't really makes sense, but should show what I want to achieve. Do some modifications via roslyn, then do something via EnvDTE, like changing Cursor position. But of course only on the real execution.
The relevant part for those who can't click the gist:
public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(continueOnCapturedContext: false);
var node = root.FindNode(context.Span);
var dec = node as MethodDeclarationSyntax;
if (dec == null)
return;
context.RegisterRefactoring(CodeAction.Create("MyAction", c => DoMyAction(context.Document, dec, c)));
}
private static async Task<Solution> DoMyAction(Document document, MethodDeclarationSyntax method, CancellationToken cancellationToken)
{
var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken);
var root = await syntaxTree.GetRootAsync(cancellationToken);
// some - for the question irrelevant - roslyn changes, like:
document = document.WithSyntaxRoot(root.ReplaceNode(method, method.WithIdentifier(SyntaxFactory.ParseToken(method.Identifier.Text + "Suffix"))));
// now the DTE magic
var preview = false; // <--- TODO: How to check if I am in preview here?
if (!preview)
{
var requestedItem = DTE.Solution.FindProjectItem(document.FilePath);
var window = requestedItem.Open(Constants.vsViewKindCode);
window.Activate();
var position = method.Identifier.GetLocation().GetLineSpan().EndLinePosition;
var textSelection = (TextSelection) window.Document.Selection;
textSelection.MoveTo(position.Line, position.Character);
}
return document.Project.Solution;
}
You can choose to override ComputePreviewOperationsAsync to have different behavior for Previews from regular code.
I've found the solution to my problem by digging deeper and trial and error after Keven Pilch's answer. He bumped me in the right direction.
The solution was to override both the ComputePreviewOperationsAsync and the GetChangedSolutionAsync methods in my own CodeAction.
Here the relevant part of my CustomCodeAction, or full gist here.
private readonly Func<CancellationToken, bool, Task<Solution>> _createChangedSolution;
protected override async Task<IEnumerable<CodeActionOperation>> ComputePreviewOperationsAsync(CancellationToken cancellationToken)
{
const bool isPreview = true;
// Content copied from http://sourceroslyn.io/#Microsoft.CodeAnalysis.Workspaces/CodeActions/CodeAction.cs,81b0a0866b894b0e,references
var changedSolution = await GetChangedSolutionWithPreviewAsync(cancellationToken, isPreview).ConfigureAwait(false);
if (changedSolution == null)
return null;
return new CodeActionOperation[] { new ApplyChangesOperation(changedSolution) };
}
protected override Task<Solution> GetChangedSolutionAsync(CancellationToken cancellationToken)
{
const bool isPreview = false;
return GetChangedSolutionWithPreviewAsync(cancellationToken, isPreview);
}
protected virtual Task<Solution> GetChangedSolutionWithPreviewAsync(CancellationToken cancellationToken, bool isPreview)
{
return _createChangedSolution(cancellationToken, isPreview);
}
The code to create the action stays quite similar, except the bool is added and I can check against it then:
public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
// [...]
context.RegisterRefactoring(CustomCodeAction.Create("MyAction",
(c, isPreview) => DoMyAction(context.Document, dec, c, isPreview)));
}
private static async Task<Solution> DoMyAction(Document document, MethodDeclarationSyntax method, CancellationToken cancellationToken, bool isPreview)
{
// some - for the question irrelevant - roslyn changes, like:
// [...]
// now the DTE magic
if (!isPreview)
{
// [...]
}
return document.Project.Solution;
}
Why those two?
The ComputePreviewOperationsAsync calls the the normal ComputeOperationsAsync, which internally calls ComputeOperationsAsync. This computation executes GetChangedSolutionAsync. So both ways - preview and not - end up at GetChangedSolutionAsync. That's what I actually want, calling the same code, getting a very similar solution, but giving a bool flag if it is preview or not too.
So I've written my own GetChangedSolutionWithPreviewAsync which I use instead. I have overriden the default GetChangedSolutionAsync using my custom Get function, and then ComputePreviewOperationsAsync with a fully customized body. Instead of calling ComputeOperationsAsync, which the default one does, I've copied the code of that function, and modified it to use my GetChangedSolutionWithPreviewAsync instead.
Sounds rather complicated in written from, but I guess the code above should explain it quite well.
Hope this helps other people.
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.
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
I know this is an old question, but I got here so... It seems like the 'recommended' way to handle interruptions is to use a Main dialog as the entry point to your bot and then make that main dialog inherit some kind of Cancel/Help dialog whose OnContinueDialogAsync function gets called first. There you can call CancelAllDialosAsync from the DialogContext provided in the parameter.
Read more here https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-howto-handle-user-interrupt?view=azure-bot-service-4.0&tabs=csharp
The Situation
I'm working on a OAuth2 Api Wrapper. Some api routes are for logged people and some for anonymous and logged.
Here is an example of one method in my wrapper :
public async Task<UploadListResponse> List(bool pagination = false, int page = 1, int limit = 10)
{
var request = UploadRequests.List(pagination, page, limit);
var cancellationTokenSource = new CancellationTokenSource();
var restResponse = await Context.Client.ExecuteTaskAsync(request, cancellationTokenSource.Token);
return restResponse.Handle<UploadListResponse>();
}
I build a request with all parameter set up then execute the request and then handle the answer in case I have an api error and then output an object containing all the data that request gave me.
The problem
With OAuth2, when you log to the API you'll receive an access token and a refresh token. If your access token is expired you have to contact the api with your refresh token to get a fresh new access token.
As I said earlier some of my method needs you to be logged but if your access token is expired I want to try to refresh token before throwing an exception like with this method :
public async Task<bool> NeedRelog()
{
try
{
var validAuth = await ValidAuth();
}
catch
{
try
{
var refresh = await Refresh(Context.Client.Config.RefreshToken);
}
catch
{
return true;
}
}
return false;
}
ValidAuth check with the API if you are logged and if I have an exception then I'll try to refreshToken.
I want to tag method that need logged to call NeedRelog() and those who aren't tag to not call it.
I may just do it in every method but it wouldn't be clean.
What I've done so far
I've found a great tool : PostSharp that seems to fit my needs.
I've started to do a checkLog aspect like this :
[Serializable]
public class CheckLog : OnMethodBoundaryAspect, IOnStateMachineBoundaryAspect
{
public CheckLog()
{
ApplyToStateMachine = false;
}
public override void OnEntry(MethodExecutionArgs args)
{
var instance = (ApiService)args.Instance;
var res = instance.Parent.OAuth.NeedRelog().Result;
if (!res)
{
args.Exception = new Exception("Need to relog");
args.FlowBehavior = FlowBehavior.Return;
}
}
}
Where I'm stuck
The Main problem is with the call to my NeedRelog() Method. Due to the fact this is an async method I'm struggling to make my aspect await for it.
If my OnEntry method is async then It won't block the call if you are not logged.
If my OnEntry method is not async and I wait for needLog it freeze and nothing happen.
I really want to know to use this kind of "conditional method call" with postsharp, it looks awesome but the fact is after looking for hours in the documentation I didn't find a way to do what I want.
I'm starting to ask myself if it is even possible to achieve what I'm aiming to do.
Did you try using a way to make the call synchronous maybe with something like this stackoverflow.com/a/25097498/3131696 ? – M22an 5 hours ago
As I can't mark a comment as answering a question I quote your comment to make this question answered as it is said here : link
Thanks you for this M22an.