CHAPTER 13. Coding Custom Channels with the Direct Line API

As you’ve seen in previous chapters, there are several channels to surface a chatbot upon. If the messaging channels don’t fit your needs, there are email, SMS, and Webchat control channels. However, there are requirements where even those channels won’t fit, and that’s where the Direct Line API comes in.

The Direct Line API lets developers create their own custom channels. For example, you can use direct line to add a chatbot to a mobile app, so in addition to its basic functionality, the app can have a page where users can communicate with a chatbot. There are different applications for the enterprise too, with many programs written for Windows Forms and Windows Presentation Foundation (WPF), and there might be a requirement to add a tab or window to host a chatbot.

The Direct Line API is based on a REST interface, but this chapter uses the Microsoft Direct Line SDK, which is a NuGet package that any project can reference. The fact that this is a REST interface means that any platform and any language can use the Direct Line API. Essentially, you can surface a Bot Framework chatbot literally anywhere there’s a capability to communicate via HTTP over the Internet. This epitomizes the multi-platform nature of the Microsoft Bot Framework. This chapter demonstrates the run anywhere concept via a custom Console Channel and the next section kicks off the chapter by talking about how that works.

Overview of the Console Channel

The example program for this chapter is the Console Channel. As its name suggests, this is a channel that exposes a chatbot on the command line. Before you dismiss the utility of a Console channel as a theoretical exercise, think about the patterns and trends developers increasingly engage in. There’s a renewed focus on the command line as we work with project automation and various scripting shells. Think about how much time Windows administrators and developers spend in PowerShell. Recently, Microsoft has added the Windows Subsystem for Linux (WSL) on Windows 10, opening the path to various flavors of Linux operating systems such as Ubuntu and SUSE. Also, consider that .NET Core not only runs on Windows, but supports applications in both Linux and MacOS – places where command line work is common. Imagine a main frame developer or operator telnetting into a computer that hosts the Console channel and it becomes even more believable that this could have a purpose and make chatbots available to anyone on any platform.

Console Channel Components

In the downloadable source code the ConsoleChannel project has the logic that uses the Direct Line API to communicate with Wine Bot. The same solution also has an updated version of WineBotLuis, from Chapter 8, as the chatbot to communicate with. While this chapter uses Wine Bot as the example chatbot, a few quick changes allows the same code to work with any chatbot.

The design of Console Channel is based on the ability to divide the program into parts that make it easy to explain. Though it’s likely that you and others would organize the code differently, the organization here is on understanding how to use the Direct Line API and making the explanation as simple as possible. Figure 13-1 shows the organization of the code into six major modules: Program, Authenticate, Listen, Configure, and Prompt. Each of these modules is a C# class in a .NET console application.

Images

FIGURE 13-1 The ConsoleChannel Program Sequence diagram.

As shown in Figure 13-1, The Program class drives the whole application and calls into the other classes. Authenticate starts the conversation and returns values that the other classes use, including an expiring token. Listen starts a stream that processes chatbot messages as they arrive. Because the token received in Authenticate expires, Configure takes the responsibility for periodically refreshing that token to keep it alive while the program runs. Once all the other infrastructure is running, Prompt starts taking user input and sending it to the chatbot. Figure 13-2 shows the Console Channel in action.

Images

FIGURE 13-2 A user session with the console channel.

You should run this program to really see the sequence of events, but we explain what you’ll be seeing. The program has prompts, Console Channel>, that are where the user can communicate with the chatbot. Before the first prompt, the Console Channel program shows a message that also includes the /exit command. Before the user has a chance to type anything (the first prompt is empty) Wine Bot responds with the welcome message. After that, the user types What white wine selections do you have with a 50 or higher rating? As an aside, this is the beauty of NLP with LUIS because it recognized a phrase it wasn’t explicitly trained for. The response shows a list of available wines. Also notice the format of the Wine Bot responses. This is a benefit of markdown because it looks great on a surface that translates it graphically, but is also readable in plain text. The user ends the program with the /exit command and the program response indicates that the operation was cancelled—a subtle indicator of how the program ends the conversation.

Examining Console Channel Code

The Console Channel program, Listing 13-1, follows the same logic described in the Figure 13-1 sequence diagram. It asynchronously calls methods of each of the classes, representing the modules of the application. This program references the Microsoft.Bot.Connector.DirectLine NuGet package – a Microsoft library for accessing the Direct Line API. Many of the types used in this program are from this package.

Listing 13-1 The Console Channel Program: Program.cs

using Microsoft.Bot.Connector.DirectLine;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleChannel
{
    class Program
    {
        static void Main() => new Program().MainAsync().GetAwaiter().GetResult();

        async Task MainAsync()
        {
            var cancelSource = new CancellationTokenSource();

            AuthenticationResults results =
                await Authenticate.StartConversationAsync(cancelSource.Token);

            await Listen.RetrieveMessagesAsync(results.Conversation, cancelSource);

            await Configure.RefreshTokensAsync(results.Conversation, results.Client, cancelSource.Token);

            await Prompt.GetUserInputAsync(results.Conversation, results.Client, cancelSource);
        }
    }
}

In Listing 13-1, Main invokes the MainAsync method. The first thing MainAsync does is instantiate a CancellationTokenSource, cancelSource, which is an argument to each method, providing a means to end the program. StartConversationAsync starts a new conversation with Direct Line and returns an instance of AuthenticationResults, results, containing the Direct Line library type, results.Conversation, holding several values the rest of the program needs to operate. The results variable also contains the DirectLineClient instance, results.Client, so other modules can share the same instance. The remaining methods operate via the general description given for Figure 13-1.

An important aspect of this program’s design is multi-threading. This program runs on three threads:

1. The Program, Authenticate, and Prompt class methods run on the main thread.

2. The Listen class methods run on a second thread.

3. The Configure class methods run on a third thread.

This simplifies the program because each thread has a unique responsibility. The first thread starts the conversation, calls each method, and makes its way to GetUserInputAsync, which you’ll learn later runs a loop to continuously accept user input until they choose to end the program. The RetrieveMessagesAsync method starts a new thread that opens the input stream from the chatbot and waits to process each message as it arrives. RefreshTokensAsync starts a new thread that delays and wakes up in time to refresh the current conversation token so the user can seamlessly continue communicating with the chatbot.


Images Note

The alternative to the multi-threaded design is to weave the functionality of these three threads together into one and perform polling, which would be inherently inefficient. When you consider the type of error handling and instrumentation required to make a real-world implementation work, a single threaded approach has the potential for more complexity than what you might think at first glance. Also, remember that this is a console application and the implementation will be much different in another technology like WPF, UWP, or a multi-platform mobile toolkit like Xamarin.


Some of the modules use a shared class, Message, that holds some common code, shown in Listing 13-2. Again, your choice for shared data might differ, but this is a simplicity-first approach, rather than attempting to adhere to one of the many opinions on the subject.

LISTING 13-2 The Console Channel Program: Message.cs

using System;

namespace ConsoleChannel
{
    static class Message
    {
        public const string ClientID = “ConsoleChannel”;
        public const string ChatbotID = “WineChatbot”;

        volatile static string watermark;

        public static string Watermark
        {
            get { return watermark; }
            set { watermark = value; }
        }

        public static void WritePrompt() => Console.Write(“Console Channel> “);
    }
}

Listing 13-2 has two constants: ClientID and ChatbotID. ConsoleChannel uses ClientID as the From ID when communicating with the chatbot. This is hard-coded, rather than writing code for users to log in. Your program might have users that log in and you’ll have a user name and ID to use instead. The ChatbotID, WineChatbot, is the registered handle with the Bot Framework for Wine Bot. If creating a generic channel that can communicate with any chatbot, this would be configurable.

An important part of communicating with a chatbot via Direct Line is the Watermark, which is a property, wrapping the watermark field. The watermark indicates which message we received from the chatbot and helps avoid duplicate messages. Both of the threads that listen for new messages and accept user input use Watermark. Because it’s a shared field, used by multiple fields, we used the volatile modifier to minimize incomplete reads and writes. Depending on your implementation, this might or might not work for you. In this example, we only care that we read the value consistently. Knowing that there’s still potential for duplicates, Channel Console has mitigating code, which you’ll see later in this chapter, to prevent showing any duplicates to the user.


Images Tip

For more information on multi-threaded programming, Microsoft Press has an excellent book: CLR via C# (https://aka.ms/clrcsbook). The book’s author, Jeffrey Richter, is one of the foremost experts on multi-threading in the Windows and .NET domains and this book has a thorough discussion on the topic for .NET developers.


