ResumeAfter method is already called without calling context.done in the next dialog - botframework

I have implemented a structure where a QnA dialog is first started. If the QnA Dialog cannot solve the problem then it starts a Luis Dialog which has some main functionalities defined. Based on those main functionalities I start specific dialogs that can solve the problem.
My problem is that when I try to start LuisDialog from QnAMaker, it starts another LuisDialog to for conversation, That dialog doesn't stop on with wait method and automatically calls ResumeAfter method immediately after executing.
QnADialog:
protected override async Task RespondFromQnAMakerResultAsync(IDialogContext context, IMessageActivity message, QnAMakerResults results)
{
if (results == null || results.Answers.Count==0 || !IsConfidentAnswer(results) || results.Answers.FirstOrDefault().Score<0.75) {
await context.Forward(new MainLuisDialog(), MessageReceived, context.Activity.AsMessageActivity(), CancellationToken.None);
}
}
First Luis Dialog:
[LuisIntent(ErrorFileLink)]
public async Task ErrorFileLinkIntentHandler(IDialogContext context, LuisResult result) {
await context.Forward(new ErrorFileLinkDialog(), CallBackHandler, context.Activity.AsMessageActivity(), CancellationToken.None);
}
private async Task CallBackHandler(IDialogContext context, IAwaitable<object> result)
{
try {
var returnedResult = await result;
if (returnedResult as string == "done")
context.Done(false);
}
catch (Exception e) {
}
}
2nd Luis Dialog:
[LuisIntent(MainAppIntent)]
public async Task MainAppIntentHandler(IDialogContext context, LuisResult result)
{
if(context.GetPrivateConversationData<SyncIssueStates>(CurrentDialogState) == SyncIssueStates.ExpectingSyncCompleteMessage)
{
await context.PostAsync(Utility.GetResourceString("SYNC_ISSUE_PLEASE_WAIT_SYNC_COMPELTE"));
context.Wait(MessageReceived);
return;
}
await context.PostAsync(Utility.GetResourceString("SYNC_ISSUE_GET_ERROR_MESSAGE"));
context.SetPrivateConversationData(CurrentDialogState, SyncIssueStates.ExpectingErrorMessage);
context.Wait(MessageReceived);
}
CallBackHandler Method in First Luis Dialog is called right after Forward is executed.

I think this behavior is due to the fact that the QnAMakerDialog calls context.Done(true); inside the DefaultWaitNextMessageAsync method. ref: https://github.com/Microsoft/BotBuilder-CognitiveServices/blob/master/CSharp/Library/QnAMaker/QnAMaker/QnAMakerDialog.cs#L203
Try overriding the DefaultWaitNextMessageAsync method instead:
protected override async Task DefaultWaitNextMessageAsync(IDialogContext context, IMessageActivity message, QnAMakerResults results)
{
if (results == null || results.Answers.Count == 0 || !IsConfidentAnswer(results) || results.Answers.FirstOrDefault().Score < 0.75)
{
await context.Forward(new FirstDialog(), AfterForward, context.Activity.AsMessageActivity(), CancellationToken.None);
context.Wait(base.MessageReceivedAsync);
}
else
{
await base.DefaultWaitNextMessageAsync(context, message, results);
}
}

Related

How to call Dialog Class From a LUIS Intent

