Bot to web communication with Azure Bot Service
bot-framework web-chat

October 29, 2018

This article refers to Web Chat v3. Current version (v4) uses a slightly different approach. See samples for reference.

I really like the open-source Web Chat component for Microsoft Bot Framework / Bot Service. I've seen developers doing magical things with it - completely changing the look and feel, adding people's icons, embedding HTML forms, redesigning buttons and the interface overall etc.

In this article I want to make note of a powerful, yet often overlooked, feature called "back channel". By using back channel you are able to communicate from your bot with the website it's hosted on.

Guess Speaker

When I was building a chatbot to be showcased at Microsoft booth during this year's mDevCamp conference, I decided that it might be an interesting twist to play with the background communication and dynamically change the website it was hosted on.

Guess Speaker bot

The idea was to guess the name of a speaker from this conference based on profile photo. Site-bot communication had three levels:

This article will contain only snippets of the back channel communication. Full source code is available on GitHub.

Note: As it was built half a year ago, this bot still uses v3 version of Bot SDK. New chatbots should be built on the v4 SDK, which is now GA. Main principle stays the same, only "plumbing" is different.

Implementation

There are two pieces which talk to each other: website and backend. Let's look into them separately.

Web part

This part is just HTML and JavaScript. Web chat control is integrated into the site in a the simpliest way:

index.html

<!-- head -->
<link href="https://cdn.botframework.com/botframework-webchat/latest/botchat.css" rel="stylesheet" />

<!-- body -->
<script src="https://cdn.botframework.com/botframework-webchat/latest/botchat.js"></script>
<script src="./js/bot.js"></script>

The only difference is that we don't create the bot control directly, using secret, but rather initialize it with botConnection:

bot.js

// DirectLine: https://docs.microsoft.com/en-us/azure/bot-service/bot-service-channel-connect-directline?view=azure-bot-service-4.0
var botConnection = new BotChat.DirectLine({secret: "<directline secret>"});

BotChat.App({
    botConnection: botConnection,
    user: { id: 'Visitor' },
    bot: { id: 'Bot' },
    resize: 'detect'
  }, document.getElementById("bot"));

Now, with the bot connection stored in a variable, we are able to do things like sending message to the server that new user has loaded the bot:

botConnection.postActivity({ type: "event", name: "startGame", from: {id: "Visitor" }}).subscribe(_ => console.log("startGame sent"));

And on the other hand, we can subscribe to events coming from the server:

botConnection.activity$
    .filter(activity => activity.type == "event")
    .subscribe(activity => handleEvent(activity));

These events are just special types of activities, which are "invisible" compared to traditional messages. The web chat control doesn't display them.

Handling events is then rather simple:

function handleEvent(event) {
    if (event.name == "endGame") {
        document.getElementById("finalScore").innerHTML = `Your final score: <b>${score}</b>.`;
        startButton.style.display = "block";
    }

    if (event.name == "scoreUpdate") {
        document.getElementById("scoreView").innerHTML = `Score so far: ${event.value}`;
    }
}

Backend part

Now let's take a look at the other side of our equation: bot backend. The general plumbing is not special in any way - it's just a regular v3 Bot Builder chatbot. You have the well-known MessagesController, the Post action and Activity handlers.

This is how it reacts to the startGame event:

MessagesController.cs

public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
    //... handling of Message ...
    else if (activity.Type == ActivityTypes.Event)
    {
        if (activity.AsEventActivity().Name == "startGame")
        {
            var connector = new ConnectorClient(new Uri(activity.ServiceUrl));
            var reply = activity.CreateReply("Welcome! Let's find out how well do you know our speakers. First - what's your **attendee code** (you can find it on your badge, something like: fd64)?");
            await connector.Conversations.ReplyToActivityAsync(reply);
        }
    }
    //... other handlers ...
    var response = Request.CreateResponse(HttpStatusCode.OK);
    return response;
}

This is the only event that comes from the client. Everything else is expected to be a message or system event.

This is how backend sends events to the browser through back channel:

Utils.cs

public static async Task SendEventAsync(IDialogContext context, string eventName, object value)
{
    var eventMsg = context.MakeMessage() as IEventActivity;
    eventMsg.Type = "event";
    eventMsg.Name = eventName;
    eventMsg.Value = value;
    await context.PostAsync((IMessageActivity)eventMsg);
}

For instance for a score change:

public static async Task ChangeScoreAsync(IDialogContext context, double delta)
{
    var score = context.ConversationData.GetValue<double>(Constants.SCORE_KEY);
    score += delta;
    context.ConversationData.SetValue(Constants.SCORE_KEY, score);
    await Utils.SendEventAsync(context, Constants.SCORE_UPDATE_EVENT, score);
}

And that's it. Using this approach you can not only change content on the website, but also do redirects etc.

Reference

Found something inaccurate or plain wrong? Was this content helpful to you? Let me know!

šŸ“§ codez@deedx.cz