How the Microsoft Bot Framework Changed Where My Friends and I Eat: Part 1
Bots are everywhere nowadays, and we interact with them all of the time. From interactions on our phones, in chat rooms, in GitHub discussions, and Slack channels, these guys are everywhere and don't seem to be going anywhere any time soon.
After letting this realization sink in, I quickly realized a simple use case that I could use a bot for that would help me with a real-world problem I was facing. This post will cover that adventure and detail how the Microsoft Bot Framework helped me figure out where I should go eat with my friends.
Getting Started with Bots
I wasn't entirely sure where to go about getting started with building a bot, so I began talking with Paul Seal on Slack, who had recently just built one for a simple hangman game that he was creating and he referred me to the Microsoft Bot Framework site.
The site itself has fantastic documentation for building bots in .NET, Node.js and a few other technologies, so depending on your preferences, you can follow the tutorial of your choice. Since I'm primarily a .NET guy, this post will cover that side more heavily, but I'd assume that the Node approach is just as simple.
To get your environment ready, you'll need the following installed:
- Visual Studio 2017 - Since we are going to be building the bot using C# and .NET, Visual Studio is going to be the way to go.
- The Bot Application Template - This provides a basic template / starting point to go about building a bot.
- Bot Framework Channel Emulator - This application will allow you to debug/test your bot locally in a virtual chat environment.
After installing Visual Studio, you'll need to take the Bot Application template and place it within the following directory on your machine:
%USERPROFILE%\Documents\Visual Studio 2017\Templates\ProjectTemplates\Visual C#\
Once you've done that, you should see the available option to create a new Bot Application within Visual Studio:
The generated project template should work very similar to an MVC / Web API style project with a single controller to handle messages and a few other areas. Additionally, this default template has some basic functionality already included to help point you in the right direction.
Making First Contact
After restoring the necessary NuGet packages, you should be ready to run the project and interact with your bot for the first time. Once you launch it, you should be met by the following page, indicating that your bot is running and listening on a specific port:
There isn't a whole lot to do here with this page, so you'll want to launch the previously downloaded Bot Framework Emulator. Once it is up and running, you'll need to set the Bot Url to point match the current port that your application is running on (e.g. http://localhost:{your-port}/api/messages
) as seen below:
The default project is extremely simple and will simply count the number of characters within your message and return the message along with the count. You can see this by taking a look at the Post()
method from the MessagesController
:
/// <summary>
/// POST: api/Messages
/// Receive a message from a user and reply to it
/// </summary>
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
if (activity.Type == ActivityTypes.Message)
{
await Conversation.SendAsync(activity, () => new Dialogs.RootDialog());
}
else
{
HandleSystemMessage(activity);
}
var response = Request.CreateResponse(HttpStatusCode.OK);
return response;
}
As you can see, when the controller receives a message, it will actually pass that activity along to another class, which handles asynchronously processing the message:
[Serializable]
public class RootDialog : IDialog<object>
{
public Task StartAsync(IDialogContext context)
{
context.Wait(MessageReceivedAsync);
return Task.CompletedTask;
}
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
var activity = await result as Activity;
// calculate something for us to return
int length = (activity.Text ?? string.Empty).Length;
// return our reply to the user
await context.PostAsync($"You sent {activity.Text} which was {length} characters");
context.Wait(MessageReceivedAsync);
}
}
So just to clarify, when you send a message the following occurs:
- The message is received by the
Post()
method within theMessagesController
. - The contents of the message (i.e. activity) are passed as context to the
RootDialog
. - The dialog handles processing the message, using the context, and returns a task upon completion.
- This result is then passed back through the
Conversation.SendAsync()
method to be sent to the user.
This could be greatly simplified or complicated based on your scenarios (e.g. defining a specific dialog or chain of dialogs) but we will leave this in for now. So let's send the bot a message to ensure that it works as expected:
So as you can see, everything seems to be working just as expected.
All we did >was ask the bot "What's the answer to the ultimate question" and it plainly had "42" in the response. Nothing strange, no singularities here, nothing out of the ordinary.
Let's just move along to getting this thing out in the wild, as what harm could come from an intelligent entity such as this being publicly accessible.
Deploying the Bot (aka Enter Skynet)
In order to avoid any Skynet-style issues, we need to properly register the Bot with the Microsoft Bot Framework, so that they can monitor it and ensure it doesn't take over the world.
To do this, follow these steps:
- Register on the Microsoft Bot Framework Site to set up your account and get the necessary API keys to use the framework / tooling.
- Visit the My Bots tab and Register Your Bot - Here you will actually register your bot and provide any details, such as where it will be hosted, descriptions, etc.
During this process, you will be given two keys: MicrosoftAppId
and MicrosoftAppPassword
, both of these are important and will need to be accessible by your bot at run time so you can configure them as an environmental variable within your hosting environment or by simply including them within a web.config
file as seen below:
<appSettings>
<add key="BotId" value="ThirdThursdayBot" />
<add key="MicrosoftAppId" value="foo" />
<add key="MicrosoftAppPassword" value="bar" />
</appSettings>
Next, you'll want to go about publishing your existing bot to your target location. In this example, we will just be hosting on a free Azure website, so you can just choose the Publish > Create New Profile (if necessary) > Microsoft Azure App Service from within Visual Studio:
From there, you can configure your resources and assign the URL that you want your bot to be hosted at (e.g. https://{your-api-app-name}.azurewebsites.net
):
After successfully creating that, you should be able to just hit Publish and your bot will be released into the wild.
NOTE: When registering your bot on the Microsoft Bot Framework, you needed to assign a messaging endpoint, which you may have left blank earlier. If that was the case, you'll want to return and ensure that you set it to the URL where your bot now lives using the
https://{your-api-app-name}.azurewebsites.net/api/messages
format as seen below:
This should finish the registration process, which will allow you to now interact with your bot using the framework portal:
We will visit this section later to tackle issues like setting up other access channels to interact with the bot.
Problem: Handling Third Thursdays
Now my entire reasoning for this bot surrounds an event that my friends and I have been doing for a few years now that we call "Third Thursday". Third Thursday is a monthly gathering, as you might expect on the third Thursday of each month and it works as follows:
- Each month a different member of the group gets to choose where we all eat.
- No restaurants can be repeated (until all local areas are exhausted).
- The restaurant must serve alcohol (and obviously food).
Fairly simple right? Well, after doing this for over two years, it can be tough to remember where we have eaten previously, whose choice it is this month, and when the next day is, so I thought that I could build a bot to handle this for us.
So my goals for the bot were as follows:
- Trivial Set-up - Since this was a small side project, I didn't want to invest a ton of time/effort into infrastructure or scaling concerns.
- Basic Question / Answer Support - Although Azure has quite a few incredible Cognitive Services features, I just wanted the bot to display a list of commands that users could enter and receive quick responses.
- Web / Skype / Text Support - I wanted my friends and members of this group to be able to easily access the bot from wherever they were. On a computer, check a site, on your phone, send it a text, etc.
These all seemed pretty doable, so let's dive into each one.
Setting Up Third Thursday Bot
I had two concerns to tackle with getting this bot set up initially, with one being more important than the other:
- Hosting - The bot needed to actually live somewhere.
- Persistence - The bot needs to know (and remember) things about what the members of the group have done.
Since this was a side project, my main priority was getting it done.
I figured that a free Azure Website would be the easily way to quickly deploy the bot and get it out there. The fact that it is free is obviously appealing, and being able to set up all my environmental configurations is quite nice as well. With regards to persistence, I elected to go with Google's Firebase platform, which is a very basic free database that you can use, which exposes an API that would make data access a breeze.
Setting up Firebase is dead simple and really only requires a Google account. You simply go in, create a new project, and you are done. Firebase projects feature all sorts of great crap like analytics, notifications, file storage, testing, etc. but in this scenario we are concerned with one thing: data.
After culling through months and months of bank account statements and text messages, I managed to compile a list of where the hell we had eaten over the past 28 months, so I needed to actually go about seeding my database with two things that I know I would need:
- Members - I need to know everyone that is in the group, so that I can determine who's turn it is.
- Restaurants - Most importantly, I need to know where the group has eaten, when we ate at a given location, and who chose it.
I figured that storing Members as a string array would be sufficient (as I really only needed the names), however Restaurants would have a few more properties, which might result in a C# class that looks like this:
public class Restaurant
{
public string Location { get; private set; }
public string PickedBy { get; private set; }
public DateTime Date { get; private set; }
}
Using that information, I knew I could simply populate the Members of my Firebase database manually, and import my existing restaurants data from JSON by converting my existing Excel spreadsheet using Mr. Data Converter. After importing the data in, I had everything that I needed:
Great! Now that I have my data, let's jump back to the bot and start up a conversation.
Striking Up a Conversation
As I mentioned earlier, there are a lot of really, really powerful things that you can do with regards to intent and text analysis to figure out what the user is trying to say. But this project was all about quick and dirty, so you aren't going to find any in-depth discussions of that here, but what you will find is: if-statements.
The first thing that I wanted the bot to do is actually reply to a response with the list of commands that it supported, so before actually creating those commands, I wanted to create a collection of all of my supported messages and templates that I built the following to separate them out in a Messages.cs
file:
public class Messages
{
public readonly static string DefaultResponseMessage =
$"Hi, I'm Third Thursday Bot! I support the following commands: {Environment.NewLine}" +
$"- 'show all' - Lists all of the previous Third Thursday selections. {Environment.NewLine}" +
$"- 'have we been to {{restaurant}}?' - Indicates if specific restaurant has been chosen. {Environment.NewLine}" +
$"- 'who's next' - Indicates who has the next selection. {Environment.NewLine}" +
$"- 'recommendation' - Get a random recommendation in the area. {Environment.NewLine}";
}
And now we can update the Post()
method to simply return that response to the user for any unrecognized response by creating another method that will handle formatting that string into something consumable by the bot and then returning it:
private async Task<ResourceResponse> ReplyWithDefaultMessageAsync(Activity activity, ConnectorClient connector)
{
var reply = activity.CreateReply(Messages.DefaultResponseMessage);
return await connector.Conversations.ReplyToActivityAsync(reply);
}
Now this cleans up things a bit, by allowing us to simply call the ReplyWithDefaultMessageAsync()
method:
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
var connector = new ConnectorClient(new Uri(activity.ServiceUrl));
if (activity.Type == ActivityTypes.Message)
{
await ReplyWithDefaultMessageAsync(activity, connector);
}
return Request.CreateResponse(HttpStatusCode.OK);
}
Now if we go tell the bot "Hi!", or really anything, it will respond as expected:
The next step will be to actually start interacting with our Firebase database and pulling some data in. To do this, we will do two things:
- Configure Firebase to Accept Read Requests - By default, read only access is only allowed for authenticated users, but for this we will want to disable that.
- Configure a Client to Make Our Web Request - This will really just involve pointing to our endpoint and reading the JSON content.
Let's start with Firebase, which will just require us to go under the Database > Rules tab within the interface and configure it as follows:
{
"rules": {
".read": "true",
".write": "auth != null"
}
}
NOTE: This isn't generally a good idea, if you are going to publicly expose an API that you are even somewhat concerned about, consider requiring authentication of some sort.
Next, we can wire up a basic HttpClient
class to handle making our requests. Since I'll be hosting this in Azure, I'll store my database endpoint as an environmental variable, but you could easily use a hard-coded string for example purposes. I'll just new this up within my MessagesController
constructor for now:
public class MessagesController : ApiController
{
private HttpClient _client;
public MessagesController()
{
_client = new HttpClient()
{
BaseAddress = new Uri(Environment.GetEnvironmentVariable("DatabaseEndpoint"))
};
}
// Omitted for brevity
}
Now we can put this all together by wiring up a method to handle calling this API endpoint and getting our data. We will start getting a list of everywhere that the group has eaten thus far.
Where Have We Eaten?
Let's build the methods that will use the client pointing to our base endpoint and pull the Restaurants data that we have defined. We can break this into three separate methods for building our messages: one for pulling the raw data and the other for formatting it:
private async Task<string> GetPreviouslyVisitedRestaurantsMessageAsync()
{
try
{
var restaurants = await GetAllVisitedRestaurantsAsync();
var message = new StringBuilder(Messages.RestaurantListingMessage);
foreach (var restaurant in restaurants)
{
message.AppendLine($"- '{restaurant.Location}' on {restaurant.Date.ToString("M/d/yyyy")} ({restaurant.PickedBy})");
}
return message.ToString();
}
catch
{
return Messages.DatabaseAccessIssuesMessage;
}
}
private async Task<Restaurant[]> GetAllVisitedRestaurantsAsync()
{
var restaurants = await _client.GetStringAsync("/Restaurants/.json");
return JsonConvert.DeserializeObject<Restaurant[]>(restaurants);
}
You'll note the use of the /{table}/.json
format that is being appended to the URL. This is explicitly telling Firebase that we want to receive our response as JSON, which will allow us to easily consume and serialize it into the earlier Restaurant
class that we defined.
After we defined these, we will need to go about exposing that to the bot so that it knows how to respond:
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
var connector = new ConnectorClient(new Uri(activity.ServiceUrl));
if (activity.Type == ActivityTypes.Message)
{
var message = activity.Text;
// If the user mentions anything related to the list, give it to them
if (Regex.IsMatch(message, "show|all|list all", RegexOptions.IgnoreCase))
{
await ReplyWithRestaurantListingAsync(activity, connector);
}
else
{
await ReplyWithDefaultMessageAsync(activity, connector);
}
}
return Request.CreateResponse(HttpStatusCode.OK);
}
At this point, we can revisit the bot and we should be able to ask it to tell us where we have eaten previously:
We can now take this bot a step further and make this publicly accessible on the web so that members of the group (or anyone) could go and talk to it. We will do this by revisiting the Bot Framework portal and setting up the Web Chat channel.
Letting the Bot Meet Everyone
By revisiting the Bot Framework portal, you should see several available integration channels that can be used to extend the reach of your bot into other platforms and environments. In this case, we will focus on the Web Chat channel, which will allow the bot to be embedded within web pages.
From the home page for your Bot within the Bot Framework portal, choose the Web Chat channel that appears on the screen:
Once here, you'll be prompted to provide the two keys that were mentioned earlier: MicrosoftAppId
and MicrosoftAppPassword
. Once you enter these, you'll be provided with an embed code that can be used within any web page that looks like this:
<iframe src='https://webchat.botframework.com/embed/ThirdThursdayBot?s=YOUR_SECRET_HERE'></iframe>
Now, you can take this code and revisit your solution in Visual Studio. From there, you can consider embedding the bot itself within the main page that gets hosted (e.g. default.htm
) so that anyone that visits your bot's page can now interact with it:
<h1><-- Bot Name --></h1>
<p><!-- Description --></p>
<iframe src='https://webchat.botframework.com/embed/ThirdThursdayBot?s=YOUR_SECRET_HERE'></iframe>
Or if you simply have a blog like this one, you can just whack that sucker right in here as seen below:
You can also visit this bot at http://thirdthursdaybot.azurewebsites.net if you'd like to interact with it and see what the results of this (and some future blog posts look like).
Coming Soon
You'll notice that the bot itself being hosted has a few other features that weren't covered within this post (but will be addressed in a later one). Namely these:
- Determining whose turn it is.
- Responding to queries about restaurants (e.g. have we been to {x}?)
- Receiving recommendations on restaurants that have not already been visited.
- Integrating with Twilio for SMS / Text Support
Additionally, I'll look into open-sourcing this project as well in the hopes that more folks might start participating in Third Thursday style events with their friends.