I am using Bot Framework V4 dispatch model to invoke LUIS and QnA services.
I can call the code inside of the top scoring intents class.
However, I couldn't find a way to call an external dialog from it. How can I do that?
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
namespace Microsoft.BotBuilderSamples
{
public class DispatchBot : ActivityHandler
{
private ILogger<DispatchBot> _logger;
private IBotServices _botServices;
public DispatchBot(IBotServices botServices, ILogger<DispatchBot> logger)
{
_logger = logger;
_botServices = botServices;
}
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
// First, we use the dispatch model to determine which cognitive service (LUIS or QnA) to use.
var recognizerResult = await _botServices.Dispatch.RecognizeAsync(turnContext, cancellationToken);
// Top intent tell us which cognitive service to use.
var topIntent = recognizerResult.GetTopScoringIntent();
// Next, we call the dispatcher with the top intent.
await DispatchToTopIntentAsync(turnContext, topIntent.intent, recognizerResult, cancellationToken);
}
protected override async Task OnMembersAddedAsync(IList<ChannelAccount> membersAdded, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
const string WelcomeText = "Type a greeting, or a question about the weather to get started.";
foreach (var member in membersAdded)
{
if (member.Id != turnContext.Activity.Recipient.Id)
{
await turnContext.SendActivityAsync(MessageFactory.Text($"Welcome to Dispatch bot {member.Name}. {WelcomeText}"), cancellationToken);
}
}
}
private async Task DispatchToTopIntentAsync(ITurnContext<IMessageActivity> turnContext, string intent, RecognizerResult recognizerResult, CancellationToken cancellationToken)
{
switch (intent)
{
case "l_HomeAutomation":
//WANT TO CALL THE EXTERNAL DIALOG CLASS FROM HERE
//await ProcessHomeAutomationAsync(turnContext, recognizerResult.Properties["luisResult"] as LuisResult, cancellationToken);
break;
case "l_Weather":
await ProcessWeatherAsync(turnContext, recognizerResult.Properties["luisResult"] as LuisResult, cancellationToken);
break;
case "q_sample-qna":
await ProcessSampleQnAAsync(turnContext, cancellationToken);
break;
default:
_logger.LogInformation($"Dispatch unrecognized intent: {intent}.");
await turnContext.SendActivityAsync(MessageFactory.Text($"Dispatch unrecognized intent: {intent}."), cancellationToken);
break;
}
}
private async Task ProcessHomeAutomationAsync(ITurnContext<IMessageActivity> turnContext, LuisResult luisResult, CancellationToken cancellationToken)
{
_logger.LogInformation("ProcessHomeAutomationAsync");
// Retrieve LUIS result for Process Automation.
var result = luisResult.ConnectedServiceResult;
var topIntent = result.TopScoringIntent.Intent;
await turnContext.SendActivityAsync(MessageFactory.Text($"HomeAutomation top intent {topIntent}."), cancellationToken);
await turnContext.SendActivityAsync(MessageFactory.Text($"HomeAutomation intents detected:\n\n{string.Join("\n\n", result.Intents.Select(i => i.Intent))}"), cancellationToken);
if (luisResult.Entities.Count > 0)
{
await turnContext.SendActivityAsync(MessageFactory.Text($"HomeAutomation entities were found in the message:\n\n{string.Join("\n\n", result.Entities.Select(i => i.Entity))}"), cancellationToken);
}
}
private async Task ProcessWeatherAsync(ITurnContext<IMessageActivity> turnContext, LuisResult luisResult, CancellationToken cancellationToken)
{
_logger.LogInformation("ProcessWeatherAsync");
// Retrieve LUIS results for Weather.
var result = luisResult.ConnectedServiceResult;
var topIntent = result.TopScoringIntent.Intent;
await turnContext.SendActivityAsync(MessageFactory.Text($"ProcessWeather top intent {topIntent}."), cancellationToken);
await turnContext.SendActivityAsync(MessageFactory.Text($"ProcessWeather Intents detected::\n\n{string.Join("\n\n", result.Intents.Select(i => i.Intent))}"), cancellationToken);
if (luisResult.Entities.Count > 0)
{
await turnContext.SendActivityAsync(MessageFactory.Text($"ProcessWeather entities were found in the message:\n\n{string.Join("\n\n", result.Entities.Select(i => i.Entity))}"), cancellationToken);
}
}
private async Task ProcessSampleQnAAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
_logger.LogInformation("ProcessSampleQnAAsync");
var results = await _botServices.SampleQnA.GetAnswersAsync(turnContext);
if (results.Any())
{
await turnContext.SendActivityAsync(MessageFactory.Text(results.First().Answer), cancellationToken);
}
else
{
await turnContext.SendActivityAsync(MessageFactory.Text("Sorry, could not find an answer in the Q and A system."), cancellationToken);
}
}
}
}
You'll need to initialize instances of the required dialogs in your DispatchBot, and then invoke them like this:
await this.weatherDialog.RunAsync(turnContext, dialogStateAccessor, cancellationToken)
Here is some sample code. It's slightly different from your design since I am delegating intent detection to a parent dialog and then do the logic branching from there (to keep the bot class logic simple), but how to invoke dialogs is the same.
Update based on discussion below:
If you are using multi turn dialogs, I would suggest you follow the pattern in my code. Otherwise you'll have to deal dialog continue by yourself and good luck with it.

