Why context.Wait in StartAsync didn't stop the dialog - c#

I'm sure is stupid question, but I will more stupid if I didn't ask
I have the following code
[Serializable]
public class RootDialog : IDialog<object>
{
public async Task StartAsync(IDialogContext context)
{
await context.PostAsync("RootDialog !");
context.Wait(MessageReceivedAsync);
}
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
var activity = await result as Activity;
// calculate something for us to return
int length = (activity.Text ?? string.Empty).Length;
// return our reply to the user
await context.PostAsync($"You sent {activity.Text} which was {length} characters");
context.Wait(MessageReceivedAsync);
}
}
I understant the statement context.Wait is use to wait the next message of the user.
But if I launch my RootDialog my statement
await context.PostAsync("RootDialog !");
Is executed and just after my statement
await context.PostAsync($"You sent {activity.Text} which was {length} characters");
is exectuted too.
Why ?
Why I didn't a pause in my programm with the statement
context.Wait(MessageReceivedAsync);
in the StartAsync function like I have in the MessageReceivedAsync function ?

There is some description of IDialogContext.Wait in the docs here: https://learn.microsoft.com/en-us/bot-framework/dotnet/bot-builder-dotnet-dialogs#implementation-details
The StartAsync method calls IDialogContext.Wait with the continuation
delegate to specify the method that should be called when a new
message is received (MessageReceivedAsync).
A bot built using the BotBuilder SDK is restful and stateless, in the sense that the server itself doesn't track or store session information. "context.Wait(method)" doesn't mean "freeze the code here", but rather: resume at this method in the dialog the next time a message comes from the user. The method to call next is actually serialized with the dialog and stored in the State Service (see here: Manage state data The last context.Wait(methodname) will be called the next time the user sends a message in the context of the same conversation.
An example might be useful:
[Serializable]
public class RootDialog : IDialog<object>
{
public Task StartAsync(IDialogContext context)
{
context.Wait(MessageReceivedAsync);
return Task.CompletedTask;
}
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
var activity = await result as Activity;
// calculate something for us to return
int length = (activity.Text ?? string.Empty).Length;
// return our reply to the user
await context.PostAsync($"You sent {activity.Text} which was {length} characters");
context.Wait(MessageReceivedAsync2);
}
private async Task MessageReceivedAsync2(IDialogContext context, IAwaitable<object> result)
{
await context.PostAsync($"Second MessageReceived");
context.Wait(MessageReceivedAsync);
}
}
The code above will switch back and forth between MessageReceivedAsync and MessageReceivedAsync2 for every message sent by the user.

Related

C# Microsoft Bot Framework with luis result directing to QNA Maker and graph api

