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!