Next step after PromptAsync is not called

I have this simple dialog, with 2 simple waterfall steps.
The user sees "How may I help you today?" and when it answers, nothing happens. I can't get Validate to work.
Am I missing something? I'm using SDK 4.1.5.
public ComplaintsDialog() : base(nameof(ComplaintsDialog))
{
var steps = new WaterfallStep[]
{
Ask,
Validate
};
AddDialog(new WaterfallDialog("flow", steps));
AddDialog(new TextPrompt("asking"));
}
private static async Task<DialogTurnResult> Ask(WaterfallStepContext sc, CancellationToken cancellationToken)
{
return await sc.PromptAsync("asking", new PromptOptions { Prompt = new Activity { Text = "How may I help you today?", Type= ActivityTypes.Message} }, cancellationToken);
}
private static async Task<DialogTurnResult> Validate(WaterfallStepContext sc, CancellationToken cancellationToken)
{
var answer = sc.Result;
await sc.Context.SendActivityAsync(answer.ToString());
return await sc.EndDialogAsync();
}
}
UPDATE
I tried to simplify the code, and this is how I currently call ComplaintsDialog directly from the main bot.
It looks like the stack is always empty when it gets to await dc.ContinueDialogAsync();, so it's going into a loop and start ComplaintsDialog over and over again
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken)
{
// Create dialog context.
var dc = await _dialogs.CreateContextAsync(turnContext);
switch (turnContext.Activity.Type)
{
case ActivityTypes.Message:
// Continue outstanding dialogs.
await dc.ContinueDialogAsync();
// Begin main dialog if no outstanding dialogs/ no one responded.
if (!dc.Context.Responded)
{
await dc.BeginDialogAsync(nameof(ComplaintsDialog));
}
break;
case ActivityTypes.ConversationUpdate:
if (dc.Context.Activity.MembersAdded != null && dc.Context.Activity.MembersAdded.Any())
{
foreach (var newMember in dc.Context.Activity.MembersAdded)
{
if (newMember.Id != dc.Context.Activity.Recipient.Id)
{
await dc.BeginDialogAsync(nameof(WelcomeDialog));
}
}
}
break;
}
}
The code example you provided looks like it should be working so the problem is probably elsewhere.
My guess is that you are starting the ComplaintsDialog inside of a WaterfallStep (from another dialog) so make sure that you are calling the BeginDialogAsync method like this:
return await stepContext.BeginDialogAsync(nameof(ComplaintsDialog));
instead of:
await stepContext.BeginDialogAsync(nameof(ComplaintsDialog));
If this is not the error probably more information is necessary
Update
Your problem is on the OnTurnAsync method. You're not saving the new turn into the conversation state. The Message case on your switch should look like this:
case ActivityTypes.Message:
if (dc.ActiveDialog == null)
{
await dc.BeginDialogAsync(nameof(ComplaintsDialog), cancellationToken);
}
else
{
await dc.ContinueDialogAsync(cancellationToken);
}
await _accessors.ConversationState.SaveChangesAsync(turnContext);
break;
And your constructor:
private readonly MyBotAccessors _accessors;
public MyBot(MyBotAccessors accessors, ILoggerFactory loggerFactory)
{
...
_accessors = accessors ?? throw new System.ArgumentNullException(nameof(accessors));
...
}
SaveChangesAsync documentation

Return values from proactive dialog

My scenario is something like this, there are 2 users -> U1 and U2.
U1 wants the bot to ping U2 and get some information back
This is where i use proactive dialog based messages.
I am currently using the v3 Bot Builder SDK sample
using (var scope = DialogModule.BeginLifetimeScope(Microsoft.Bot.Builder.Dialogs.Conversation.Container, message))
{
var botData = scope.Resolve<IBotData>();
await botData.LoadAsync(CancellationToken.None);
//This is our dialog stack
var task = scope.Resolve<IDialogTask>();
// Interrupt the stack. This means that we're stopping whatever conversation that is currently happening with the user
// Then adding this stack to run and once it's finished, we will be back to the original conversation
var dialog = new GetFeedbackDialog(this.UserData);
task.Call(dialog.Void<object, IMessageActivity>(), null);
await task.PollAsync(CancellationToken.None);
// Flush the dialog stack back to its state store.
await botData.FlushAsync(CancellationToken.None);
}
class GetFeedbackDialog : IDialog<object>
{
private RequestFeedbackData userData;
public GetFeedbackDialog()
{
}
public GetFeedbackDialog(RequestFeedbackData userData)
{
this.userData = userData;
}
public async Task StartAsync(IDialogContext context)
{
await context.PostAsync($"Hello, Please provide feedback for "+ userData.OrganizerName+" on "+userData.FeedbackSubject);
context.Wait(this.MessageReceivedAsync);
}
public virtual async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
{
var message = await result;
if (message.Text.ToLower().Equals("quit"))
{
await context.PostAsync("Quitting.. Thank you for using FeedbackBot!");
context.Done(String.Empty); // Finish this dialog.
}
await context.PostAsync("Thank you for your feedback!");
context.Done(message.Text);
}
}
How do can i access the response typed by U2 in GetFeedbackDialog from where i initiated the proactive message ?
Thanks in advance for your help

A very long message for LUIS dialog resets dialog state

Using bot framework emulator v.3.5.36, if a user sends long text (about 1K characters), emulator silently resets dialog stack back to root dialog, without any errors or warnings. (see the screenshot below.)
Is there a declared message limit for bot framework?
Is there a way for bot to handle such situations and warn user instead of this silent something?
There is nothing really specific about the code at all:
[LuisModel("{GUID}", "{CODE}", LuisApiVersion.V2, domain: "westeurope.api.cognitive.microsoft.com", threshold: 0.5)]
[Serializable]
public class LuisSearchDialog2 : LuisDialog<object>
{
[LuisIntent("")]
[LuisIntent("None")]
public async Task None(IDialogContext context, LuisResult result)
{
await context.PostAsync(JsonConvert.SerializeObject(result));
context.Wait(this.MessageReceived);
}
}
A simple approach would be to check the length of your message in the MessageController and decide whether you want to process it or not.
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
if (activity.Type == ActivityTypes.Message)
{
MicrosoftAppCredentials.TrustServiceUrl(activity.ServiceUrl);
var connector = new ConnectorClient(new Uri(activity.ServiceUrl));
if (activity.Text != null && activity.Text.Length > 200)
{
var errorReply = activity.CreateReply();
errorReply.Text = "Well well, that is too much of data. How about keeping it simple? How can I help you?";
await connector.Conversations.ReplyToActivityAsync(errorReply);
}
else
{
await Conversation.SendAsync(activity, () => new Dialogs.RootDialog());
}
}
}
The reason is that base LuisDialog doens't handle failed API requests (in case if query is too long, it returns 414 code). So the simplest way to handle such errors is to override MessageReceived as follows:
[Serializable]
public class LuisSearchDialog2 : LuisDialog<object>
{
protected override async Task MessageReceived(IDialogContext context, IAwaitable<IMessageActivity> activity)
{
try
{
await base.MessageReceived(context, activity);
}
catch(HttpRequestException e)
{
// Handle error here
//await context.PostAsync("Error: " + e.ToString());
context.Wait(this.MessageReceived);
}
}
}