I made a Bot using Microsoft bot framework and made use of Luis for matching intents. Some of the intents directs it to QNA and and some other intents directs it to graph api.
My Question is what is the best approach for identifying whether it should go to qna for searching the related intents in qna or whether it should go to graph api for fetching results.
As of now i did it using multiple Luis Intents for matching the correct intent and then redirect it according to the intent functionality needed(whether to direct it to qna dialog or graph api dialog).
`
[LuisModel("model id", "key")]
[Serializable]
public class RootDialog : DispatchDialog
{
//this intent directs to graph api dialog
[LuisIntent(DialogMatches.GraphApiIntent)]
public async Task RunGraphapiIntent(IDialogContext context, IActivity activity)
{
UserMessage = (activity as IMessageActivity).Text;
await context.Forward(new GraphApiDailog(), EndDialog, context.Activity, CancellationToken.None);
}
//This intent directs to qna dialog
[LuisIntent(DialogMatches.BlogMatch)]
public async Task RunBlogDialogQna(IDialogContext context, LuisResult result)
{
var userQuestion = (context.Activity as Activity).Text;
(context.Activity as Activity).Text = DialogMatches.BlogMatch;
await context.Forward(new BasicQnAMakerDialog(), this.EndDialog, context.Activity, CancellationToken.None);
}
`
But this approach requires me to match every intents using [LuisIntent("intentstring")].. Since i can have 50 or 100's of intent, this is not practical to write 50 functions for 50 intents.
I found out a way to call an api for fetching intent from utterances in Quickstart: Analyze text using C#
it makes use of "https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/df67dcdb-c37d-46af-88e1-8b97951ca1c2?subscription-key=&q=turn on the bedroom light"
api for fetching intent.
Again My Question is how will i know whether i should redirect it to QnaDialog or Graph Api Dialog for fetching data using the intent result that i got?
Thanks in advance
If you want things to scale you will be better off by writing your own Nlp service that calls the Luis API to detect the intent. I think the best way to handle dialog redirection by intent is to make something like an IntentDetectorDialog whose only job is to analyze the user utterance and forwarding to the dialog that corresponds with the detected intent.
Here's a neat approach I've been using for a while:
public abstract class BaseDialog : IDialog<BaseResult>
{
public bool DialogForwarded { get; protected set; }
public async Task StartAsync(IDialogContext context)
{
context.Wait(OnMessageReceivedAsync);
}
public async Task OnMessageReceivedAsync(
IDialogContext context,
IAwaitable<IMessageActivity> result)
{
var message = await result;
var dialogFinished = await HandleMessageAsync(context, message);
if (DialogForwarded) return;
if (!dialogFinished)
{
context.Wait(OnMessageReceivedAsync);
}
else
{
context.Done(new DefaultDialogResult());
}
}
protected abstract Task<bool> HandleMessageAsync(IDialogContext context, IMessageActivity message);
protected async Task ForwardToDialog(IDialogContext context,
IMessageActivity message, BaseDialog dialog)
{
DialogForwarded = true;
await context.Forward(dialog, (dialogContext, result) =>
{
// Dialog resume callback
// this method gets called when the child dialog calls context.Done()
DialogForwarded = false;
return Task.CompletedTask;
}, message);
}
}
The base dialog, parent of all other dialogs, will handle the general flow of the dialog. If the dialog has not yet finished, it will notify the bot framework by calling context.Wait otherwise it will end the dialog with context.Done. It will also force all the child dialogs to implement the method HandleMessageAsync which returns a bool indicating whether the dialog has finished or not. And also exposes a reusable method ForwardToDialog that our IntentDetectorDialog will use to handle intent redirection.
public class IntentDetectorDialog : BaseDialog
{
private readonly INlpService _nlpService;
public IntentDetectorDialog(INlpService nlpService)
{
_nlpService = nlpService;
}
protected override async Task<bool> HandleMessageAsync(IDialogContext context, IMessageActivity message)
{
var intentName = await _nlpService.AnalyzeAsync(message.Text);
switch (intentName)
{
case "GoToQnaDialog":
await ForwardToDialog(context, message, new QnaDialog());
break;
case "GoToGraphDialog":
await ForwardToDialog(context, message, new GraphDialog());
break;
}
return false;
}
}
That is the IntentRedetectorDialog: son of BaseDialog whose only job is to detect the intent and forward to the corresponding dialog. To make things more scalable you could implement a IntentDialogFactory which can build dialogs based on the detected intent.
public class QnaDialog : BaseDialog
{
protected override async Task<bool> HandleMessageAsync(IDialogContext context, IMessageActivity message)
{
if (message.Text == "My name is Javier")
{
await context.PostAsync("What a cool name!");
// question was answered -> end the dialog
return true;
}
else
{
await context.PostAsync("What is your name?");
// wait for the user response
return false;
}
}
}
And finally we have our QnaDialog: also son of BaseDialog whose only job is to ask for the user's name and wait for the response.
Edit
Based on your comments, in your NlpService you can have:
public class NlpServiceDispatcher : INlpService
{
public async Task<NlpResult> AnalyzeAsync(string utterance)
{
var qnaResult = await _qnaMakerService.AnalyzeAsync(utterance);
var luisResult = await _luisService.AnalyzeAsync(utterance);
if (qnaResult.ConfidenceThreshold > luisResult.ConfidenceThreshold)
{
return qnaResult;
}
else
{
return luisResult;
}
}
}
Then change the IntentDetectorDialog to:
public class UtteranceAnalyzerDialog : BaseDialog
{
private readonly INlpService _nlpService;
public UtteranceAnalyzerDialog(INlpService nlpService)
{
_nlpService = nlpService;
}
protected override async Task<bool> HandleMessageAsync(IDialogContext context, IMessageActivity message)
{
var nlpResult = await _nlpService.AnalyzeAsync(message.Text);
switch (nlpResult)
{
case QnaMakerResult qnaResult:
await context.PostAsync(qnaResult.Answer);
return true;
case LuisResult luisResult:
var dialog = _dialogFactory.BuildDialogByIntentName(luisResult.IntentName);
await ForwardToDialog(context, message, dialog);
break;
}
return false;
}
}
And there you have it! You don't need to repeat utterances in Luis and QnaMaker, you can just use both and set your strategy based on the more confident result!

how to close a QnAMaker dialog?

I am trying to closing a QnAMaker dialog so that user can go back to the Luis dialog and use it again.
here is my code that i use
in luisdialog.cs:
[LuisIntent("FAQ")]
public async Task FAQ(IDialogContext context, LuisResult result)
{
await context.PostAsync("FAQ");
var userQuestion = (context.Activity as Activity).Text;
await context.Forward(new QnADialog(), ResumeAfterQnA, context.Activity, CancellationToken.None);
//await Task.Run(() => context.Call(new QnADialog(), Callback));
}
private async Task ResumeAfterQnA(IDialogContext context, IAwaitable<object> result)
{
context.Done<object>(null);
}
While here is the QnA dialog:
[Serializable]
[QnAMakerService("endpoint", "knowledge base id", "subscription key")]
public class QnADialog : QnAMakerDialog<object>
{
}
I tried to override the start async method so that it will quit the dialog by using context.done(0) if the user type "done" but the QnA maker doesn't start at all which is confusing.
Also why is that by calling the luis intent using "FAQ" it also tried to go to the knowledge base without the user typing it again is it possible to fix that ?
I am trying to closing a QnAMaker dialog so that user can go back to the Luis dialog and use it again.
You can try to override the DefaultMatchHandler and call context.Done to close QnAMaker dialog and pass control back to the parent dialog. The following modified code snippet work for me, please refer to it.
In LuisDialog:
[LuisIntent("FAQ")]
public async Task HelpIntent(IDialogContext context, LuisResult result)
{
await context.PostAsync("FAQ");
await context.Forward(new QnADialog(), ResumeAfterQnA, context.Activity, CancellationToken.None);
}
private async Task ResumeAfterQnA(IDialogContext context, IAwaitable<object> result)
{
//context.Done<object>(null);
context.Wait(MessageReceived);
}
In QnADialog:
public override async Task DefaultMatchHandler(IDialogContext context, string originalQueryText, QnAMakerResult result)
{
await context.PostAsync($"I found {result.Answers.Length} answer(s) that might help...{result.Answers.First().Answer}.");
context.Done(true);
}
Test result:

Bot Framework's context.Wait() not waiting for user input

