Your company started looking for a new platform to create new chat bots and port existing bots and is searching which better suits their needs. One of the requirements is that you'll need to support different platforms (Facebook, Slack, Skype and Webchat). Several chat bots already exists in these platforms using different tools and learned that different platforms support different native features. Moreover, there's a running implementation of the bot in the native platform (e.g. a Facebook bot) which makes you want to make sure using native features is supported.
You came across the Microsoft Bot Framework which support a great variety of channels (platforms), programming languages (C# and Node) and supports both state-of-the-art standard bot features and mechanisms to also take advantage of native features (via ChannelData).
The minimum prerequisites to run this sample are:
- The latest update of Visual Studio 2015. You can download the community version here for free.
- The Bot Framework Emulator. To install the Bot Framework Emulator, download it from here. Please refer to this documentation article to know more about the Bot Framework Emulator.
Many messaging channels provide the ability to attach richer objects. The Bot Framework has the ability to render rich cards as attachments.
The bot will render a Welcome message upon the first message using a HeroCard attachment within the RootDialog.WelcomeMessageAsync
method.
The sample also includes the HeroCardExtensions
class providing methods to ease the creation of rich cards.
private async Task WelcomeMessageAsync(IDialogContext context)
{
var reply = context.MakeMessage();
var options = new[]
{
Resources.RootDialog_Welcome_Orders,
Resources.RootDialog_Welcome_Support
};
reply.AddHeroCard(
Resources.RootDialog_Welcome_Title,
Resources.RootDialog_Welcome_Subtitle,
options,
new[] { "https://placeholdit.imgix.net/~text?txtsize=56&txt=Contoso%20Flowers&w=640&h=330" });
await context.PostAsync(reply);
context.Wait(this.OnOptionSelected);
}
Emulator | Skype | |
---|---|---|
Another example of rich card, is the ReceiptCard which renders differently depending on the messaging channel being supported.
The receipt card is created in the RootDialog.GetReceiptCard
method and is rendered once the bot's user checkouts an order.
private Attachment GetReceiptCard()
{
var order = this.ordersService.RetrieveOrder(this.order.OrderID);
var creditCardOffuscated = order.PaymentDetails.CreditCardNumber.Substring(0, 4) + "-****";
var receiptCard = new ReceiptCard
{
Title = Resources.RootDialog_Receipt_Title,
Facts = new List<Fact> {
new Fact(Resources.RootDialog_Receipt_OrderID, order.OrderID),
new Fact(Resources.RootDialog_Receipt_PaymentMethod, creditCardOffuscated)
},
Items = new List<ReceiptItem>
{
new ReceiptItem(
title: order.FlowerCategoryName,
subtitle: order.Bouquet.Name,
price: order.Bouquet.Price.ToString("C"),
image: new CardImage(order.Bouquet.ImageUrl)),
},
Total = order.Bouquet.Price.ToString("C")
};
return receiptCard.ToAttachment();
}
Emulator | Skype | |
---|---|---|
You can also see a full sample explaining the different types of rich cards in the Rich Cards Bot Sample.
You can send multiple rich card attachments in a single message. On most channels they will be sent as a list of rich cards, but some channels (like Skype and Facebook) can render them as a carousel of rich cards.
Listing categories and product accompanied with a descriptive image is an example of how a Carousel of Cards can be used. The FlowerCategoriesDialog
and the BouquetsDialog
inherits the reusable PagedCarouselDialog
class which showcase how a carousel of very rich cards can be created.
Specifically, the PagedCarouselDialog<T>.ShowProducts
method renders an attachments list using the AttachmentLayoutTypes.Carousel
layout.
protected async Task ShowProducts(IDialogContext context)
{
var reply = context.MakeMessage();
reply.AttachmentLayout = AttachmentLayoutTypes.Carousel;
reply.Attachments = new List<Attachment>();
var productsResult = this.GetCarouselCards(this.pageNumber, this.pageSize);
foreach (HeroCard productCard in productsResult.Cards)
{
reply.Attachments.Add(productCard.ToAttachment());
}
await context.PostAsync(reply);
if (productsResult.TotalCount > this.pageNumber * this.pageSize)
{
await this.ShowMoreOptions(context);
}
}
Emulator | Skype | |
---|---|---|
You can also see a full sample bot sending multiple rich card attachments in a single message using the Carousel layout in the Carousel of Cards Bot Sample.
Dialogs can be composed with other dialogs to maximize reuse, and a dialog context maintains a stack of dialogs active in the conversation.
In this sample, the main flow is implemented in the RootDialog
class and it is composed of several other classes implementing IDialog
.
You can also see a full sample bot showing different kind of dialogs in the Multi-Dialog Bot Sample.
As seen in the two examples above, you can reuse your dialogs in different segments of the bot's flow, or even different bots, and extract them into a library. An example of this is the ContosoFlowers.BotAssets project which includes several reusable dialogs and extension methods.
- Contoso Flowers' Dialogs
- Extensions
Additionally, the Bing Location Control is being used to resolve and validate the shipping and billing addresses, backed by Bing Maps REST services. Take a look here to see how to leverage DI to configure the control, and here to see how to use the DialogFactory to create an instance of the Control's Location Dialog.
Emulator | Skype | |
---|---|---|
Handling a guided conversation like ordering a bouquet of flowers for your loved one can require a lot of effort. In order to simplify building guided conversations the Bot Framework provides a powerful dialog building block known as FormFlow. A FormFlow dialog guides the user through filling in the form; a collection of fields that you want to fill in through a conversation with the user.
In this sample, the Order
class serves as the model that holds the Form values entered by the user. You can see several Prompt
and Pattern
attributes decorating properties which helps customizing the FormFlow behavior.
Included in the class, there's a static BuildOrderForm
method that uses a FormBuilder
to build your form. Follow this approach for maximum flexibility which allow specifying each fields behavior as needed.
public static IForm<Order> BuildOrderForm()
{
return new FormBuilder<Order>()
.Field(nameof(RecipientFirstName))
.Field(nameof(RecipientLastName))
.Field(nameof(RecipientPhoneNumber))
.Field(nameof(Note))
.Field(new FieldReflector<Order>(nameof(UseSavedSenderInfo))
.SetActive(state => state.AskToUseSavedSenderInfo)
.SetNext((value, state) =>
{
var selection = (UseSaveInfoResponse)value;
if (selection == UseSaveInfoResponse.Edit)
{
state.SenderEmail = null;
state.SenderPhoneNumber = null;
return new NextStep(new[] { nameof(SenderEmail) });
}
else
{
return new NextStep();
}
}))
.Field(new FieldReflector<Order>(nameof(SenderEmail))
.SetActive(state => !state.UseSavedSenderInfo.HasValue || state.UseSavedSenderInfo.Value == UseSaveInfoResponse.Edit)
.SetNext(
(value, state) => (state.UseSavedSenderInfo == UseSaveInfoResponse.Edit)
? new NextStep(new[] { nameof(SenderPhoneNumber) })
: new NextStep()))
.Field(nameof(SenderPhoneNumber), state => !state.UseSavedSenderInfo.HasValue || state.UseSavedSenderInfo.Value == UseSaveInfoResponse.Edit)
.Field(nameof(SaveSenderInfo), state => !state.UseSavedSenderInfo.HasValue || state.UseSavedSenderInfo.Value == UseSaveInfoResponse.Edit)
.Build();
}
Finally, is worth mention how this is all combined into an Order FormDialog
called from the main RootDialog.
var orderForm = new FormDialog<Models.Order>(this.order, Models.Order.BuildOrderForm, FormOptions.PromptInStart);
context.Call(orderForm, this.AfterOrderForm);
Emulator | Skype | |
---|---|---|
Once you have a great bot working in a single language, you might want to enable it in other languages. The localization language is determined by the current thread's CurrentUICulture
and CurrentCulture
. By default the culture comes from the Language field of the current message, but you can change that if you wish.
However, you'll notice there's almost no literal strings in the code that reach the user and they were all moved to a Resources
file This is a good practice since you'll probably want to adapt the messages without changing the conversation's flow. An example of this is the previously highlighted WelcomeMessageAsync
method.
private async Task WelcomeMessageAsync(IDialogContext context)
{
var reply = context.MakeMessage();
var options = new[]
{
Resources.RootDialog_Welcome_Orders,
Resources.RootDialog_Welcome_Support
};
reply.AddHeroCard(
Resources.RootDialog_Welcome_Title,
Resources.RootDialog_Welcome_Subtitle,
options,
new[] { "https://placeholdit.imgix.net/~text?txtsize=56&txt=Contoso%20Flowers&w=640&h=330" });
await context.PostAsync(reply);
context.Wait(this.OnOptionSelected);
}
On the other hand, the Bot Framework provides built-in localization for PromptDialog and FormFlow:
The static strings in a form include strings that are generated from the information in your C# class and from the strings you supply as prompts, templates, messages or confirmations. It does not include strings generated from built-in templates since those are already localized. Since many strings are automatically generated, it is not easy to use normal C# resource strings directly.
This sample includes the ContosoFlowers.Models.Order.resx FormFlow resource file for the Orders
form. This file was generated with the supplied rview tool included in the NuGet package.
Bots built using Bot Framework are designed to be stateless so that they can easily be scaled to run across multiple compute nodes. The conversation state (i.e. the stack of active dialogs and each dialog's state) is stored in the Bot State Service provided by the Bot Connector service.
The IDialogContext
has several properties which are useful for tracking state.
Property | Use cases |
---|---|
ConversationData | Remembering context data associated with a conversation. |
PrivateConversationData | Remembering context data associated with a user in a conversation. |
UserData | Remembering context data associated with a user (across all channels and conversations). |
In this sample, the UserData
property is used to store and retrieve several user settings. The best example is the SettingsDialog
which main purpose is to manage this user data.
Emulator | Skype | |
---|---|---|
You can also see a full sample bot tracking context of a conversation in the State API Bot Sample.
Additionally, you'll notice the SettingsDialog
is globally available meaning that the user can type settings
anytime and the settings dialog will be taken on top of the conversation's dialog stack.
The SettingsScorable
makes this action globally available because a piece of middleware inspects every incoming message and gives all IScorable implementations the opportunity to intercept the message and manipulate the conversation stack, interrupting the normal dialog flow.
Emulator | Skype | |
---|---|---|
To ease unit testing of Dialogs and other components the Bot Framework leverages Autofac for Dependency Injection. The top level composition root class Conversation
provides the Autofac IContainer Container
property for registering a resolving components.
Additionally, as a good practice, you can create and register app-specific Autofac Modules. For instance, all dialogs and services as well as the IScorable
implementation are registered in the ContosoFlowersModule
.
Because all components are serialized to the per-user, per-conversation IBotDataBag all classes must be marked with the serializable attribute. The cleanest solution to reference non-serializable services is to resolve the dependency through Autofac and
FiberModule.Key_DoNotSerialize
.
builder.RegisterType<ContosoFlowersDialogFactory>()
.Keyed<IContosoFlowersDialogFactory>(FiberModule.Key_DoNotSerialize)
.AsImplementedInterfaces()
.InstancePerLifetimeScope();
builder.RegisterType<RootDialog>()
.As<IDialog<object>>()
.InstancePerDependency();
builder.RegisterType<SettingsScorable>()
.As<IScorable<double>>()
.InstancePerLifetimeScope();
The exchange of messages between bot and user through a channel (e.g. Facebook Messenger, Skype, Slack) is the primary means of interaction. However, in some scenario the bot is waiting for an event that occurs in an external component.
For example, the passing of time, an external authentication provider (e.g. OAuth scenarios) or an external payment service. In such cases, the ResumptionCookie
has the information necessary to resume the conversation.
In this sample, the user proceed to checkout an order browsing a url provided by the bot. This url includes an encoded version of the ResumptionCookie (using the ResumptionCookie.GZipSerialize
helper method) generated in the RootDialog.BuildCheckoutUrl
method.
private string BuildCheckoutUrl(string orderID)
{
var encodedCookie = this.resumptionCookie.GZipSerialize();
var uriBuilder = new UriBuilder(this.checkoutUriFormat);
var query = HttpUtility.ParseQueryString(uriBuilder.Query);
query["state"] = encodedCookie;
query["orderID"] = orderID;
uriBuilder.Query = query.ToString();
var checkoutUrl = uriBuilder.Uri.ToString();
return checkoutUrl;
}
Once the user browse the checkout page and process the payment, the ResumptionCookie
included in the url is then decoded (using the ResumptionCookie.GZipDeserialize
method) and used to resume the conversation with the bot. You can check the CheckOutController.Index
action calling the Conversation.ResumeAsync
method.
[Route("")]
[HttpPost]
public async Task<ActionResult> Index(string state, string orderId, PaymentDetails paymentDetails)
{
this.ordersService.ConfirmOrder(orderId, paymentDetails);
// Send Receipt
var resumptionCookie = ResumptionCookie.GZipDeserialize(state);
var message = resumptionCookie.GetMessage();
message.Text = orderId;
await Conversation.ResumeAsync(resumptionCookie, message);
return this.RedirectToAction("Completed", new { orderId = orderId });
}
Emulator | Skype | |
---|---|---|
While not covered in this sample, it is important to highlight two features that can help tailoring your bot the your specific needs.
If you want to be able to take advantage of special features or concepts for a channel we provide a way for you to send native metadata to that channel giving you much deeper control over how your bot interacts on a channel. The way you do this is to pass extra properties via the ChannelData property.
You can also see a full sample bot sending native metadata to Facebook using ChannelData in the ChannelData Bot Sample.
One of the key problems in human-computer interactions is the ability of the computer to understand what a person wants, and to find the pieces of information that are relevant to their intent. In the LUIS application, you will bundle together the intents and entities that are important to your task.
You can also see a full sample bot using LuisDialog to integrate with a LUIS.ai application in the LUIS Bot Sample.
To get more information about how to get started in Bot Builder for .NET please review the following resources:
- Dialogs
- Add media attachments to messages
- Add rich card attachments to messages
- Implement channel-specific functionality
- Create a Facebook notification
- Guide conversations with FormFlow
- Manage conversational state
- Autofac
- Add language understanding to a bot
Limitations
The functionality provided by the Bot Framework Activity can be used across many channels. Moreover, some special channel features can be unleashed using the ChannelData property.The Bot Framework does its best to support the reuse of your Bot in as many channels as you want. However, due to the very nature of some of these channels, some features are not fully portable.
The features used in this sample are fully supported in the following channels:
- Skype
- DirectLine
- WebChat
- GroupMe
They are also supported, with some limitations, in the following channels:
- Microsoft Teams (Receipt card not supported)
- Slack
On the other hand, they are not supported and the sample won't work as expected in the following channels:
- Telegram
- SMS
- Kik