A sample that shows how to use the Dialog system in the Bot Builder for .NET SDK to manage a bot's conversation with the user.
In this sample, we'll use the Dialog system to ask the user their name and age, and reply with their responses.
To run this sample, install the prerequisites by following the steps in the Create a bot with the Bot Builder SDK for .NET section of the documentation.
The Bot Builder for .NET SDK provides the Dialogs namespace to allows developers to easily model a conversation in the bots they develop. Dialogs are classes that implement the IDialog interface and are used to manage the messages sent and received from the conversation. Dialogs can be simple classes that prompt the user for information and validate the response, or they can be more complex conversation flows composed of other dialogs.
When a dialog is called, it's passed an instance of the IDialogContext
interface. This context object manages all dialogs in the conversation as a stack, by implementing the IDialogStack
interface. In this dialog stack, the dialog on the top of the stack is the active dialog and has access to the dialog context. The active dialog can use the dialog context to:
- Post messages to the conversation.
- Wait for messages from the conversation, suspending the bot until the message arrives.
- Call children dialogs, pushing them onto the stack and making them the active dialog.
- Mark themselves as complete, popping them from the stack, and passing control back to the parent dialog.
Let's look at how these concepts are used to manage a simple conversation in a bot.
When managing a conversation using the Dialog system, the conversation is rooted in a single dialog, often called the Root Dialog. The Root Dialog is the first dialog added to the dialog stack for the conversation. All other dialogs in the conversation are called from the Root Dialog, either directly or indirectly (in the case of a child dialog calling another dialog) and return to the Root Dialog (either directly or indirectly). The Root Dialog doesn't complete until your bot process ends.
To create the RootDialog
class, create a class that is marked with the [Serializable]
attribute (so the dialog can be serialized to state) and implement the IDialog
interface.
To implement the IDialog
interface, you implement the StartAsync()
methond. StartAsync()
is called when the dialog becomes active. The method is passed the IDialogContext
object, used to manage the conversation.
To wait for a message from the conversation, call context.Wait()
and pass it the method you called when the message is received. When MessageReceivedAsync()
is called, it's passed the dialog context and an IAwaitable
of type IMessageActivity
. To get the message, await the result.
[Serializable]
public class RootDialog : IDialog<object>
{
public async Task StartAsync(IDialogContext context)
{
context.Wait(this.MessageReceivedAsync);
}
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
{
var message = await result;
}
}
The RootDialog
is added to the conversation in the MessageController
class via the Post()
method. In the Post()
method, the call to Conversation.SendAsync()
creates an instance of the RootDialog
, adds it to the dialog stack to make it the active dialog, calling the RootDialog.StartAsync()
, passing the message.
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
if (activity.Type == ActivityTypes.Message)
{
await Conversation.SendAsync(activity, () => new RootDialog());
}
else
{
HandleSystemMessage(activity);
}
var response = Request.CreateResponse(HttpStatusCode.OK);
return response;
}
At this point, you have a RootDialog
that's added to the conversation and able to interact with the conversation via IDialogContext
. Next, you can start creating other dialogs to call in order to manage the conversation with the user.
The NameDialog
class is used to ask for the user's name. We'll create the NameDialog
class just like the RootDialog
above, but we'll implement IDialog<string>
to designate that the dialog returns with a string value when done.
We'll use context.PostAsync()
to post a message to the conversation ("What's your name?")and wait for a response by calling context.Wait()
. MessageReceivedAsync()
is called when the message is received from the user. Note that our bot stops and waits until that message is received.
When MessageReceivedAsync()
is called, we validate the message to be a valid name by making sure the message has text (vs. an image as an attachment) and that the text isn't empty. If the message is a valid name, our dialog has completed successfully and calls context.Done()
to pass the name as a string back to the calling dialog.
If the message isn't a valid name, we'll reprompt the user and wait for a response. Note that we're calling MessageReceivedAsnc()
recursively until we get a valid response or after 3 attempts. After 3 attempts, we let the calling dialog know this dialog failed by calling context.Fail()
and pass an exception that describes the issue.
Note: All dialogs should limit the number of retries they perform to avoid the bot getting stuck when a user doesn't know how to respond to a prompt.
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
[Serializable]
public class NameDialog : IDialog<string>
{
private int attempts = 3;
public async Task StartAsync(IDialogContext context)
{
await context.PostAsync("What is your name?");
context.Wait(this.MessageReceivedAsync);
}
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
{
var message = await result;
if ((message.Text != null) && (message.Text.Trim().Length > 0))
{
context.Done(message.Text);
}
else
{
--attempts;
if (attempts > 0)
{
await context.PostAsync("I'm sorry, I don't understand your reply. What is your name (e.g. 'Bill', 'Melinda')?");
context.Wait(this.MessageReceivedAsync);
}
else
{
context.Fail(new TooManyAttemptsException("Message was not a string or was an empty string."));
}
}
}
}
}
The AgeDialog
works the same way, but validates the reply to be a valid age and implements IDialog<int>
to return an integer to the calling dialog.
To manage the conversation, RootDialog
calls the NameDialog
and AgeDialog
dialogs to get the user's name and age and posts the results to the conversation.
In SendWelcomeMessageAsync()
, a welcome message is posted to the conversation and the NameDialog
is added to the dialog stack via a call to context.Call()
. NameDialogResumeAfter()
is called when NameDialog
completes successfully (calling context.Done()
) or fails (calling context.Fail()
).
If NameDialog
completed by calling context.Done()
, the name is returned as a string and the AgeDialog
is called. If NameDialog
completed by calling context.Fail()
, the exception is caught and the RootDialog
starts over by calling SendWelcomeMessageAsync()
.
private async Task SendWelcomeMessageAsync(IDialogContext context)
{
await context.PostAsync("Hi, I'm the Basic Multi Dialog bot. Let's get started.");
context.Call(new NameDialog(), this.NameDialogResumeAfter);
}
private async Task NameDialogResumeAfter(IDialogContext context, IAwaitable<string> result)
{
try
{
this.name = await result;
context.Call(new AgeDialog(this.name), this.AgeDialogResumeAfter);
}
catch (TooManyAttemptsException)
{
await context.PostAsync("I'm sorry, I'm having issues understanding you. Let's try again.");
await this.SendWelcomeMessageAsync(context);
}
}
When the AgeDialog
completes, AgeDialogResumeAfter
is called. If AgeDialog
completed by calling context.Done()
, the age is returned as an integer and the result of both dialogs is posted on the conversation. If AgeDialog
completed by calling context.Fail()
, the exception is caught and handled with a message to the user.
Note: Under either path, SendWelcomeMessageAsync()
is called, starting the RootDialog
process all over again. This is expected for the RootDialog
. The RootDialog is the root of all conversation, so it never ends until the bot process ends. In a real world bot, you'll add logic to manage this loop to make the conversation more engaging.
private async Task AgeDialogResumeAfter(IDialogContext context, IAwaitable<int> result)
{
try
{
this.age = await result;
await context.PostAsync($"Your name is { name } and your age is { age }.");
}
catch (TooManyAttemptsException)
{
await context.PostAsync("I'm sorry, I'm having issues understanding you. Let's try again.");
}
finally
{
await this.SendWelcomeMessageAsync(context);
}
}
Here's what the conversation looks like in the Bot Framework Emulator when supplying a valid name and age.
And here's what the convesation looks like when providing invalid responses to the AgeDialog
.
For more information on managing the conversation using Dialogs, check out the following resources: