how to close a QnAMaker dialog? - c#

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:

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 exit from qnadialog and to continue with other luis intent

i have created a bot using luis and qnamaker dialog. in my questions is a part of the code of LuisDialog.cs . During the conversations if user make a questions that is part of qna intent ( the bot jumb to QnADialog) , but i want to pass to other intent when user make another questions to the bot .
LuisDialog.cs here is my code updated with other intent . I want to quit from qnadialog when user type a questions that correspond to test intent for example
using Microsoft.Bot.Builder.Dialogs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.Bot.Builder.Luis;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Luis.Models;
using Microsoft.Bot.Connector;
using MultiDialogsBot.Dialogs;
using System.Threading;
namespace MultiDialogsBot
{
[LuisModel("xxxxxxx", "yyyyyyyyyyy")]
[Serializable]
public class LuisDialog : LuisDialog<object>
{
private object activity;
public async Task StartAsync(IDialogContext context)
{
context.Wait(MessageReceived);
}
[LuisIntent("None")]
[LuisIntent("")]
public async Task None(IDialogContext context, LuisResult result)
{
string message = $"Désolé je n'ai pas compris '{result.Query}'. Veuillez formuler votre question";
await context.PostAsync(message);
context.Wait(this.MessageReceived);
}
[LuisIntent("test")]
public async Task test(IDialogContext context, LuisResult result)
{
await context.PostAsync("nous testons");
context.Wait(MessageReceived);
}
[LuisIntent("qna")]
public async Task qna(IDialogContext context, IAwaitable<IMessageActivity> activity, LuisResult result)
{
var msg = await activity;
// await context.Forward(new QnADialog(), ResumeAfterOptionDialog, msg, CancellationToken.None);
context.Call(new QnADialog(), this.ResumeAfterOptionDialog);
}
public async Task ResumeAfterOptionDialog(IDialogContext context, IAwaitable<object> result)
{
var messageHandled = await result;
if (messageHandled != null)
{
await context.PostAsync("Désolé je n'ai pas compris");
context.Wait(MessageReceived);
}
}
If you want to treat the user input inside your QnA database first, your should change your logic described in your question and set an overridden QnADialog that will get your user input first, and when there is no reply, call a LuisDialog to try to handle the case with 1 or several interesting intents.
You can check here how the QnAMakerDialog is made. You will see that you probably need to rewrite the class to change the MessageReceivedAsync method to avoid the reply from the QnAMakerDialog here:
if (sendDefaultMessageAndWait)
{
// The following line should be removed if you don't want that the QnADialog replies if no answer found
await context.PostAsync(qnaMakerResults.ServiceCfg.DefaultMessage);
await this.DefaultWaitNextMessageAsync(context, message, qnaMakerResults);
}
Your QnAOverriddenDialog must be called from where your LuisDialog was called previously (from your MessageController I guess, as I don't have the details of your implementation).
And your LuisDialog will look like the following:
[LuisModel("xxxxxxx", "yyyyyyyyyyy")]
[Serializable]
public class LuisDialog : LuisDialog<object>
{
public async Task StartAsync(IDialogContext context)
{
context.Wait(MessageReceived);
}
[LuisIntent("None")]
[LuisIntent("")]
public async Task None(IDialogContext context, LuisResult result)
{
string message = $"Désolé je n'ai pas compris '{result.Query}'. Veuillez formuler votre question";
await context.PostAsync(message);
context.Wait(this.MessageReceived);
}
[LuisIntent("test")]
public async Task test(IDialogContext context, LuisResult result)
{
await context.PostAsync("nous testons");
context.Wait(MessageReceived);
}
[LuisIntent("yourOtherIntent1")]
public async Task OtherIntent1(IDialogContext context, LuisResult result)
{
await context.PostAsync("fallback 1");
context.Wait(MessageReceived);
}
[LuisIntent("yourOtherIntent2")]
public async Task OtherIntent1(IDialogContext context, LuisResult result)
{
await context.PostAsync("fallback 2");
context.Wait(MessageReceived);
}
public async Task ResumeAfterOptionDialog(IDialogContext context, IAwaitable<object> result)
{
var messageHandled = await result;
if (messageHandled != null)
{
await context.PostAsync("Désolé je n'ai pas compris");
context.Wait(MessageReceived);
}
}
}
Try:
[LuisIntent("qna")]
public async Task qna(IDialogContext context, IAwaitable<IMessageActivity> activity, LuisResult result)
{
var msg = await activity;
await context.Forward(new QnADialog(), ResumeAfterQnA, context.Activity, CancellationToken.None);
await this.ShowLuisResult(context, result);
}
private async Task ResumeAfterQnA(IDialogContext context, IAwaitable<object> result)
{
context.Done<object>(null);
}
private async Task ShowLuisResult(IDialogContext context, LuisResult result)
{
await context.PostAsync($"You have reached {result.Intents[0].Intent}. You said: {result.Query}");
context.Wait(MessageReceived);
}

Stack is empty error when calling context.Done in child dialog

I have a LuisDialog which makes a forward to another LuisDialog in the "None" intent as some kind of fallback:
[LuisIntent("None")]
public async Task None(IDialogContext context, IAwaitable<IMessageActivity> message, LuisResult result)
{
var luisService = new LuisService(new LuisModelAttribute("XXX", "XXX"));
await context.Forward(new MyChildDialog(luisService), null, await message);
context.Wait(MessageReceived);
}
The method executed in MyChildDialog is like this:
[LuisIntent("myLuisIntent")]
public async Task MyLuisIntent(IDialogContext context, LuisResult result)
{
await context.PostAsync("Hi!");
context.Done(0);
}
When the context.Done() is executed, emulator shows an error: "Stack is empty". But, how can it be empty if forward adds the dialog to the stack?
Make sure you have a handler for what to do when the MyChildDialog is fininshed
[LuisIntent("None")]
public async Task None(IDialogContext context, IAwaitable<IMessageActivity> message, LuisResult result)
{
var luisService = new LuisService(new LuisModelAttribute("XXX", "XXX"));
await context.Forward(new MyChildDialog(luisService), WaitForMessageResume, await message);
context.Wait(MessageReceived);
}
private Task WaitForMessageResume(IDialogContext context, IAwaitable<object> result)
{
context.Wait(MessageReceived);
return Task.CompletedTask;
}

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

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.

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);
}
}

Categories