Delayed dialog with resume handler

I have the following scenario I think I'm doing it wrong.
I have a RootDialog which calls a ResultDialog. The ResultDialog presents the user a list of results (using HeroCard).
The ResultDialog closes itself using context.Done(..) after the message was posted.
In the RootDialog- AfterResultDialog Resume handler I want to ask the user if he has found the matching result, using another dialog (NotificationDialog), but I want to do that after 30 seconds.
After some research, this seems like it must be done using proactive messages.
It this example, I found a way to post the NotificationDialog in a proactive way.
private async Task AfterResultDialog(IDialogContext context, IAwaitable<object> result)
{
var message = await result as IMessageActivity;
var conversationReference = context.Activity.ToConversationReference();
ConversationStarter.conversationReference = JsonConvert.SerializeObject(conversationReference);
t = new Timer(timerEvent);
t.Change(30000, Timeout.Infinite);
context.Wait<string>(NotificationDialogAfter);
}
public void timerEvent(object target)
{
t.Dispose();
ConversationStarter.Resume();
}
But the problem I have is that I'm interested in the result of this NotifcationDialog to know what the user wants to do next.
But all examples I found using proactive-messages do not regard the result of a proactive message with dialog:
public class ConversationStarter
{
public static string conversationReference;
public static async Task Resume()
{
var message = JsonConvert.DeserializeObject<ConversationReference>(conversationReference).GetPostToBotMessage();
var client = new ConnectorClient(new Uri(message.ServiceUrl));
using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message))
{
var botData = scope.Resolve<IBotData>();
await botData.LoadAsync(CancellationToken.None);
var task = scope.Resolve<IDialogTask>();
// here it seems only to be possible to call a dialog using Void
var dialog = new NotificationDialog();
task.Call(dialog.Void<object, IMessageActivity>(), null);
await task.PollAsync(CancellationToken.None);
await botData.FlushAsync(CancellationToken.None);
}
}
}
The NotificationDialogAfter handler should decide based on the user input which dialog to call next:
private async Task NotificationDialogAfter(IDialogContext context, IAwaitable<string> result)
{
var whereToContinue = await result;
if (whereToContinue.Equals("Start over"))
{
context.ClearAllConversationDataKeys();
context.Call(new TagDialog(), this.TagDialogAfter);
}
else if (whereToContinue == "Tell friends")
{
context.Call(new TellFriendsDialog(), TellFriendsDialogAfter);
}
else if (whereToContinue == "Feedback")
{
context.Call(new FeedbackDialog(), this.FeedbackDialogAfter);
}
}
So what I basically want is that the result of the NotificationDialog is forwarded to the NotificationDialogAfter handler which the Root dialog is waiting for.
Is this even possible?
I solve the problem by defining static continue handlers (in GlobalContinueHandler), that I can provide inside the NotificationDialog, when calling other dialogs.
[Serializable]
public class NotificationDialog : IDialog<string>
{
public Task StartAsync(IDialogContext context)
{
PromptDialog.Choice(context, Resume, new List<string> { "yes", "no" },
"Found what you're looking for?");
return Task.CompletedTask;
}
private async Task Resume(IDialogContext context, IAwaitable<string> result)
{
var message = await result;
if (message == "yes")
{
context.Call(new SignupDialog(), GlobalContinueHandler.SignupDialogAfter);
}
else
{
context.Call(new FeedbackDialog(), GlobalContinueHandler.FeedbackDialogAfter);
}
}
}
I'm really not fan of this solution but for now it seems to work.

Resources