Creating A Bot Using ChoiceFlow for Bot Builder v4 .NET SDK

Jan 10

Written by:
1/10/2019 9:57 PM  RssIcon

 

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).

 

image

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.

image

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

 

Install ADefHelpDesk

image

You can download and install ADefHelpDesk from the site at: http://ADefHelpDesk.com.

image

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.

image

You will need to copy the information on the Connection Information tab, for the API account, to use later.

image

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

image

Open Visual Studio and select File, then New, then Project.

image

In the New Project dialog, select the Bot Builder Template, name the project ChoiceFlowBot and click OK.

image

The project will be created.

Hit F5 to run the project.

image

The web browser will open.

 

Connect Using The Bot Framework Emulator

image

Open the Bot Framework Emulator.

image

Select Open Bot.

image

Select the .Bot file that is in the root directory of the project.

image

You can now chat with the Bot.

Return to Visual Studio, and stop debugging.

 

Add The Nuget Package

image

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.

image

Search for Bot.Builder.Community.Dialogs.ChoiceFlow and install the latest version.

Add The Models

image

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

image

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.

image

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

image

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.

image

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": []
              }
            ]
          }
        ]
      }
    ]
  }
]

 

image

After saving the file, click on it, and in the Properties, set Copy to Output Directory to Copy always.

 

Create The Bot Code

image

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

 

image

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

AdefHelpDesk (Documentation)

Download

The project is available at http://aihelpwebsite.com/Downloads

You must have Visual Studio 2017 (or higher) installed to run the code.