​Building a Conversational Booking Bot with Acuity Scheduling

Lego Party is my side hustle — Lego-themed entertainment like parties, classes, boozy Lego building, therapy, you name it! My last article took this business into the 21st Century (and let me stop taking calls during my day job) when we created an online class calendar with Acuity Scheduling.

Folks find businesses online. That's hardly news, but more and more it isn't enough to just be online. Businesses need to meet the customer on their own turf. Today Lego Party is making that next step, and bringing online scheduling directly to people on Facebook using Acuity Scheduling's appointment scheduling API. We'll also need a conversational AI platform such as Init.ai, Wit.ai and api.ai to name a few. For this project, I've selected Init.ai's conversational API because of their nice SDK and straight forward Facebook Messenger connection.

Overview

We'll be building a conversational chat bot for Lego Party's Facebook page to handle the two most common scheduling tasks for clients:

Table of Contents

Booking new classes

Checking their booked classes

We'll need both an Acuity Scheduling account and an Init.ai account (no credit cards required!). Acuity Scheduling is an online appointment scheduler with all the bells and whistles Lego Party needs, as well as a developer friendly scheduling API for checking upcoming classes and creating new bookings. Init.ai is a developer platform for building conversational apps powered by Natural Language Processing, with direct integration to Facebook Messenger.

Init.ai

Init.ai is in a free beta — signup only requires a github account. After signing up, create a new Project for Class Bookings. The complexity of dealing with the Init.ai is abstracted away nicely by their SDK so I won't focus on that here. Instead, there are a couple key concepts to keep in mind for the implementation:

Training Conversations Example conversations used to define intents and entities and build a model.

Intents Meanings of particular messages, such as a greeting or providing a particular piece of data.

Entities Important words within a message, extracted as data and a corresponding type.

Models Programs built by Init.ai from Training Conversations mapping input language to intents and entities, and vice-versa.

The first step in a new project is to create some training language — the more the better! Init.ai has a simple markup language for the task. Here's an example:

user> What time is my class?
* check
service> What is your e-mail address?
* prompt/email
user> [legopartytime@example.com](email/email)
* provide/email
service> You do not have any upcoming classes scheduled.
* upcoming/none

A user kicks off a conversation, and the service makes a reply. An intent, marked with an asterisk, follows each message, eg. * check. And entities, marked with brackets and followed by it's optional type and a name in parentheses, are defined inside messages eg. [legopartytime@example.com](email/email).

For this project, you can find example training conversations in the github project. Add those to your new Init.ai project and Init.ai will automatically populate a list of intents and entities for you. Then just click "Train your model" in the main menu and Init.ai will build the model to power the application. That'll take a few minutes, but we can carry on in the meantime!

Finally under Settings, connect a messanging plugin. Init.ai has a built in "test" messenger you can find in the bottom right-hand corner of your Init.ai console. For this project we'll use Facebook Messenger, which supports a couple extra features that the test messenger doesn't. First you'll need a Facebook page — you can create a new one here for testing — then enable the Facebook Messenger connection.

For the rest of this project you'll interact with your chatbot through the Facebook Messenger. As soon as your Facebook page is connected, we'll start the server and build our application.

The Application

Now we're ready to dive into the code. First, we'll start up the server.

Starting The Server

We'll be starting with a sample project Init.ai has put together. Clone that repository to get started:

git clone git@github.com:init-ai/sample-logic-server.git

Now install base modules, and the couple additional modules we'll be using:

Our dev server will start a "webhook" tunnel to receive messages from Init.ai:

Just copy and paste that webhook into Settings under Webhooks in your Init.ai project. Note: Each time you restart the dev server, you'll need to update that webhook URL in your project settings!

Application Code

This sample application contains a bit of boilerplate code that's worth checking out, but everything we'll need to edit is in a single file: src/runLogic.js. Code in this module will be executed each time we receive an event (eg. a message) from Init.ai through the webhook tunnel started by the server.

First things first, include the modules and a bit of config that we'll be working with:

A lot of our service's intents are asynchronous, and will gather data from Acuity's API — Init.ai's SDK is built to help with this type of asynchronous task. In the promise body, create instances of the Acuity and Init.ai SDKs:

Each edit we make to this file triggers the development server to reload. Our Acuity credentials are already in the server's environment from when we started the server, so we're good to go! Now we can write the logic for the two conversation tasks:

Booking new classes

Checking their booked classes

Conversation Logic

Conversations in Init.ai are controlled by a flow. Tasks for a particular conversation, such as "book a class" or "check bookings", are called streams. Each stream can have multiple steps such as getEmailAddress. And each step usually corresponds to a pair of question-answer intents, processing the entities from a message to store data, request new data, etc.

All of this is set up in the client.runFlow method, the main entry point for our Init.ai conversation logic. Add this down to the bottom of src/runLogic.js and then we'll fill in the steps above it:

// Set up the logic for our flow.
//
// We have two separate conversation streams: the default stream for
// booking a new class, and a separate stream to get current bookings.
// Conversations default to the bookClass stream, unless we receive a
// 'check' intent. Then we'll kick off the getBookings stream.
client.runFlow({
classifications: {
'check': 'getBookings'
},
streams: {
main: 'bookClass',
bookClass: [getAppointmentType, getDatetime, getName, getEmail, bookAppointment],
getBookings: [getEmail, getAppointments]
}
});

Our two conversation types, or streams, are bookClass and getBookings. Most people will book a class before they check their bookings, so we define that as the default stream using the main attribute. The classification attribute lets us map other intents to specific streams. In our case, we'll map the check intent to the getBookings stream.

Step 1: getAppointmentType

Now we'll define our first step: getAppointmentType. Add this step above the call to runFlow:

Each step has a few different pieces. In our application, we'll be using extractInfo, satisfied and prompt. The extractInfo method takes any entities and other data extracted from a message and decides what to do with them. Irregardless of where we are in a conversation, extractInfo is run for each step when any message is received. Chatters can provide info in an unexpected order, or provide info for multiple steps all at once, but each step should only concern itself with that step's entities.

"Hello. My name is Inigo Montoya. You killed my father. Prepare to die."

Execution continues and runFlow decides which stream we're in. Steps for the current stream are executed in order, and satisfied is called. If the step has everything it needs and can be considered complete, satisfied should return true. For the first step that is not satisfied, the prompt method is called and execution finishes.

In our first step, we're extracting an appointment type from the response (hopefully!) —

client.getPostbackData() returns structured data from pre-defined replies in a prompt — we'll cover that in a minute. If we've received an appointment type ID, store it in the conversation state, akin to a session in a web application.

This step is considered satisfied once we have an appointmentTypeID stored.

Our appointment type prompt creates a button for each class which displays the class name, and has the appointmentTypeID stored. When the user selects a button, we'll receive a reply with that data from that getPostbackData method called in extractInfo.

The service's reply is defined with client.addResponseWithReplies, sending the prompt/typeintent and the class buttons:

client.addResponseWithReplies('prompt/type', null, replies);

Last of all client.done(); is called. We made an asynchronous request to the Acuity API. client.done() resolves the promise, and the service ships the response back to the user.

Step 2: getDatetime

Our next step is to pick a particular class session. Similar to appointment types, we'll grab available class sessions from the Acuity API and provide a list of convenient buttons to choose from. We'll extract the session datetime from the postback data and satisfy the step once the datetime is saved:

Step 3: getEmail

Unlike the previous two steps which extract info from the user's button choice, getEmail grabs the email entity directly from the client's message using getFirstEntityWithRole. This step is also shared — it is used in both the bookClass stream and the getBookings stream.

The prompt for this step sets an expectation if we're not in the default stream:

client.expect(client.getStreamName(), ['provide/email']);

Since getEmail is shared between multiple streams, that serves as a hint that reply should provide an e-mail address and should be part of the current stream (eg. getBookings).

Step 4: getName

Now we're ready to collect the user's name. Another benefit of using the Facebook Messenger connection is that it's aware of the user's Facebook profile, including their name. We can grab that with client.getMessagePart().sender and automatically store it to the conversation state.

Just in case it's not available, our training data contains an intent to prompt the user for their name.

Step 5: bookAppointment

Finally we've got what we need to book an appointment:

client name and e-mail address

the class to book

and the date and time.

This step won't contain an extractInfo() step since we're not looking for anything else from the user and the satisfied() method returns false since it's the last step. To book the class, the prompt() method will call Acuity's post appointments API with the client info we've been collecting in the conversation state:

Last, we'll send a response to the client letting them know the appointment is confirmed. The second argument for addResponse is a map of entities for the intent to include in the message to the client. In this case, we'll want to echo the class name and time back to the client in the confirmation message:

With these steps implemented, you'll be able to interact with the booking bot through Facebook Messenger and schedule an appointment: https://www.messenger.com/

Checking Appointments

Once a couple appointments are scheduled, it's time to implement the getBookingsstream to check existing bookings. To keep things simple we'll look up a user's schedule by their e-mail address, reusing the getEmail step. After we have a client's email, there's only one more step: getAppointments to provide the user with their upcoming schedule.

We'll use Acuity's get appointments API along with two parameters: email and minDate, set to now:

If there are any upcoming appointments matching the email address, we'll format them into a list for the response. First, sort them in chronological order then format them into a list with one session on each line. Similar to the confirmation step in the bookClass stream, we'll provide that data for the response entities:

Just before calling client.done() we'll clear the expected stream with client.expect(null). This resets the hint set in getEmail, allowing the client to enter a different stream such as booking another class after checking their schedule. Here's the complete listing for our final step:

Conclusion

Well, this article didn't write itself! But Conversational AI has come a long way, and today is more dev-friendly than ever. Creating a bot that performs well for the two tasks I gave it — that's not bad for an afternoon!

Additional training data helping the model handle unexpected input and additional API cases such as no availability for the month.

Expanding the bot into other common tasks, such as canceling or rescheduling a booking.

Plugging a conversational interface into existing APIs such as Acuity Scheduling is already a practical way to bring useful conversational functionality to other apps such as Facebook Messenger. From here, it'll only get better!