I am starting to develop a simple Bot, evolving from the Echo bot in the documentation. And I've hit an issue quickly.
I have these three methods on my RootDialog:
public async Task StartAsync(IDialogContext context)
{
await context.PostAsync("Olá! Eu sou um bot!");
await context.PostAsync("Qual é o teu nome?");
context.Wait(NameReceivedAsync);
}
private async Task NameReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
var activity = await result as Activity;
userName = activity.Text;
await context.PostAsync($"Olá {userName}. Podes dizer alguma coisa e eu vou repetir.");
context.Wait(MessageReceivedAsync);
}
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
var activity = await result as Activity;
// calculate something for us to return
int length = (activity.Text ?? string.Empty).Length;
// return our reply to the user
await context.PostAsync($"Tu disseste { activity.Text}, que tem {length} caracteres");
context.Wait(MessageReceivedAsync);
}
And my MessageController Post method is like this:
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
await Conversation.SendAsync(activity, () => new Dialogs.RootDialog());
return new HttpResponseMessage(System.Net.HttpStatusCode.Accepted);
}
The idea is for the bot to send two messages right away, wait for input with userName from the user, send another message, and then go to MessageReceivedAsync where he'll start his echo loop. The problem is the bot is not waiting for the inputs, only stopping at the end of the MessageReceivedAsync, where he'll start the echo.
I can't seem to understand why this happens, since from what I've seen the context.Wait(...) should make the Bot wait for input, which is not happening. I'm testing it with the Bot Framework Channel Emulator on Chrome, if that makes any difference.
context.Wait(method) is a little confusing because it actually sets up a “continuation delegate to specify the method that should be called when a new message is received” from: https://learn.microsoft.com/en-us/bot-framework/dotnet/bot-builder-dotnet-dialogs#implementation-details However, the context.Wait(method) in .StartAsync will execute the "method" immediately, since the dialog is being run for the first time.
If you change your code to something like the following, it should work as you expect:
[Serializable]
public class RootDialogTest : IDialog<object>
{
public async Task StartAsync(IDialogContext context)
{
context.Wait(NameReceivedAsync);
}
private async Task NameReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
var activity = await result as Activity;
if (!context.UserData.ContainsKey("askedname"))
{
await context.PostAsync("Olá! Eu sou um bot!");
await context.PostAsync("Qual é o teu nome?");
context.UserData.SetValue("askedname", true);
context.Wait(NameReceivedAsync);
}
else
{
var userName = activity.Text;
await context.PostAsync($"Olá {userName}. Podes dizer alguma coisa e eu vou repetir.");
context.Wait(MessageReceivedAsync);
}
}
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
var activity = await result as Activity;
// calculate something for us to return
int length = (activity.Text ?? string.Empty).Length;
// return our reply to the user
await context.PostAsync($"Tu disseste { activity.Text}, que tem {length} caracteres");
context.Wait(MessageReceivedAsync);
}
}
Edit: another option, that involves fewer changes to your current dialog:
[Serializable]
public class RootDialogTest : IDialog<object>
{
public async Task StartAsync(IDialogContext context)
{
await context.PostAsync("Olá! Eu sou um bot!");
await context.PostAsync("Qual é o teu nome?");
context.Wait(SetupMethodWait);
}
private async Task SetupMethodWait(IDialogContext context, IAwaitable<object> result)
{
context.Wait(NameReceivedAsync);
}
private async Task NameReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
var activity = await result as Activity;
var userName = activity.Text;
await context.PostAsync($"Olá {userName}. Podes dizer alguma coisa e eu vou repetir.");
context.Wait(MessageReceivedAsync);
}
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
var activity = await result as Activity;
// calculate something for us to return
int length = (activity.Text ?? string.Empty).Length;
// return our reply to the user
await context.PostAsync($"Tu disseste { activity.Text}, que tem {length} caracteres");
context.Wait(MessageReceivedAsync);
}
}
From what I can tell from developing with the Bot Framework, the Conversation.SendAsync() works like a context.Forward() forwarding the message onto the RootDialog().
I suggest directing the user to a separate dialog using such as AskNameDialog() using context.Call() and process your code there.
Have a look at the samples the BotBuilder provides
https://github.com/Microsoft/BotBuilder-Samples they should help you in understanding.

Bot Framework Forward Type Arguments Error

