In this exercise you will learn how to add conversation abilities to the bot to guide the user to create a help desk ticket.
Inside this folder you will find a solution with the code that results from completing the steps in this exercise. You can use this solution as guidance if you need additional help as you work through this exercise.
The following software is required for completing this exercise:
- Visual Studio 2017 Community or higher
- The Bot Framework Emulator (make sure it's configured with the
en-US
Locale)
In this task you will modify the bot code to ask the user a sequence of questions before performing some action.
-
Open the solution you've obtained from the previous exercise. Alternatively, you can open the solution from the exercise1-EchoBot folder.
-
Open the Dialogs\RootDialog.cs file.
-
Add the following variables at the beginning of the
RootDialog
class. We will use them later to store the user answers.private string category; private string severity; private string description;
-
Replace the method
MessageReceivedAsync
with the following code.public async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> argument) { var message = await argument; await context.PostAsync("Hi! I’m the help desk bot and I can help you create a ticket."); PromptDialog.Text(context, this.DescriptionMessageReceivedAsync, "First, please briefly describe your problem to me."); } public async Task DescriptionMessageReceivedAsync(IDialogContext context, IAwaitable<string> argument) { this.description = await argument; await context.PostAsync($"Got it. Your problem is \"{this.description}\""); context.Done<object>(null); }
You will notice the Dialog implementation consist of a set of methods that are connected together using either the conversation flow control methods (provided by the
IDialogContext
interface) or some of thePromptDialog
helper methods which also use theIDialogContext
methods behind the scene to manage the conversation flow.When the conversation first starts, the dialog does not contain state, so the
Conversation.SendAsync
constructsRootDialog
and calls itsStartAsync
method. TheStartAsync
method callsIDialogContext.Wait
with the continuation delegate to specify the method that should be called when a new message is received (in this case is theMessageReceivedAsync
method).The Bot Framework SDK provides a set of built-in prompts to simplify collecting input from a user. The
MessageReceivedAsync
method waits for a message, which once received, posts a response greeting the user and callsPromptDialog.Text()
to prompt him to describe the problem.Also, the response is persisted in the dialog instance by the framework. Notice it was marked as
[Serializable]
. This is essential for storing temporary information in between the steps of the dialog. -
Run the solution in Visual Studio (click the Run button) and open the emulator. Type the bot URL as usual (
http://localhost:3979/api/messages
) and test the bot as show below.
In this task you are going to add more message handlers to the bot code to prompt for all the ticket details.
-
Stop the app and open the Dialogs\RootDialog.cs file.
-
Update the
DescriptionMessageReceivedAsync
to store the description the user entered and prompt the ticket's severity. The following code uses thePromptDialog.Choice
method which will give the user a set of choices to pick.public async Task DescriptionMessageReceivedAsync(IDialogContext context, IAwaitable<string> argument) { this.description = await argument; var severities = new string[] { "high", "normal", "low" }; PromptDialog.Choice(context, this.SeverityMessageReceivedAsync, severities, "Which is the severity of this problem?"); }
-
Next, add the
SeverityMessageReceivedAsync
method that receives the severity and prompts the user to enter the category using thePromptDialog.Text
method.public async Task SeverityMessageReceivedAsync(IDialogContext context, IAwaitable<string> argument) { this.severity = await argument; PromptDialog.Text(context, this.CategoryMessageReceivedAsync, "Which would be the category for this ticket (software, hardware, networking, security or other)?"); }
-
Now add the
CategoryMessageReceivedAsync
method which stores the category and prompt the user to confirm the ticket creation using thePromptDialog.Confirm
method.public async Task CategoryMessageReceivedAsync(IDialogContext context, IAwaitable<string> argument) { this.category = await argument; var text = $"Great! I'm going to create a \"{this.severity}\" severity ticket in the \"{this.category}\" category. " + $"The description I will use is \"{this.description}\". Can you please confirm that this information is correct?"; PromptDialog.Confirm(context, this.IssueConfirmedMessageReceivedAsync, text); }
NOTE: Notice that you can use Markdown syntax to create richer text messages. However it's important to note that not all channels support Markdown.
-
Add a method to handle the response from the confirmation message.
public async Task IssueConfirmedMessageReceivedAsync(IDialogContext context, IAwaitable<bool> argument) { var confirmed = await argument; if (confirmed) { await context.PostAsync("Awesome! Your ticked has been created."); } else { await context.PostAsync("Ok. The ticket was not created. You can start again if you want."); } context.Done<object>(null); }
-
Re-run the app and use the 'Start new conversation' button of the emulator . Test the new conversation.
NOTE: At this point if you talk to the bot again, the dialog will start over.
Now you have all the information for the ticket, however that information is discarded when the dialog ends. You will now add the code to create the ticket using an external API. For simplicity purposes, you will use a simple endpoint that saves the ticket into an in-memory array. In the real world, you would use any external API that is accessible from your bot's code.
NOTE: One important fact about bots to keep in mind is most bots you will build will be a front end to an existing API. Bots are simply apps, and they do not require artificial intelligence (AI), machine learning (ML), or natural language processing (NLP), to be considered a bot.
-
Stop the app. In the Controllers folder copy the TicketsController.cs from the assets folder of this hands-on lab. This will handle the POST request to the
/api/tickets
endpoint, add the ticket to an array and respond with the ticket id created. -
Add a new
Util
folder to your project. In the new folder, copy the TicketAPIClient.cs file which will call the ticket API from the bot. -
Update your
Web.Config
file by adding the key TicketsAPIBaseUrl under the appSettings section. This key will contain the Base URL where the Ticket API will run. In this exercise, it will be the same URL where the bot is running, but in a real world scenario it may be different URL.<add key="TicketsAPIBaseUrl" value="http://localhost:3979/" />
-
Open the Dialogs\RootDialog.cs file.
-
Add the
HelpDeskBot.Util
using statements.using HelpDeskBot.Util;
-
Replace the content of the
IssueConfirmedMessageReceivedAsync
method to make the call using the TicketAPIClient.public async Task IssueConfirmedMessageReceivedAsync(IDialogContext context, IAwaitable<bool> argument) { var confirmed = await argument; if (confirmed) { var api = new TicketAPIClient(); var ticketId = await api.PostTicketAsync(this.category, this.severity, this.description); if (ticketId != -1) { await context.PostAsync($"Awesome! Your ticked has been created with the number {ticketId}."); } else { await context.PostAsync("Ooops! Something went wrong while I was saving your ticket. Please try again later."); } } else { await context.PostAsync("Ok. The ticket was not created. You can start again if you want."); } context.Done<object>(null); }
-
Re-run the app and use the Start new conversation button of the emulator . Test the full conversation again to check that the ticket id is returned from the API.
In this task you will enhance the confirmation message that is shown to the user after the ticket using Adaptive Cards. Adaptive Cards are an open card exchange format enabling developers to exchange UI content in a common and consistent way. Their content can be specified as a JSON object. Content can then be rendered natively inside a host application (Bot Framework channels), automatically adapting to the look and feel of the host.
-
You will need to add the
Microsoft.AdaptiveCards
NuGet package. Right click on your project's References folder in the Solution Explorer and click Manage NuGet packages. Search for theMicrosoft.AdaptiveCards
and then click on the Install button. Or you can type in the Packager Manager ConsoleInstall-Package Microsoft.AdaptiveCards
. -
Open the Dialogs\RootDialog.cs file.
-
Add the
System.Collections.Generic
andAdaptiveCards
using statements.using System.Collections.Generic; using AdaptiveCards;
-
At the end of the file (inside the
RootDialog
class) add the following code that creates the Adaptive card:Anatomy of this example card,
- A header section containing the title with the ticketID
- A middle section containing a
ColumnSet
with two columns: one for aFactSet
with the Severity and Category and another with an icon - A last section that includes a description block with the ticket description
private AdaptiveCard CreateCard(int ticketId, string category, string severity, string description) { AdaptiveCard card = new AdaptiveCard(); var headerBlock = new TextBlock() { Text = $"Ticket #{ticketId}", Weight = TextWeight.Bolder, Size = TextSize.Large, Speak = $"<s>You've created a new Ticket #{ticketId}</s><s>We will contact you soon.</s>" }; var columnsBlock = new ColumnSet() { Separation = SeparationStyle.Strong, Columns = new List<Column> { new Column { Size = "1", Items = new List<CardElement> { new FactSet { Facts = new List<AdaptiveCards.Fact> { new AdaptiveCards.Fact("Severity:", severity), new AdaptiveCards.Fact("Category:", category), } } } }, new Column { Size = "auto", Items = new List<CardElement> { new Image { Url = "https://raw.githubusercontent.com/GeekTrainer/help-desk-bot-lab/master/assets/botimages/head-smiling-medium.png", Size = ImageSize.Small, HorizontalAlignment = HorizontalAlignment.Right } } } } }; var descriptionBlock = new TextBlock { Text = description, Wrap = true }; card.Body.Add(headerBlock); card.Body.Add(columnsBlock); card.Body.Add(descriptionBlock); return card; }
-
Update the
IssueConfirmedMessageReceivedAsync
method to call this method when the ticket was successfully created.public async Task IssueConfirmedMessageReceivedAsync(IDialogContext context, IAwaitable<bool> argument) { var confirmed = await argument; if (confirmed) { var api = new TicketAPIClient(); var ticketId = await api.PostTicketAsync(this.category, this.severity, this.description); if (ticketId != -1) { var message = context.MakeMessage(); message.Attachments = new List<Attachment> { new Attachment { ContentType = "application/vnd.microsoft.card.adaptive", Content = CreateCard(ticketId, this.category, this.severity, this.description) } }; await context.PostAsync(message); } else { await context.PostAsync("Ooops! Something went wrong while I was saving your ticket. Please try again later."); } } else { await context.PostAsync("Ok. The ticket was not created. You can start again if you want."); } context.Done<object>(null); }
-
Re-run the app and use the Start new conversation button of the emulator . Test the new conversation. You should see the Adaptive Card as follows.
If you want to continue working on your own you can try with these tasks: