Developing a Bot for ParkAround using Microsoft Bot Framework

In this blog post we’ll discuss how we built a Bot for ParkAround using Microsoft Bot Framework and hosted it in Azure platform.

ParkAround is a prominent startup in Greece which allows you to book your place in hundreds of car parks in the cities of Athens/Thessaloniki as well as the airports of Barcelona and Malaga. We worked with ParkAround to build a Bot that allows the user to book a parking spot at the airports of Athens and Thessaloniki. You can currently chat with the Bot on Facebook’s Messenger platform, whereas support for other channels (e.g. Skype) will be rolled out in the next few weeks.

Bot has the name of “Mitsaras, the parking assistant” (“Mitsaras” being the folk/friendly name for “Dimitris”) and you can chat with it here: https://www.messenger.com/t/parkaroundbotBeware, bot currently uses Greek language only since it targets Greek audience for now. So, don’t get confused if it’s all Greek to you!

To develop the bot, we used Microsoft’s Bot Framework which allows you to create a bot that will interact with various conversation channels, such as Messenger, Skype, Slack and other services. Bot Framework supports a REST API and has two SDKs, one for .NET and one for Node.js. As most Microsoft SDKs nowadays, both of them are open source. If you aren’t acquainted with Bot Framework SDK, please take a look at the extensive documentation in order to better understand the code segments listed below. Also, ParkAround is a BizSpark startup, so we naturally chose Azure App Service PaaS platform to host the bot, so we can easily scale up/out if needed.

Last but definitely not least, before we continue with bot’s internals, we should mention that this work is a collaboration between myself, my colleague Sophia Chanialaki and ParkAround’s CEO, John Katsiotis.

Bot request/response data flow

So, how does the bot accomplish its goal?

Bot follows the steps listed below, in an effort to acquire the necessary data from the user

  1. Bot displays a welcome message to the user, as soon as she speaks to it
    1
  2. User selects the airport location she wishes to park her car
    2
  3. User selects the date/time she will arrive at the airport
    3
  4. User selects the date/time she will come back from her trip and pick the car
    4
  5. Bot searches in the available parking lots to find an empty booking. If successful, it continues to next step. Otherwise, it returns to step 1. User is presented with a carousel displaying the available parking lots along with their prices and the distance from the airport.
  6. Now, user has to enter the necessary details for the booking to be performed. Bot starts by asking user to enter their full name, suggesting the one taken by the conversation channel used (e.g. Facebook given name)
  7. Bot sequentially asks for user’s email, phone and car’s license plate7
  8. Bot asks for a final confirmation before it performs the actual booking
    8
  9. If user confirms, the bot calls the ParkAround web service to perform the actual booking and user receives the necessary details in her e-mail address. Bot presents a receipt to the user
    1

Steps can be represented with the following diagram

capture
Diagram of the bot’s input flow (questions it asks the user)

Bot core code

In the beginning, we built a small prototype using Bot SDK’s FormFlow. FormFlow is a great tool that you can use to develop a bot using minor effort. Specifically, you can “feed” it a C# class and it can generate required request (input) questions. FormFlow searches your class for public fields which can be either a simple data type or enum and will build a sequential series of questions that the bot will ask the user. Add to this the fact that you can add C# attributes to filter and validate the answers given by the user and you’ve got yourself a great mechanism which you can use to build a powerful bot with minimal effort and few lines of code.

However, we needed some customisations that were not supported by FormFlow. For example, on the date question we needed the user to reply either with a valid date (e.g. 30/10/2016) or with a string (e.g. Today, Tomorrow). That proved rather tricky to do with FormFlow. Consequently, we quickly opted to revise our architecture and used the more powerful SDK’s Dialogs instead.

We started by creating a simple dialog to hold all request and response messages. This worked but, as we described, the amount of input steps required was big enough. So, we again quickly decided to create custom dialogs for each one of the described steps. Each dialog would ask the user a question, get the response, filter/validate it and either finish its operation (if the response was OK) or ask the user again (if the response was not OK). And what a better way to “chain” all these dialogs together than a dialog chain, already supported by the framework? Without further ado, here’s the Bot Framework Chain that’s powering our bot.


public class BookParkingDialogChain : IDialog<object>
{
private string selectedName, selectedEmail, selectedPhoneNumber;
private string selectedLicensePlate;
private ParkingAreaDetails selectedParkingLocationDetails;
private DateTime arrivalDateTime, departureDateTime;
private ParkingLotDetails selectedParkingLot;
ParkAroundBot.Data.ReserveTravelResponse result;
private bool finalConfirmation;
public async Task StartAsync(IDialogContext context)
{
context.Wait<string>(conversationStarted);
}
private async Task conversationStarted(IDialogContext context, IAwaitable<string> s)
{
await context.PostAsync(StringMessages.Welcome);
var dialog = CreateChain();
context.Call(dialog, ProcessFinished);
}
private IDialog<bool> CreateChain()
{
var dialog = Chain.From(() => new ParkingAreaSelectionDialog())
.ContinueWith<ParkingAreaDetails, DateTime>
(async (ctx, parkinglocationdetails) =>
{
selectedParkingLocationDetails = await parkinglocationdetails;
return new DateTimeInputDialog(null, selectedParkingLocationDetails);
})
.ContinueWith<DateTime, DateTime>
(async (ctx, dt) =>
{
arrivalDateTime = await dt;
return new DateTimeInputDialog(arrivalDateTime, selectedParkingLocationDetails);
})
.ContinueWith<DateTime, ParkingLotDetails>
(async (ctx, dt) =>
{
departureDateTime = await dt;
return new ParkingLotSelectionDialog
(selectedParkingLocationDetails, arrivalDateTime, departureDateTime);
})
.LoopWhile(async (ctx, parkingLot) =>
{
if ((await parkingLot) == null)
return true;
else
return false;
})
.ContinueWith<ParkingLotDetails, string>
(async (ctx, parkingLot) =>
{
selectedParkingLot = await parkingLot;
return new FullnameInputDialog();
})
.ContinueWith<string, string>
(async (ctx, username) =>
{
selectedName = await username;
var emailDialog = CustomTextInputDialog.CreateCustomTextInputDialog
(StringMessages.EnterEmail, StringMessages.EnterEmail, new EmailCustomInputValidator());
return emailDialog;
})
.ContinueWith<string, string>
(async (ctx, email) =>
{
selectedEmail = await email;
var phoneDialog = CustomTextInputDialog.CreateCustomTextInputDialog
(StringMessages.PhoneNumber, StringMessages.PhoneNumber, new PhoneCustomInputValidator());
return phoneDialog;
})
.ContinueWith<string, string>
(async (ctx, phoneNumber) =>
{
selectedPhoneNumber = await phoneNumber;
var licensePlateDialog = CustomTextInputDialog.CreateCustomTextInputDialog
(StringMessages.EnterLicensePlate, StringMessages.EnterCorrectLicensePlate, new TextCustomInputValidator(2, 10));
return licensePlateDialog;
})
.ContinueWith<string, bool>
(async (ctx, licenseplate) =>
{
selectedLicensePlate = await licenseplate;
var confirmationDialog = new ConfirmBookingDialog(selectedParkingLot.parkingLotName, selectedParkingLot.Price, selectedEmail, selectedParkingLot.Image, arrivalDateTime.ToShortDateString(), departureDateTime.ToShortDateString());
return confirmationDialog;
}).ContinueWith<bool, bool>
(async (ctx, confirmation) =>
{
finalConfirmation = await confirmation;
return Chain.Return(finalConfirmation);
});
return dialog;
}
}

In the beginning we’re creating some private fields. These will be used to hold the responses given by the user. Then, the entire chain is created in the CreateChain method. There the flow of request/response to and from the user is described. At each step, we call a specific dialog, get the result given from the user, save it in one of the private fields and continue with the next dialog in the chain. You may notice that we are using a LoopWhile method that is not present in the official Bot Framework. Thing is, we needed a loop in the chain at this point (if you don’t find any available parking lots in the time frame provided, start over). At the time we wrote that piece of code the Bot Framework didn’t have one, so we wrote one ourselves. However, there is such a method now (and we of course recommend using it).

Take a look at my LoopWhileDialog class


using Microsoft.Bot.Builder.Dialogs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
namespace ParkAroundBot.CustomDialog
{
public static class Extensions
{
/// <summary>
/// Loop the <see cref="IDialog{T}"/> while <see cref="whileFunc"/> returns true.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="antecedent"></param>
/// <param name="whileFunc"></param>
/// <returns></returns>
public static IDialog<T> LoopWhile<T>(this IDialog<T> antecedent, Func<IBotContext,IAwaitable<T>, Task<bool>> whileFunc)
{
return new LoopWhileDialog<T>(antecedent, whileFunc);
}
}
[Serializable]
public sealed class LoopWhileDialog<T> : IDialog<T>
{
public readonly Func<IBotContext, IAwaitable<T>, Task<bool>> WhileFunc;
public readonly IDialog<T> Antecedent;
public LoopWhileDialog(IDialog<T> antecedent, Func<IBotContext, IAwaitable<T>, Task<bool>> whileFunc)
{
this.Antecedent = antecedent;
this.WhileFunc = whileFunc;
}
async Task IDialog<T>.StartAsync(IDialogContext context)
{
context.Call<T>(this.Antecedent, ResumeAsync);
}
private async Task ResumeAsync(IDialogContext context, IAwaitable<T> result)
{
if (await WhileFunc(context, result))
context.Call<T>(this.Antecedent, ResumeAsync);
else
context.Done(await result);
}
}
}

Let’s proceed in seeing some of the code in the dialogs used by our bot.

CustomTextInputDialog

We needed a simple dialog that would ask the user a question, get an answer, validate it and either return success or prompt the user for a correct one. With these, requirements, we build the CustomTextInputDialog class


public sealed class CustomTextInputDialog : IDialog<string>
{
private CustomTextInputDialog()
{ }
public string InputPrompt { get; private set; }
public string WrongInputPrompt { get; private set; }
public ICustomInputValidator Validator { get; private set; }
public static CustomTextInputDialog CreateCustomTextInputDialog
(string inputPrompt, string wrongInputPrompt, ICustomInputValidator validator)
{
return new CustomTextInputDialog()
{ InputPrompt = inputPrompt, WrongInputPrompt = wrongInputPrompt, Validator = validator };
}
public async Task StartAsync(IDialogContext context)
{
await context.PostAsync(InputPrompt);
context.Wait(InputGiven);
}
public async Task InputGiven(IDialogContext context, IAwaitable<IMessageActivity> argument)
{
var message = await argument;
string msg = message.Text.Trim();
if (!Validator.IsValid(msg))
{
await context.PostAsync(WrongInputPrompt);
context.Wait(InputGiven);
}
else
context.Done(msg);
}
}
public interface ICustomInputValidator
{
bool IsValid(string input);
}
[Serializable()]
public class PhoneCustomInputValidator: ICustomInputValidator
{
public bool IsValid(string input)
{
input = input.Replace(" ", "");
input = input.Replace("+", "");
input = input.Replace("(", "");
input = input.Replace(")", "");
if (input.Length > 9)
{
long number1 = 0;
bool canConvert = long.TryParse(input, out number1);
return canConvert;
}
return false;
}
}
[Serializable()]
public class EmailCustomInputValidator : ICustomInputValidator
{
public bool IsValid(string input)
{
var foo = new EmailAddressAttribute();
return foo.IsValid(input);
}
}
[Serializable()]
public class TextCustomInputValidator : ICustomInputValidator
{
private int MinLength, MaxLength;
public TextCustomInputValidator(int minLength, int maxLength)
{
MinLength = minLength;
MaxLength = maxLength;
}
public bool IsValid(string input)
{
return input.Length >= MinLength && input.Length <= MaxLength;
}
}

Based on this class, we built special classes to validate specific user details, such as PhoneCustomInputValidator, EmailCustomInputValidator and a TextCustomInputValidator (which basically checks whether the input string is within a specified length). Classes have to implement the simple ICustomValidator interface.

DateTimeInputDialog

We needed a class that would get date+time details of user’s arrival and departure from the parking lot. We developed the DateTimeInputDialog class for this purpose


[Serializable]
public class DateTimeInputDialog : IDialog<DateTime>
{
/// <summary>
/// If it is null, we're looking for DateTimeFrom
/// Else, we're looking for DateTimeTo
/// </summary>
private DateTime? inputDateTime;
private ParkingAreaDetails selectedParkingLocationDetails;
string selectedDateString, selectedTimeString;
DateTime selectedDateTime;
public DateTimeInputDialog(DateTime? inputdt, ParkingAreaDetails selectedPLD)
{
inputDateTime = inputdt;
selectedParkingLocationDetails = selectedPLD;
}
public async Task StartAsync(IDialogContext context)
{
await PostPromptDateInputMessageAsync(context);
context.Wait(DateSelected);
}
/// <summary>
/// Parses user's input. If it's "today" or "tomorrow" then we're good to go. Otherwise, it tries to parse the date. If all fail, it asks again for input.
/// </summary>
/// <param name="context"></param>
/// <param name="argument"></param>
/// <returns></returns>
public virtual async Task DateSelected(IDialogContext context, IAwaitable<IMessageActivity> argument)
{
bool success = false;
var message = await argument;
if (message.Text.CompareInput(StringMessages.Today)) //"today" as input
{
success = true;
var date = DateTime.UtcNow.AddHours(selectedParkingLocationDetails.UtcTimeDiff).ToString("dd/MM/yyy");
selectedDateString = date;
}
else if (message.Text.CompareInput(StringMessages.Tomorrow))//"tomorrow" as input
{
success = true;
var date = DateTime.UtcNow.Date.AddDays(1).AddHours(selectedParkingLocationDetails.UtcTimeDiff).ToString("dd/MM/yyy");
selectedDateString = date;
}
else if (Utilities.DateRegex.IsMatch(message.Text))
{
var selectedDate = Utilities.ConvertDateTimeStringToDate(message.Text, null);
if (selectedDate < DateTime.UtcNow.AddHours(selectedParkingLocationDetails.UtcTimeDiff).Date)
{
success = false;
await context.PostAsync(StringMessages.SelectedDateLessThanNow);
}
else
{
success = true;
selectedDateString = message.Text;
}
}
else //no valid input
{
success = false;
await context.PostAsync(StringMessages.WrongInputDate);
}
if (success)
{
await PostPromptTimeInputMessageAsync(context);
}
else
{
await PostPromptDateInputMessageAsync(context);
context.Wait(DateSelected);
}
}
public virtual async Task TimeSelected(IDialogContext context, IAwaitable<IMessageActivity> argument)
{
var message = await argument;
if (Utilities.TimeRegex.IsMatch(message.Text))
{
selectedTimeString = message.Text;
selectedDateTime = Utilities.ConvertDateTimeStringToDate(selectedDateString, selectedTimeString);
//check if selected date is in the past
if (selectedDateTime < DateTime.Now)
{
await context.PostAsync(StringMessages.SelectedTimeLessThanNow);
context.Wait(TimeSelected);
}
//we're checking the "to" date and it's less than "from"
else if (inputDateTime.HasValue && selectedDateTime < inputDateTime)
{
await context.PostAsync(StringMessages.DepartureDateLessThanArrival);
context.Wait(DateSelected);
}
else
{
context.Done(selectedDateTime);
}
}
else
{
await context.PostAsync(StringMessages.WrongInputTime);
context.Wait(TimeSelected);
}
}
private async Task PostPromptTimeInputMessageAsync(IDialogContext context)
{
string msg = inputDateTime.HasValue ? StringMessages.SelectTimeTo : StringMessages.SelectTimeFrom;
await context.PostAsync(msg);
context.Wait(TimeSelected);
}
private async Task PostPromptDateInputMessageAsync(IDialogContext context)
{
string msg = inputDateTime.HasValue ? StringMessages.SelectDateToList : StringMessages.SelectDateFromList;
var message = context.MakeMessage();
message.AttachmentLayout = AttachmentLayoutTypes.List;
var attachments = new List<Attachment>();
//we're looking for the arrival date
if (inputDateTime == null)
{
var actions = new List<CardAction>()
{
Utilities.CreateCardAction(StringMessages.Today,StringMessages.Today),
Utilities.CreateCardAction(StringMessages.Tomorrow,StringMessages.Tomorrow),
};
attachments.Add(Utilities.CreateHeroCardAttachment(msg, StringMessages.UpcomingDates, null, null, actions));
}
else //we're looking for the departure date
//add 3 and 5 days to the arrival date and suggest them to the user
{
var after3days = inputDateTime.Value.Date.AddDays(3).ToString("dd/MM/yyyy");
var after5days = inputDateTime.Value.Date.AddDays(5).ToString("dd/MM/yyyy");
var actions = new List<CardAction>()
{
Utilities.CreateCardAction(after3days,after3days),
Utilities.CreateCardAction(after5days,after5days),
};
attachments.Add(Utilities.CreateHeroCardAttachment(msg, StringMessages.UpcomingDatesReturn, null, null, actions));
}
message.Attachments = attachments;
await context.PostAsync(message);
}
}

This dialog allows user to either select a specific date (today/tomorrow for arrival, after 3 days/after 5 days for departure) via a HeroCard or input a specific date and time. Moreover, it performs various validation checks, e.g. making sure that dateTimeTo > dateTimeFrom. Attention is also being paid to the date/time’s input, regarding time difference between Greece (where the parking space is) and Azure’s Europe Western Europe data center, where the bot code resides. For instance, if the user wishes to arrive at the parking “today”, we need to take into account what “today” means for the Azure data center. We know that we’re deploying to Azure’s Western Europe data center, so we’re calculating the time difference between Western Europe and Greece, taking into account DaylightSavingTime. Maybe (well, almost certainly!) there’s a better way to do this, but if it ain’t broke, why fix it :) ?

FullNameInputDialog

FullNameInputDialog is pretty straight forward, they key here is that we’re using the Message.Recipient.Name property to get the user’s name from the conversation channel.


[Serializable]
public class FullnameInputDialog : IDialog<string>
{
public async Task StartAsync(IDialogContext context)
{
var msg = context.MakeMessage();
var attachments = new List<Attachment>();
msg.AttachmentLayout = AttachmentLayoutTypes.List;
var actions = new List<CardAction>()
{
Utilities.CreateCardAction(StringMessages.YesButton,StringMessages.YesButton)
};
attachments.Add(Utilities.CreateHeroCardAttachment($"{StringMessages.EnterUserName.Replace("$UserName$", msg.Recipient.Name)}", null, $"{StringMessages.EnterCustomUserName}", null, actions));
msg.Attachments = attachments;
await context.PostAsync(msg);
context.Wait(UsernameSelected);
}
public virtual async Task UsernameSelected(IDialogContext context, IAwaitable<IMessageActivity> argument)
{
var message = await argument;
if (message.Text == StringMessages.YesButton)
{
context.Done(context.MakeMessage().Recipient.Name);
}
else
{
if (!string.IsNullOrEmpty(message.Text) && message.Text.Trim().Length >= 5 && message.Text.Trim().Length <= 30)
{
context.Done(message.Text);
}
else
{
await context.PostAsync(StringMessages.EnterUserNameQuestion);
context.Wait(UsernameSelected);
}
}
}
}