Various parts of the program call WritePrompt, which is a single location for a consistent user prompt. The remaining sections of this chapter explain how each of these modules work, with the next section explaining how to start a conversation.

Starting a Conversation

As mentioned earlier, this application uses the Microsoft Direct Line NuGet package. The code in Listing 13-3 shows how to use that package to instantiate a DirectLineClient and start a conversation. We’ve called the containing class Authenticate because starting a conversation also authenticates at the same time.

LISTING 13-3 The Console Channel Program: Authenticate.cs

using Microsoft.Bot.Connector.DirectLine;
using System.Configuration;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleChannel
{
    class AuthenticationResults
    {
        public Conversation Conversation { get; set; }
        public DirectLineClient Client { get; set; }
    }

    class Authenticate
    {
        public static async Task<AuthenticationResults> 
            StartConversationAsync(CancellationToken cancelToken)
        {
            System.Console.WriteLine(
                “
Console Channel Started
” +
                “Type ”/exit” to end the program
”);
            Message.WritePrompt();

            string secret = ConfigurationManager.AppSettings[“DirectLineSecretKey”];
            var client = new DirectLineClient(secret);
            Conversation conversation =
                await client.Conversations.StartConversationAsync(cancelToken);

            return 
                new AuthenticationResults
                {
                    Conversation = conversation,
                    Client = client
                };
        }
    }
}

StartConversationAsync accepts a CancellationToken and returns an instance of AuthenticationResults, which contains Conversation and DirectLineClient. We discuss the cancelToken in a later part of the chapter on ending a conversation, but you’ll notice that it’s passed to all async method calls throughout the program.

The first part of the StartConversationAsync method lets a user know that the program started and sends the prompt to the screen, letting the user know that the program is ready for input.

The app.config configuration file contains an appSettings key for DirectLineSecretKey. You can get this key by visiting the Bot Framework site for the chatbot you want to communicate with, Wine Bot, and creating a Direct Line channel. Chapters 11 and 12 showed how to configure several channels and this process is similar. Just create the channel and copy a secret key into an appSettings entry, like below:

  <appSettings>
    <add key=”DirectLineSecretKey” value=”Your secrect key goes here”/>
  </appSettings>

The StartConversationAsync method passes that secret key as an argument to instantiate a new DirectLineClient. This is the DirectLineClient instance returned to the caller.

The DirectLineClient type has a Conversations property, representing the /conversations segment of the REST endpoint URL. Calling StartConversationAsync, the Direct Like API returns a Conversations object, holding several values required in the rest of the program. You’ll see what these values are and how they’re used in context in the following sections of this chapter.

With a started conversation, the program starts a new thread for listening for new activities from the chatbot.

Listening for New Activities

Direct Line offers a couple of ways to receive chatbot messages: polling and stream. The polling approach uses a GetActiviesAsync method that returns a set of activities from the chatbot. The thing is that you need to continue polling and the activities returned are as fresh as the time between polls. Polling too frequently results in wasted bandwidth and might slow down a program and polling with too much of a delay leaves a less than ideal experience for the user.

The most efficient technique for receiving new activities from a chatbot is via a stream, which is an implementation of Web Sockets. This means that the program receives a response as soon as the chatbot sends it out, ensuring efficient operation and a responsive user experience. Fortunately, the .NET Framework has support for Web Sockets that the Console Channel program uses, as shown in Listing 13-4.

LISTING 13-4 The Console Channel Program: Listen.cs