I am getting this following error
when trying to use the MS Bot Framework Example to call a different dialog. This is my code:
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
namespace ReadMeBot.Dialogs
{
[Serializable]
public class RootDialog : IDialog<object>
{
public Task StartAsync(IDialogContext context)
{
context.Wait(MessageReceivedAsync);
return Task.CompletedTask;
}
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
var activity = await result as Activity;
if (activity != null && activity.Text.ToLower().Contains("what is"))
{
await
context.Forward(new InternetSearchDialog(), this.ResumeAfterInternetSearchDialog, activity, CancellationToken.None);
}
// calculate something for us to return
int length = (activity.Text ?? string.Empty).Length;
// return our reply to the user
await context.PostAsync($"You sent {activity.Text} which was {length} characters. Thank you!");
context.Wait(MessageReceivedAsync);
}
private async Task ResumeAfterInternetSearchDialog(IDialogContext context, IAwaitable<string> result)
{
}
}
}
How can I solve this? I googled around and nobody seems to have this issue. What am I doing wrong?
Since you are forwarding to another dialog, you don't need to wait in this dialog. You'll want to call context.Wait in the resume though.
Things should work as expected if you change your code to something like this:
[Serializable]
public class RootDialog : IDialog<object>
{
public Task StartAsync(IDialogContext context)
{
context.Wait(MessageReceivedAsync);
return Task.CompletedTask;
}
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
var activity = await result as Activity;
if (activity != null && activity.Text.ToLower().Contains("what is"))
{
await
context.Forward(new InternetSearchDialog(), this.ResumeAfterInternetSearchDialog, activity, CancellationToken.None);
}
else
{
// calculate something for us to return
int length = (activity.Text ?? string.Empty).Length;
// return our reply to the user
await context.PostAsync($"You sent {activity.Text} which was {length} characters. Thank you!");
context.Wait(MessageReceivedAsync);
}
}
private async Task ResumeAfterInternetSearchDialog(IDialogContext context, IAwaitable<string> result)
{
context.Wait(MessageReceivedAsync);
}
}

Bot framework - PromptDialog.Choice has duplicate replies

In Bot Framework, while using C#, I have a Dialog and use PromptDialog.Choice to let users select a question they are interested in.
But when it runs, I got duplicate replies, as seen in the following picture:
public async Task StartAsync(IDialogContext context)
{
this.ShowOptions(context);
}
private void ShowOptions(IDialogContext context)
{
PromptDialog.Choice(
context,
this.OnOptionSelected,
new List() { ImageOption, ToolOption },
"Please select one of the following category.",
"Not a valid option",
3);
}
private async Task OnOptionSelected(IDialogContext context, IAwaitable result)
{
string optionSelected = await result;
switch (optionSelected)
{
case ImageOption:
context.Call(new ImgRelated(), this.ResumeAfterOptionDialog);
break;
case ToolOption:
context.Call(new ToolPBmDailog(), this.ResumeAfterOptionDialog);
break;
}
}
Anyone have an idea?
ShowOptions needs to be an async method, and it needs to be called with context.Wait(this.ShowOptions) in StartAsync instead of just straight up calling it.
public async Task StartAsync(IDialogContext context)
{
context.Wait(this.ShowOptions);
}
public async virtual Task ShowOptions(IDialogContext context, IAwaitable<IMessageActivity> result)
{
var message = await result;
PromptDialog.Choice<string>(
context,
this.OnOptionSelected,
new List() { ImageOption, ToolOption },
"Please select one of the following category.",
"Not a valid option",
3);
}
You have to make the OnOptionSelected method so as it waits for an input from user before executing it. Also, make the ShowOptions method as async, otherwise you might get an exception, because right now, your ShowOptions method returns void to the StartAsync method. The IAwaitable<IMessageActivity> waits for a response from user. Try the following and see if it works for you.
public async Task StartAsync(IDialogContext context)
{
await this.ShowOptions(context);
}
private async Task ShowOptions(IDialogContext context)
{
PromptDialog.Choice(context, this.OnOptionSelected,
new List() { ImageOption, ToolOption },
"Please select one of the following category.",
"Not a valid option", 3);
}
private async Task OnOptionSelected(IDialogContext context, IAwaitable<IMessageActivity> result)
{
string optionSelected = await result;
switch (optionSelected)
{
case ImageOption:
context.Call(new ImgRelated(), this.ResumeAfterOptionDialog);
break;
case ToolOption:
context.Call(new ToolPBmDailog(), this.ResumeAfterOptionDialog);
break;
}
}

Categories