Showing a receipt to the user

As soon as the bot gathers all required data from the user, it asks for a final confirmation. If the user issues it, then bot calls ParkAround’s internal API to perform the booking. Afterwards, it has to deliver a receipt to the user. ReceiptCard class is an extendable class (via the Facts property) which we use to deliver a Facebook Messenger-formatted receipt to the user.


private async Task ProcessFinished(IDialogContext ctx, IAwaitable<bool> confirmation)
{
bool userConfirmed = await confirmation;
if (userConfirmed == false)
{
ctx.Done("finished");
return;
}
result = await
ReserveTravelUtility.ReserveTravelAsync(selectedEmail, selectedName, selectedLicensePlate,
selectedParkingLocationDetails.Name, selectedParkingLot.parkingLotID, arrivalDateTime, departureDateTime,
selectedParkingLot.Price, selectedParkingLot.PricelistId);
if (result.Success)
{
var response = result.Result;
var imessageactivity = ctx.MakeMessage();
var messageactivity = imessageactivity as Activity;
var replyToConversation = messageactivity;
replyToConversation.Type = "message";
replyToConversation.Recipient = messageactivity.From;
List<CardImage> cardImages = new List<CardImage>();
cardImages.Add(new CardImage(url: selectedParkingLot.Image));
ReceiptItem lineItem = new ReceiptItem()
{
Title = result.Result.Booking.ParkingLotOperator,
Subtitle = result.Result.Booking.ParkingLotAddress,
Text = result.Result.Booking.Email,
Image = new CardImage(url: selectedParkingLot.Image),
Price = result.Result.Booking.PriceNumber.ToString(),
Quantity = "1",
Tap = null
};
List<ReceiptItem> receiptList = new List<ReceiptItem>();
receiptList.Add(lineItem);
//https://developers.facebook.com/docs/messenger-platform/send-api-reference/receipt-template
ReceiptCard plCard = new ReceiptCard()
{
Title = "I'm a receipt card",
Items = receiptList,
Total = result.Result.Booking.PriceNumber.ToString(),
Tax = "0",
Facts = new List<Fact>(){
new Fact("payment_method","Cash"),
new Fact("currency","EUR"),
new Fact("total_tax","01"),
new Fact("address:street_1", selectedEmail),
new Fact("order_number",result.Result.Booking.Code.ToString()),
new Fact("currency","EUR"),
new Fact("order_url",result.Message ?? "http://www.parkaround.gr/")},
};
var msg=ctx.MakeMessage();
Attachment plAttachment = plCard.ToAttachment();
replyToConversation.Attachments = new List<Attachment>();
replyToConversation.Attachments.Add(plAttachment);
replyToConversation.Recipient.Name = msg.Recipient.Name;
await ctx.PostAsync(StringMessages.ConfirmedReservation);
await ctx.PostAsync(replyToConversation);
}
else
{
await ctx.PostAsync(result.Message);
}
ctx.Done("Finished");
}

Localization

ASP.NET localization is used in the (likely) event that we’d like our bot to support more languages in the near future. In the aforementioned pieces of code, you’ll find many references to a StringMessages class. This is the class we’re using in our resource files and holds our localised string values. It’s considered a best practice to use these in every project since there’s always a chance that a new language will be required to add in the project in order to target a new market.

Bot deployment to Azure

You may ask how we deploy the bot. Well, we could use Build/Deploy from Visual Studio but we liked something better. Since ParkAround uses BitBucket as a source code repository, we chose to use App Service continuous deployment support to deploy bot’s to Azure. Upon publishing to a specific repository, App Service grabs the code and deploys it.

Nice, where can I find more bots? How can I register mine and connect it to conversation channels?

Feel free to browse the Bot Framework Bot Directory here and register a new bot you have developed here. There, you can also find documentation guidance on how to connect it to various conversation channels. Moreover, if you need natural language processing for your bot, LUIS is your friend.

Thanks for reading, hope it helps!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s