using Microsoft.Bot.Connector.DirectLine;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleChannel
{
    class Listen
    {
        public static async Task RetrieveMessagesAsync(
            Conversation conversation, CancellationTokenSource cancelSource)
        {
            const int ReceiveChunkSize = 1024;

            var webSocket = new ClientWebSocket();
            await webSocket.ConnectAsync(
                new Uri(conversation.StreamUrl), cancelSource.Token);

            var runTask = Task.Run(async () =>
            {
                try
                {
                    while (webSocket.State == WebSocketState.Open)
                    {
                        var allBytes = new List<byte>();
                        var result = new WebSocketReceiveResult(0, WebSocketMessageType.Text, false);
                        byte[] buffer = new byte[ReceiveChunkSize];

                        while (!result.EndOfMessage)
                        {
                            result = await webSocket.ReceiveAsync(
                                new ArraySegment<byte>(buffer), cancelSource.Token);

                            allBytes.AddRange(buffer);
                            buffer = new byte[ReceiveChunkSize];
                        }

                        string message = Encoding.UTF8.GetString(allBytes.ToArray()).Trim();
                        ActivitySet activitySet = JsonConvert.DeserializeObject<ActivitySet>(message);

                        if (activitySet != null)
                            Message.Watermark = activitySet.Watermark;

                        if (CanDisplayMessage(message, activitySet, out List<Activity> activities))
                        {
                            Console.WriteLine();
                            activities.ForEach(activity => Console.WriteLine(activity.Text));
                            Message.WritePrompt();
                        }
                    }
                }
                catch (OperationCanceledException oce)
                {
                    Console.WriteLine(oce.Message);
                }
            });
        }

        static bool CanDisplayMessage(string message, ActivitySet activitySet, out List<Activity> activities)
        {
            if (activitySet == null)
                activities = new List<Activity>();
            else
                activities =
                    (from activity in activitySet.Activities
                     where activity.From.Id == Message.ChatbotID &&
                           !string.IsNullOrWhiteSpace(activity.Text)
                     select activity)
                    .ToList();

            SuppressRepeatedActivities(activities);

            return !string.IsNullOrWhiteSpace(message) && activities.Any();
        }

        static Queue<string> processedActivities = new Queue<string>();
        const int MaxQueueSize = 10;

        static void SuppressRepeatedActivities(List<Activity> activities)
        {
            foreach (var activity in activities)
            {
                if (processedActivities.Contains(activity.Id))
                {
                    activities.Remove(activity);
                }
                else
                {
                    if (processedActivities.Count >= 10)
                        processedActivities.Dequeue();

                    processedActivities.Enqueue(activity.Id);
                }
            };
        }
    }
}

Listing 13-4 is quite extensive, as you might expect because it launches a new thread that implements a loop, handling the Direct Line stream through Web Sockets. The RetrieveMessagesAsync method starts by instantiating a WebSocket and connecting, as shown below:

            var webSocket = new ClientWebSocket();
            await webSocket.ConnectAsync(
                new Uri(conversation.StreamUrl), cancelSource.Token);

The Uri argument to ConnectAsync uses the StreamUrl from the conversation parameter. This is the same Conversation instance that StartConversation returned. The code uses the Task Parallel Library (TPL) Task.Run to start the rest of the code on a new thread. There’s also a while loop that iterates as long as the WebSocket is in the open state.

The Direct Line API returns chunks of text in blocks of 1024 bytes, so we need a loop that collects each block until the entire set of activities delivers, shown below:

                        var allBytes = new List<byte>();
                        var result = new WebSocketReceiveResult(0, WebSocketMessageType.Text, false);
                        byte[] buffer = new byte[ReceiveChunkSize];

                        while (!result.EndOfMessage)
                        {
                            result = await webSocket.ReceiveAsync(
                                new ArraySegment<byte>(buffer), cancelSource.Token);

                            allBytes.AddRange(buffer);
                            buffer = new byte[ReceiveChunkSize];
                        }

The result variable is an instance of WebSocketReceiveResult. The ReceiveAsync method waits for the next available set of activities from the chatbot and returns a WebSocketReceiveResult to indicate current status, causing the loop to continue until EndOfMessage is true. ReceiveAsync also populates buffer, through the ArraySegment instance. The allBytes collects all of the bytes returned for later deserialization.

Notice that the last line of the loop re-instantiates buffer, which is important because buffer retains the contents of the previous call to ReceiveAsync. You would receive garbage on any final loop, from the previous buffer contents, for a set of activities where the number of bytes are less than 1024 bytes, which is frequent.

When the code receives a full activity, result.EndOfMessage is true, it stops executing the while loop and processes the activity, repeated below:

                        string message = Encoding.UTF8.GetString(allBytes.ToArray()).Trim();
                        ActivitySet activitySet = JsonConvert.DeserializeObject<ActivitySet>(message);

                        if (activitySet != null)
                            Message.Watermark = activitySet.Watermark;

                        if (CanDisplayMessage(message, activitySet, out List<Activity> activities))
                        {
                            Console.WriteLine();
                            activities.ForEach(activity => Console.WriteLine(activity.Text));
                            Message.WritePrompt();
                        }

