1/11/2019 Webmaster
Creating A Bot Using ChoiceFlow for Bot Builder v4 .NET SDK
ChoiceFlow is part of the Bot Builder Community Extensions project which contains various pieces of middleware, recognizers and other components for use with the Bot Builder .NET SDK v4.
This middleware component allows you to provide the user with a series of guided choice prompts, (defined in a JSON file, or as a collection of ChoiceFlowItem objects), similar to when calling a telephone line with a series of automated options.
To demonstrate this, we will create a Bot, that uses the ChoiceFlow component, that will search Help Desk Tickets in the popular open source Help Desk application ADefHelpDesk.com.
ADefHelpDesk has an API that provides the following features:
- Requires callers to first obtain a JSON Web Token (JWT) (also called a Bearer Token), using a combination of a UserName, Password, and Application GUID. The token is then passed in the header of the subsequent requests to the other methods to authenticate the requests.
- Allows external caller to search for Help Desk Tickets (and perform all other functionality).
When a user interacts with the Bot, they will be presented with questions, and depending on the method they are using to interact with the Bot, a series of cards will display the acceptable choices. Selecting a card, or entering the value in the chat input, will make the selection and move through the guided prompts.
The user will provide a response for the Ticket type, priority, and status to search for.
They will then be presented with the top 5 matching Tickets to their search.
Requirements
- Visual Studio 2017 (or higher) with the following workloads:
- ASP.NET and web development
- Azure development
- .NET Core cross-platform development
- Bot Builder V4 SDK Template for Visual Studio
- Bot Framework Emulator (download the latest version even if it is “Alpha”)
- ADefHelpDesk (download ADefHelpDesk from the site at: http://ADefHelpDesk.com)
Install ADefHelpDesk
You can download and install ADefHelpDesk from the site at: http://ADefHelpDesk.com.
After you have the site running, follow the directions at the link: API Security (Swagger Rest API) on the documentation page (that can be found at this link), to create an account that can call the API.
You will need to copy the information on the Connection Information tab, for the API account, to use later.
In the ADefHelpDesk site there is a Swagger page (to learn about Swagger, see: https://swagger.io/), that documents the REST based API endpoints at: http://{your default web address}/swagger/ (for example: http://adefhelpdesk.azurewebsites.net/swagger/).
You will also want to create a few Help Desk Tickets, of various Statuses and Assignments, so that you have something to search for.
Create The Application
Open Visual Studio and select File, then New, then Project.
In the New Project dialog, select the Bot Builder Template, name the project ChoiceFlowBot and click OK.
The project will be created.
Hit F5 to run the project.
The web browser will open.
Connect Using The Bot Framework Emulator
Open the Bot Framework Emulator.
Select Open Bot.
Select the .Bot file that is in the root directory of the project.
You can now chat with the Bot.
Return to Visual Studio, and stop debugging.
Add The Nuget Package
The first step is to add the Bot.Builder.Community.Dialogs.ChoiceFlow NuGet package.
Right-click on the project in the Solution Explorer, and select Manage NuGet Packages.
Search for Bot.Builder.Community.Dialogs.ChoiceFlow and install the latest version.
Add The Models
Create a Models folder and add the following code files:
CustomSettings.cs
namespace ChoiceFlowBot
{// This allows us to retrieve custom settings
// from appsettings.json
public class CustomSettings{public string APIWebAddress { get; set; }public string ApplicationGUID { get; set; }public string UserName { get; set; }public string Password { get; set; }}}
DTOApiToken.cs
namespace ChoiceFlowBot
{public class DTOApiToken{public string userName { get; set; }public string password { get; set; }public string applicationGUID { get; set; }}}
DTOSearchParameters.cs
using System.Collections.Generic;
namespace ChoiceFlowBot
{public class DTOSearchParameters{public string searchText { get; set; }public string status { get; set; }public string priority { get; set; }public string createdDate { get; set; }public string dueDate { get; set; }public string assignedRoleId { get; set; }public List<int> selectedTreeNodes { get; set; }public string sortOrder { get; set; }public string sortField { get; set; }public int rowsPerPage { get; set; }public int pageNumber { get; set; }}}
DTOTask.cs
using System.Collections.Generic;
namespace ChoiceFlowBot
{public class DTOTask{public int? taskId { get; set; }public int? portalId { get; set; }public string description { get; set; }public string status { get; set; }public string priority { get; set; }public string createdDate { get; set; }public string estimatedStart { get; set; }public string estimatedCompletion { get; set; }public string dueDate { get; set; }public int? assignedRoleId { get; set; }public string assignedRoleName { get; set; }public string ticketPassword { get; set; }public int? requesterUserId { get; set; }public string requesterName { get; set; }public string requesterEmail { get; set; }public string requesterPhone { get; set; }public int? estimatedHours { get; set; }public bool? sendEmails { get; set; }public List<int> selectedTreeNodes { get; set; }public List<DTOTaskDetail> colDTOTaskDetail { get; set; }}}
DTOTaskDetail.cs
using System.Collections.Generic;
namespace ChoiceFlowBot
{public class DTOTaskDetail{public string taskDetailDescription { get; set; }public int detailId { get; set; }public string detailType { get; set; }public string contentType { get; set; }public string insertDate { get; set; }public int userId { get; set; }public string userName { get; set; }public string emailDescription { get; set; }public string startTime { get; set; }public string stopTime { get; set; }public bool? sendEmails { get; set; }}}
DTOTaskList.cs
using System.Collections.Generic;
namespace ChoiceFlowBot
{public class DTOTaskList{public List<DTOTask> taskList { get; set; }public int totalRows { get; set; }public string errorMessage { get; set; }}}
Add and Load Settings
Open appsettings.json and change it to the following (replacing ** Your Setting ** with your own values gathered earlier):
{"botFilePath": "ChoiceFlowBot.bot","botFileSecret": "",
"CustomSettings": {
"APIWebAddress": "** Your Setting **","ApplicationGUID": "** Your Setting **","UserName": "** Your Setting **","Password": "** Your Setting **"}}
Note: For the APIWebAddress, remove the /swagger part at the end.
Open the Starup.cs file change all the code in the public void ConfigureServices(IServiceCollection services) method to:
// Get the CustomSettings from appsettings.json
// allow them to be passed to any class using dependency injection
services.Configure<CustomSettings>(Configuration.GetSection("CustomSettings"));
IStorage dataStore = new MemoryStorage();
var conversationState = new ConversationState(dataStore);
services.AddSingleton(dataStore);services.AddSingleton(conversationState);services.AddSingleton(new BotStateSet(conversationState));
services.AddBot<ChoiceFlowBotBot>(options =>{var secretKey = Configuration.GetSection("botFileSecret")?.Value;
var botFilePath = Configuration.GetSection("botFilePath")?.Value;
var botConfig = BotConfiguration.Load(botFilePath ?? @".\BotConfiguration.bot", secretKey);
services.AddSingleton(sp => botConfig ?? throw new InvalidOperationException($"The .bot config file could not be loaded. ({botConfig})"));
var environment = _isProduction ? "production" : "development";var service = botConfig.Services.Where(s => s.Type == "endpoint" && s.Name == environment).FirstOrDefault();
if (!(service is EndpointService endpointService)){throw new InvalidOperationException($"The .bot file does not contain an endpoint with name '{environment}'.");
}options.CredentialProvider =new SimpleCredentialProvider(endpointService.AppId, endpointService.AppPassword);
options.Middleware.Add(new AutoSaveStateMiddleware(conversationState));
ILogger logger = _loggerFactory.CreateLogger<ChoiceFlowBotBot>();options.OnTurnError = async (context, exception) =>{logger.LogError($"Exception caught : {exception}");
await context.SendActivityAsync("Sorry, it looks like something went wrong.");
};});
Create The Bot Flow
The ChoiceFlow component uses a .json file to define the conversation flow.
The GitHub page describes the format for the .json file that you use to define the Bot.
Create a file called choiceFlow.json using the following code:
[{"name": "Top","prompt": "What status type of Ticket would you like to search for?","reprompt": "Choose a type from All or New","id": 0,
"choices": [
{"id": 1,
"name": "All","prompt": "What Priority?","choices": [
{"id": 2,
"name": "All","prompt": "What assignment status?","choices": [
{"id": 3,
"name": "All","choices": []
},{"id": 4,
"name": "Unassigned","choices": []
}]},{"id": 5,
"name": "High","prompt": "What assignment status?","choices": [
{"id": 6,
"name": "All","choices": []
},{"id": 7,
"name": "Unassigned","choices": []
}]}]},{"id": 8,
"name": "New","prompt": "What Priority?","choices": [
{"id": 9,
"name": "All","prompt": "What assignment status?","choices": [
{"id": 10,
"name": "All","choices": []
},{"id": 11,
"name": "Unassigned","choices": []
}]},{"id": 12,
"name": "High","prompt": "What assignment status?","choices": [
{"id": 13,
"name": "All","choices": []
},{"id": 14,
"name": "Unassigned","choices": []
}]}]}]}]
After saving the file, click on it, and in the Properties, set Copy to Output Directory to Copy always.
Create The Bot Code
Open the ChoiceFlowBotBot.cs file and replace all the code with the following:
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Bot.Builder.Community.Dialogs.ChoiceFlow;
namespace ChoiceFlowBot
{public class ChoiceFlowBotBot : IBot{// Used to make REST calls
public static HttpClient client;// To store the settings from the appsettings.json file
private CustomSettings _CustomSettings;
// Tracks Conversation State
private ConversationState _conversationState;
// Holds the collection of Dialogs
private DialogSet Dialogs { get; set; }public ChoiceFlowBotBot(
ConversationState conversationState,IOptions<CustomSettings> CustomSettings){// Get the custom settings using dependency injection
_CustomSettings = CustomSettings.Value;// The conversation will be tracked in _conversationState
_conversationState = conversationState ??throw new ArgumentNullException(nameof(conversationState));// Initialize the Dialogs collection
Dialogs =new DialogSet(_conversationState.CreateProperty<DialogState>(nameof(ChoiceFlowBotBot)));
// Get the path to the choiceFlow.json file that contains the flow for the Bot
var pathToChoiceFlowJson =Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),"choiceFlow.json");
// Use the the choiceFlow.json to create the dialogs
Dialogs.Add(new ChoiceFlowDialog(pathToChoiceFlowJson));
}// This method is called each time the user interacts with the Bot
public async Task OnTurnAsync(
ITurnContext turnContext,CancellationToken cancellationToken = default(CancellationToken))
{// Get the Dialogs from the current context
// This will contain a 'stack' of Dialogs as the
// conversation progresses
var dc = await Dialogs.CreateContextAsync(turnContext);switch (turnContext.Activity.Type)
{case ActivityTypes.Message:
// If we are in the middle of a dialog continue it
var dialogResult = await dc.ContinueDialogAsync();if (!dc.Context.Responded)
{switch (dialogResult.Status)
{case DialogTurnStatus.Empty:
// If we are not in the middle of a Dialog
// start one
await dc.BeginDialogAsync("MainDialog");
break;
case DialogTurnStatus.Waiting:
break;
case DialogTurnStatus.Complete:
await dc.EndDialogAsync();break;
default:
await dc.CancelAllDialogsAsync();break;
}}break;
}}}}
This sets up the shell of the code.
Essentially is initializes all the object that will be needed such as the dialogs and the conversation state. It also loads the choiceFlow.json file and creates a collection of waterfall dialog steps from it.
(Note: For more information on Dialogs, see: Using Dialogs (MS Bot Framework V4 Edition))
The code also implements the OnTurnAsync method that will be called each time the user interacts with the Bot in a conversation.
The OnTurnAsync will continue a conversation, by traversing the set of Dialogs. If it is not already in a conversation, it will start with the MainDialog.
We will create the code that will define the MainDialog in the final step.
Add The ADefHelpDesk Methods
The Bot will need to contact ADefHelpDesk to search for Tickets.
Before it can retrieve the Tickets, it needs to obtain an authorization token (also called a JSON Web Token (JWT) or Bearer Token), using the settings entered into the appsettings.json file, and injected into the class using dependency injection.
To facilitate this, add the following method to the class:
private static async Task<string> GetAuthTokenAsync(string APIWebAddress,
DTOApiToken paramApiToken){// Store the final result
string strResult = "";
// Use the HttpClient
using (client)
{// Initialize the HttpClient
client = new HttpClient();
// Create a new REST request
using (var request = new HttpRequestMessage()){// The Swagger page indicates this must be a "Post"
request.Method = HttpMethod.Post;// Set the destination to the method indicated on the Swagger page
// to: api/V1/GetAuthToken
request.RequestUri = new Uri($"{APIWebAddress}api/V1/GetAuthToken");// Convert the parameters to JavaScript Object Notation (JSON) format
var json = JsonConvert.SerializeObject(paramApiToken);request.Content = new StringContent(json, Encoding.UTF8, "application/json");// Make the request to the API endpoint on the ADefHelpDesk site
var response = client.SendAsync(request).Result;// Receive the response
var JsonDataResponse =await response.Content.ReadAsStringAsync();// Convert the response (the JWT (Auth Token)) to a String value
strResult =JsonConvert.DeserializeObject<string>(JsonDataResponse);
// Strip the word Bearer from the token
strResult = strResult.Replace("Bearer ", " ");}}// Return the JWT
return strResult;
}
Now, we need to add a method, to the class, that will use the authentication token to search for Help Desk Tickets:
private static async Task<DTOTaskList> SearchTasksAsync(string APIWebAddress,
string paramBearerToken,
DTOSearchParameters paramDTOSearchParameters){// Store the final result
DTOTaskList strResult = new DTOTaskList();
// Use the HttpClient
using (client)
{// Initialize the HttpClient
client = new HttpClient();
// Create a new REST request
using (var request = new HttpRequestMessage()){// The Swagger page indicates this must be a "Post"
request.Method = HttpMethod.Post;// Set the destination to the method indicated on the Swagger page
// to: api/V1/SearchTasks
request.RequestUri = new Uri($"{APIWebAddress}api/V1/SearchTasks");// Pass the JWT in the 'header' of the request with the word "Bearer" in front
client.DefaultRequestHeaders.Authorization =new AuthenticationHeaderValue("Bearer", paramBearerToken);// Convert the parameters to JavaScript Object Notation (JSON) format
var json = JsonConvert.SerializeObject(paramDTOSearchParameters);request.Content = new StringContent(json, Encoding.UTF8, "application/json");// Make the request to the API endpoint on the ADefHelpDesk site
var response = client.SendAsync(request).Result;// Handle if the JWT is expired
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{strResult.errorMessage = "Unauthorized";
return strResult;
}// Receive the response
var JsonDataResponse =await response.Content.ReadAsStringAsync();// Convert the response to a String value
strResult =JsonConvert.DeserializeObject<DTOTaskList>(JsonDataResponse);}}// Return the response
return strResult;
}
Create the MainDialog
As the final step, we will create the code that will define the MainDialog.
This defines the starting point and the ending point of the conversation.
At the end of the conversation, the code will call the GetAuthTokenAsync and SearchTasksAsync methods to search for Help Desk Tickets.
Add the following code to the end of the ChoiceFlowBotBot constructor method:
Dialogs.Add(new WaterfallDialog("MainDialog", new WaterfallStep[]{async (dc, cancellationToken) =>{// Start the ChoiceFlowDialog that was loaded earlier
// This will take the conversation through the
// 'waterfall' steps defined in the choiceFlow.json file
return await dc.BeginDialogAsync(ChoiceFlowDialog.DefaultDialogId);
},async (dc, cancellationToken) =>{// This is called after the final step
// This is called because we have run out of 'waterfall' steps
// that were created when we loaded the choiceFlow.json file
if (dc.Result is ChoiceFlowItem returnedItem){// *******
// Get a BearerToken (JWT - Auth Token)
// Instantiate the DTOApiToken class and set the parameters
// using the values from CustomSettings
DTOApiToken paramApiToken = new DTOApiToken();
paramApiToken.userName = _CustomSettings.UserName;paramApiToken.password = _CustomSettings.Password;paramApiToken.applicationGUID = _CustomSettings.ApplicationGUID;// Call the GetAuthToken method and retreive the BearerToken (JWT - Auth Token)
string BearerToken = await GetAuthTokenAsync(_CustomSettings.APIWebAddress, paramApiToken);
// *******
// Use the BearerToken to search the Tickets
DTOSearchParameters objDTOSearchParameters = new DTOSearchParameters();
objDTOSearchParameters.pageNumber = 1;objDTOSearchParameters.rowsPerPage = 5;objDTOSearchParameters.sortField = "createdDate";
objDTOSearchParameters.sortOrder = "desc";
objDTOSearchParameters.createdDate = "";objDTOSearchParameters.dueDate = "";objDTOSearchParameters.searchText = "";objDTOSearchParameters.selectedTreeNodes = new List<int>();// Determine what the final dialog Id is
int intFinalDialogId = returnedItem.Id;
// Use this chart to determine what parameters to use:
// (Status/Priority/Assignment Status)
// 03 - All/All/All
// 04 - All/All/Unassigned
// 06 - All/High/All
// 07 - All/High/Unassigned
// 10 - New/All/All
// 11 - New/All/Unassigned
// 13 - New/High/All
// 14 - New/High/Unassigned
switch (intFinalDialogId)
{case 3:
objDTOSearchParameters.status = "All";
objDTOSearchParameters.priority = "All";
objDTOSearchParameters.assignedRoleId = "";break;
case 4:
objDTOSearchParameters.status = "All";
objDTOSearchParameters.priority = "All";
objDTOSearchParameters.assignedRoleId = "-1";
break;
case 6:
objDTOSearchParameters.status = "All";
objDTOSearchParameters.priority = "High";
objDTOSearchParameters.assignedRoleId = "";break;
case 7:
objDTOSearchParameters.status = "All";
objDTOSearchParameters.priority = "High";
objDTOSearchParameters.assignedRoleId = "-1";
break;
case 10:
objDTOSearchParameters.status = "New";
objDTOSearchParameters.priority = "All";
objDTOSearchParameters.assignedRoleId = "";break;
case 11:
objDTOSearchParameters.status = "New";
objDTOSearchParameters.priority = "All";
objDTOSearchParameters.assignedRoleId = "-1";
break;
case 13:
objDTOSearchParameters.status = "New";
objDTOSearchParameters.priority = "High";
objDTOSearchParameters.assignedRoleId = "";break;
case 14:
objDTOSearchParameters.status = "New";
objDTOSearchParameters.priority = "High";
objDTOSearchParameters.assignedRoleId = "-1";
break;
default:
break;
}// Call
var result = await SearchTasksAsync(_CustomSettings.APIWebAddress,BearerToken,objDTOSearchParameters);int intTopMatchingCount =
(result.totalRows > 5) ? 5 : result.totalRows;await dc.Context.SendActivityAsync($"Here are the top {intTopMatchingCount} matching tickets... " +
$"The total found is {result.totalRows}");
foreach (var ticket in result.taskList){await dc.Context.SendActivityAsync($"Ticket#{ticket.taskId} Description: {ticket.description} " +
$"Created: {ticket.createdDate} By: {ticket.requesterName}");
}}return await dc.EndDialogAsync();
}}));
Hit F5 to run the project.
You can now converse with the Bot to search Help Desk Tickets on ADefHelpDesk.
Links
Bot Builder Community - .NET Extensions
- ChoiceFlow for Bot Builder v4 .NET SDK
- Microsoft Bot Framework
- Creating a Hello World! Bot (MS Bot Framework V4)
- Retrieve Emails Using Azure Functions And Create Help Desk Tickets Using API
- Calling a REST API from .Net Core Including Uploading Files
Download
The project is available at http://aihelpwebsite.com/Downloads
You must have Visual Studio 2017 (or higher) installed to run the code.