The Direct Line API returns data in a UTF-8 format and the code uses that to convert bytes to a string, which is a JSON object. This JSON object represents a set of activities, called an ActivitySet, which is a type in the Direct Line library.

ActivitySet also has a Watermark property. If you recall from the Message class, Listing 13-2, discussion, Watermark keeps track of messages to help avoid duplicates. In our case, we’re minimizing duplicates and you’ll see how that happens soon. The CanDisplayMessage, shown below, accepts the message and activitySet arguments and returns a List<Activity>, activities, that can be displayed to the user:

        static bool CanDisplayMessage(string message, ActivitySet activitySet, out List<Activity> activities)
        {
            if (activitySet == null)
                activities = new List<Activity>();
            else
                activities =
                    (from activity in activitySet.Activities
                     where activity.From.Id == Message.ChatbotID &&
                           !string.IsNullOrWhiteSpace(activity.Text)
                     select activity)
                    .ToList();

            SuppressRepeatedActivities(activities);

            return !string.IsNullOrWhiteSpace(message) && activities.Any();
        }

The Direct Line API sends empty messages/activities as a form of keep-alive message to keep the stream open during periods of inactivity. So, the code has to detect these keep-alive messages. When activitySet has values in its Activities property, the LINQ statement filters to make sure that only activities with text can appear. It also ensures that the message received is from the chatbot, rather than the Bot Framework, or replays of the user’s message. The method returns false if the message was a keep-alive message or none of the activities passed the LINQ filter. Prior to returning, the code also makes sure we don’t show the user any duplicates that might have slipped through, via the SuppressRepeatedActivities method as follows:

        static Queue<string> processedActivities = new Queue<string>();
        const int MaxQueueSize = 10;

        static void SuppressRepeatedActivities(List<Activity> activities)
        {
            foreach (var activity in activities)
            {
                if (processedActivities.Contains(activity.Id))
                {
                    activities.Remove(activity);
                }
                else
                {
                    if (processedActivities.Count >= 10)
                        processedActivities.Dequeue();

                    processedActivities.Enqueue(activity.Id);
                }
            };
        }

Images Note

In addition to message activities, Direct Line supports a large subset of other activity types. It doesn’t support contactRelationUpdate. Also, since the Bot Connector takes care of conversationUpdate, you don’t need to (so, conversationUpdate isn’t available either). The typing activity is only available via web sockets, but not polling (HTTP GET). All other activities are supported via both polling and web sockets.


The Direct Line API tends to be overprotective to ensure it doesn’t lose any messages, so it’s probable that you’ll receive duplicates. This program keeps track of the most recent messages and doesn’t show a message that has already been shown to a user. The implementation simulates a circular buffer with a Queue<string>, processedActivities. We chose the size to be 10 because more than 10 duplicates for Wine Bot is unlikely. Generally, you want to minimize the size to prevent resource waste from too many queue searches, yet large enough to cover message bursts from the chatbot to avoid duplicates. The code checks processedActivities for each of the activities and removes duplicates. If the queue is larger than max, it dequeues the oldest activity ID before enqueueing the current activity ID.

That was how you can receive messages from a chatbot. Next, let’s look at how to keep the conversation going.

Keeping the Conversation Open

The Authenticate module, discussed previously, started the conversation. One of the outputs of that process was the Direct Line type, Conversation, which holds ConversationId and ExpiresIn properties. Because the conversation expires, in the number of seconds indicated by ExpiresIn, this program takes a pro-active approach and keeps the conversation alive. An alternative is to wait until the conversation closes and re-open. The pro-active approach minimizes interruption to the user because disconnections typically involve timeouts and network latency that might result in a degraded user experience. It’s your choice and this discussion is to help you think about the trade-offs when designing a custom channel. The Configure class in Listing 13-5 implements the Configure module from Figure 13-1, starting a new thread to handle token refreshes.

LISTING 13-5 The Console Channel Program: Configure.cs

using Microsoft.Bot.Connector.DirectLine;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleChannel
{
    class Configure
    {
        public static async Task RefreshTokensAsync(
            Conversation conversation, DirectLineClient client, CancellationToken cancelToken)
        {
            const int ToMilliseconds = 1000;
            const int BeforeExpiration = 60000;

            var runTask = Task.Run(async () =>
            {
                try
                {
                    int millisecondsToRefresh =
                        ((int)conversation.ExpiresIn * ToMilliseconds) - BeforeExpiration;

                    while (true)
                    {
                        await Task.Delay(millisecondsToRefresh);

                        await client.Conversations.ReconnectToConversationAsync(
                            conversation.ConversationId,
                            Message.Watermark,
                            cancelToken);
                    }
                }
                catch (OperationCanceledException oce)
                {
                    Console.WriteLine(oce.Message);
                }
            });
            await Task.FromResult(0);
        }
    }
}

The RefreshTokensAsync method in Listing 13-5 starts a new thread with Task.Run that loops and periodically updates the tokens associated with the current conversation. The DirectLineClient instance has a Tokens property that contains the tokens obtained during the call to StartConversationAsync. The program shares the same DirectLineClient instance with multiple methods, preventing the need to re-instantiate a new object every time we want to call a method.

The ToMilliseconds constant helps convert the ExpiresIn seconds to milliseconds. Subtracting the BeforeExpiration constant sets the number of milliseconds to wait before refreshing the token. The goal here is to do the refresh for a certain amount of time before expiration to avoid ending the conversation and you might experiment with this value to account for any delays or network latency between your code and the Direct Line API. You can see the Task.Delay, taking millisecondsToRefresh as a make-shift timer between refreshes in the while loop.

ReconnectToConversationAsync performs the refresh and keeps the conversation open. Notice that it’s also passing the Watermark as an argument. This is why you want to think about how to synchronize access to Watermark because there are two threads reading or writing to it. See the discussion in the previous section for more information on how Watermark is used there.

Now that we’re receiving real-time messages from the chatbot and are keeping the conversation open, let’s look at how to send user input to the chatbot.

Sending Activities

While separate threads are running for receiving tasks and keeping the program alive, the main thread makes its way down to the Prompt class, which implements the Prompt module from Figure 13-1. Prompt starts a loop that takes user input and sends that input to the chatbot. Listing 13-6 shows the Prompt implementation.

LISTING 13-6 The Console Channel Program: Prompt.cs

using Microsoft.Bot.Connector.DirectLine;
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleChannel
{
    class Prompt
    {
        public static async Task GetUserInputAsync(
            Conversation conversation, DirectLineClient client, CancellationTokenSource cancelSource)
        {
            string input = null;

            try
            {
                while (true)
                {
                    input = Console.ReadLine().Trim().ToLower();

                    if (input == “/exit”)
                    {
                        await EndConversationAsync(conversation);
                        cancelSource.Cancel();
                        await Task.Delay(500);
                        break;
                    }

                    if (string.IsNullOrWhiteSpace(input))
                    {
                        Message.WritePrompt();
                    }
                    else
                    {
                        IMessageActivity activity = Activity.CreateMessageActivity();
                        activity.From = new ChannelAccount(Message.ClientID);
                        activity.Text = input;

                        await client.Conversations.PostActivityAsync(
                            conversation.ConversationId,
                            activity as Activity,
                            cancelSource.Token);
                    }
                }
            }
            catch (OperationCanceledException oce)
            {
                Console.WriteLine(oce.Message);
            }
        }

        static async Task EndConversationAsync(Conversation conversation, DirectLineClient client)
        {
            IEndOfConversationActivity activity = Activity.CreateEndOfConversationActivity();
            activity.From = new ChannelAccount(Message.ClientID);

            await client.Conversations.PostActivityAsync(
                conversation.ConversationId, activity as Activity);
        }
    }
}

The GetUserInputAsync method in Listing 13-6 handles the task of obtaining user input and sending it to the chatbot. The while loop continuously prompts the user for input until the user types /exit and we’ll discuss how that code works in the next section.

The if statement prevents sending empty text to the chatbot and writes a new prompt. If user input does contain text, the code builds a new Activity instance. The From property receives a ChannelAccount instance, with an ID set to Message.ClientID. If you recall from previous discussions, your implementation might have a user log in, thus passing your user ID and user name in the ChannelAccount instance. Finally, PostAsync sends the activity, containing user input, to the chatbot.

So far, you’ve seen how this program receives chatbot messages, keeps the conversation alive, and sends user input to the chatbot. Next, let’s discuss what happens when the program ends.

Ending Conversations

.NET has formalized support for cancelling async and multi-threaded applications through the CancellationTokenSource and CancellationToken types. This program uses that cancellation support for handling when a user wants to exit the conversation. In this section, we’ll discuss the .NET cancellation support and then show how ending a conversation works.

Examining CancellationTokenSource and CancellationToken

Listing 13-1 shows how MainAsync creates a CancellationTokenSource instance, cancelSource. This CancellationTokenSource instance is responsible for notifying all threads when they should stop running. In this example, the purpose of stopping threads is to close the program.

Notice that RetrieveMessagesAsync and GetUserInputAsync accept the cancelSource instance, giving them the ability to cancel all threads in addition to being notified when their threads should be canceled. StartConversationAsync and RefreshTokensAsync receive cancelSource.Token, where Token is type CancellationToken. Having a CancellationToken gives the methods the ability to know when their thread is being canceled. Let’s examine how a couple of these methods use CancellationToken.

The StartConversationAsync method, Listing 13-3, passes the cancelToken parameter to the Direct Line StartConversationAsync.

            Conversation conversation =
                await client.Conversations.StartConversationAsync(cancelToken);

The RefreshTokenAsync method, Listing 13-5, passes the cancelToken parameter to the Direct Line ReconnectToConversationAsync.

                        await client.Conversations.ReconnectToConversationAsync(
                            conversation.ConversationId,
                            Message.Watermark,
                            cancelToken);

In each of these cases when the CancellationTokenSource is told to cancel, each of the async methods, if currently running, receives a notification through cancelToken that they should stop running. This lets the program stop running all threads simultaneously and shut down gracefully.


Images Note

How graceful a thread shuts down upon receiving a cancellation notice, is up to you for the code you’ve written. 3rd party developer libraries might also have guidance on how to end their threads.


Next, let’s discuss what happens when the user wants to end the conversation.

Handling User Exits

When a user wants to stop the conversation, they type /exit. This command goes to the GetUserInputAsync method, which handles all user input. The logic to handle the /exit command is below, repeated from Listing 13-6:

                    if (input == “/exit”)
                    {
                        await EndConversationAsync(conversation);
                        cancelSource.Cancel();
                        await Task.Delay(500);
                        break;
                    }

After calling EndConversationAsync, the code uses its CancellationTokenSource parameter, cancelSource, to Cancel all threads. Since all methods receive the same CancellationTokenSource instance or the CancellationToken from that instance, they will all be notified to cancel. The previous section showed how the RetrieveMessagesAsync method handles cancellation when this happens. This code also adds a 500 millisecond delay before exiting, just in case the other threads need a little more time because the break causes the current loop to exit, return to MainAsync, return to Main and end the program. You might want to tweak this, depending on how you decide to manage the lifetime of your own threads.

The EndConversationAsync method, repeated from Listing 13-6 below, shows how to let Direct Line know that the user wants to end the conversation:

        static async Task EndConversationAsync(Conversation conversation, DirectLineClient client)
        {
            IEndOfConversationActivity activity = Activity.CreateEndOfConversationActivity();
            activity.From = new ChannelAccount(Message.ClientID);

            await client.Conversations.PostActivityAsync(
                conversation.ConversationId, activity as Activity);
        }

The Direct Line library, from the NuGet package, has methods to handle most scenarios, but doesn’t handle ending a conversation. So, EndConversationAsync uses client.Conversations.PostActivityAsync to send an IEndOfConversationActivity to the chatbot.


Images Tip

You can use the technique in EndConversationAsync to send other types of non-message activities to a chatbot.



Images Note

In addition to the services discussed in this chapter, you can attach files also by using the client.Conversations.UploadAsync method, where client is an instance of the DirectLineClient type.


Summary

Now you know how to build a client with the Bot Framework Direct Line API. The demo program in this chapter is a Console Channel. While minimal to make the demo simple, this program demonstrates that you can expose a chatbot on literally any platform where code can make HTTP calls.

This code was organized for learning and there were several tips along the way to help consider how to design your own custom channel implementation. In particular this code runs on three threads where the main thread does set up and then gets user input. The other two threads receive user input and keep the conversation open. Finally, you saw how to exit the program using the standard .NET CancellationTokenSource for graceful shutdown.

The last few chapters have discussed built-in, 3rd party, and custom channels. In the next chapter we’ll move from client side, back to server side development and discuss how to add intelligence to a chatbot with Cognitive Services.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.172.115