Table of Contents generated with DocToc
- Introduction
- Get started
- Bot Concepts
- Features of the HackfestBotBase
- Set up Azure Environment
- Publish
- Having issues?
Microsoft has a barebones C# starter project for bots, however it can require some plumbing to get running properly. By all means, if you wish to use one of those example projects, you can. For the purposes of the Hackfest, I have extracted patterns and helpers developed and used in a very recent bot.
I have also added a conversation to demonstrate how a conversation may flow, employing some of the helpers included in the HackfestBotBase.
The purpose is to identify the users name and persist it. If the name is already saved, the user is not asked and it loads from memory. It then prompts the user to type a phrase, provides suggested responses, and uses the response to determine the next step. This demo implemented in dialogs/DemoDialog.cs
of the HackfestBotBase incorporates all of the bot concepts and features described before, and serves as an example. You can experiment with it here.
- Visual Studio 2017 (optional: Azure SDK)
- Azure Bot Emulator (v3.5.35) (Install Instructions)
- Azure subscription (Sign up for a trial here)
- Access the code on the VSO project (https://intergen.visualstudio.com/FHF%20Bot%20Hackfest/Attendees/_git/HackfestBotBase). Fork the HackfestBotBase repository for your team and start hacking! This repository needs to be kept private as it is internal IP. For Auckland, the MA developer in your team will have admin access and can add you as a member. Alternatively, get the code from here
- Join the Hackfest group on Yammer. It will be the primary channel to post questions, suggestions, or to show off ideas. These grants visibility to all tech leads to pitch in when appropriate.
- Microsoft Edge (use other browsers at your own risk, they can give issues in Azure or while running the demo)
- Ensure you have all prerequisites above.
- Extract and open the Hackfest bot solution.
- Right click on the solution > Restore nuget packages.
- Build solution.
- Start debugging the API project (F5).
- Open the Bot Emulator.
- Navigate to http://localhost:3978/api/messages and press connect.
One of the fastest ways to get something working is looking through examples and making changes. Play around with the demo project, using the docs below and on MSDN to supplement.
- Run the Hackfest bot solution.
- Open the bot emulator.
- Navigate to http://localhost:3978/api/messages and press connect.
- If successful, you should see 200 in the bot emulator logs.
- SAY HI!
This section will primarily link to the Bot Service and Bot Builder SDK documentation on MSDN as the main source of information. At the very least, I suggest reading through the following short documents to understand the key concepts.
Use Bot Society to design and visually 'see' your conversation before you start building, if you wish. It simplifies the building process significantly.
Starting the bot with an open-ended question such as "How can I help you?" is generally not recommended. If your bot has a hundred different things it can do, chances are users won’t be able to guess most of them. Your bot didn’t tell them what it can do, so how can they possibly know? Menus provide a simple solution to that problem.
Dialogs enable the bot developer to logically separate areas of bot functionality and guide conversation flow.
When one dialog invokes another, the Bot Builder adds the new dialog to the top of the dialog stack. The dialog that is on top of the stack is in control of the conversation. Every new message sent by the user will be subject to processing by that dialog until it either closes or redirects to another dialog. When a dialog closes, it's removed from the stack, and the previous dialog in the stack assumes control of the conversation.
When a dialog is invoked, it takes control of the conversation flow. Every new message will be subject to processing by that dialog until it either closes or redirects to another dialog.
In C#, you can use context.Wait()
to specify the callback to invoke the next time the user sends a message. To close a dialog and remove it from the stack (thereby sending the user back to the prior dialog in the stack), use context.Done()
. You must end every dialog method with context.Wait()
, context.Fail()
, context.Done()
, or some redirection directive such as context.Forward()
or context.Call()
. A dialog method that does not end with one of these will result in an error (because the framework does not know what action to take the next time the user sends a message).
This will allow you to see all the different types of cards that can be attached to messages to add rich data to your conversation.
- MAKE BOTS SMARTER with Cognitive Services
- Bot scenarios
- BotBuilder-Location
- C# samples
All of the features below, posting and receiving messages, and handling basic conversation flow, are implemented in the DemoDialog.cs
example class. When you first run the project, this is the dialog that will power the conversation (as configured in MessageControler.cs
).
This project is built on top of the base bot project by Microsoft, so all of the documentation on MSDN is still relevant. However some patterns and helpers have been developed while building another prototype and developing internal IP, which are included within this project.
This is one of the first iterations of the internal base project, and it will improve over time.
The bot framework has three data stores. (MSDN)
User
: data associated with a specific user (across all channels and conversations)Conversation
: data associated with a specific conversation with a specific userPrivateConversation
: data associated with a specific user within the context of a specific conversation
These are stored in an in-memory store, but if you set the AzureWebJobsStorage
config value, it will use Table Storage. This is automatically configured once you deploy to Azure, but locally the in-memory store will be used.
These are key-value stores and can get unwieldy to manage when data is written to one store with a certain key, and read from a different store with a different key. This can especially happen when there are many key-value pairs being stored. It is welcoming bugs and is a trigger for rip-your-hair-out syndrome.
There are two main classes that enable a cleaner method of managing the data stores.
models/DataStoreKey.cs
is an enum, defining keys for the key-value pairs. Each enum value has an attribute identifying the data store to use (User, Conversation, PrivateConversation)services/BotDataService.cs
contains logic pertaining to reading and writing from the data store
The implementation in the BotDataService.cs
and SetValue()
/GetValueOrDefault()
extension methods ensure data is read/written against the correct key in the correct store, and is used as per the examples below. In order to use this bot service, inject the interface IBotDataService
to your class.
public enum DataStoreKey
{
[DataStoreEntry("Preferred name", DataStore.User)]
PreferredFirstName
}
// Snippet from class BotDataService
...
public void SetPreferredName(IBotData botData, string name)
{
botData.SetValue(DataStoreKey.PreferredFirstName, name);
}
public string GetPreferredName(IBotData botData)
{
return botData.GetValueOrDefault<string>(DataStoreKey.PreferredFirstName);
}
...
A conversation and all of its instances/resources will get serialized and saved to the data store by the bot framework, so when creating services to integrate with the bot, we need to ensure they don't get serialized and are instantiated each time. Serialization can cause unnecessary issues.
When registering a service with Autofac, use the FiberModule.Key_DoNotSerialize
key.
builder.RegisterType<MessageService>()
.Keyed<IMessageService>(FiberModule.Key_DoNotSerialize)
.AsImplementedInterfaces()
.InstancePerLifetimeScope();
Use the examples in the IoC
folder for registrations.
The dialog builder simplifies resolution of a dialog through an Autofac registration. Because of the way Autofac requires the creation of a lifetimescope each time a registered services needs to be resolved, the code can get cluttered with unnecessary plumbing.
For this reason, all logic pertaining to dialog creation/resolution should be contained within the dialogs/DialogBuilder.cs
class.
Note: A conversation, its dialog stack and its state gets serialized and saved into the bot data store. For this reason, all dialogs must have the
[Serializable]
attribute applied to the class.
Use the example implementations and notes in the files below.
- IoC registration:
IoC/ApplicationDialogsModule.cs
- IoC resolution:
dialogs/DialogBuilder.cs
- Usage:
dialogs/DemoDialog.cs
As in the examples below, the CreateDialog method handles all the lifetime scope creation/deletion duties when resolving an instance.
// Snippet from DialogBuilder.cs
public NameDialog BuildNameDialog(IMessageActivity message)
{
return CreateDialog(message, scope => scope.Resolve<NameDialog>());
}
// Snippet from NameDialog.cs
...
private readonly IBotDataService _botDataService;
public NameDialog(IBotDataService botDataService)
{
SetField.NotNull(out _botDataService, nameof(botDataService), botDataService);
}
...
// Snippet from ApplicationDialogsModule.cs
...
builder.RegisterType<NameDialog>().AsSelf().InstancePerDependency();
...
// Snippet from DialogBuilder.cs
public ShowSuggestedActionsDialog BuildShowSuggestedActionsDialog(IMessageActivity message, string prompt, List<string> options)
{
return CreateDialog(message, scope => scope.Resolve<ShowSuggestedActionsDialog>(TypedParameter.From(prompt), TypedParameter.From(options)));
}
// Snippet from ShowSuggestedActionsDialog.cs
...
private readonly IMessageService _messageService;
public ShowSuggestedActionsDialog(string prompt, List<string> options, IMessageService messageService)
{
SetField.NotNull(out _messageService, nameof(messageService), messageService);
SetField.NotNull(out _options, nameof(options), options);
SetField.NotNull(out _prompt, nameof(prompt), prompt);
}
...
// Snippet from ApplicationDialogsModule.cs
...
builder.Register((c, p) =>
new ShowSuggestedActionsDialog(
p.TypedAs<string>(),
p.TypedAs<List<string>>(),
c.Resolve<IMessageService>()))
.AsSelf()
.InstancePerDependency();
...
The purpose of this service is to send multiple messages from the bot to the user. This helper service will split a string into separate chat messages, using newline characters \n
in the original string.
For example, _messageService.PostAsync("Hello!\nI'm on a new line!");
will send messages as shown in the following image.
Reading separate concise messages is nicer than reading a big paragraph, in a conversational context. Think about how to effectively communicate using shorter messages.
When you use any channel other than a webchat, such as Messenger, a user id is returned unique to each channel. When using a webchat, if the web app has a concept of user accounts, then that account id is used to identify an existing user and load their data from state.
In a web app without the concept of a user account, there is no way of automatically identifying a return user. The BotFramework-WebChat control by Microsoft is open-source, so on my fork I have added a new boolean property named persistUser
. The compiled version of this fork exists here, and can be used to embed an app using the CDN links (https://cdn.rawgit.com/develohpanda/Bot-Hackfest/master/botchat.css, https://cdn.rawgit.com/develohpanda/Bot-Hackfest/master/botchat.js).
If this flag is set, the modification is enabled. On launching the chat, it will generate a new user id and persist it to localstorage. If an id already exists, that id will be used to identify a return user. A timeout can be added in the future, but at the moment there is no timeout. This means in the demo above, if you have already provided your name, because the bot identifies a return user with the saved id, it knows your name and doesn't ask you again.
Make sure to use Microsoft Edge to browse the Azure Portal if you come across any issues. I have faced bugs with creating a Web App Bot using Chrome and Firefox.
- Log into the Azure Portal.
- Select Create a Resource from the menu.
- We will set up a Web App Bot as a base. It's found under AI + Cognitive Services.
- Enter a bot name, and the remaining fields should automatically populate. Use the Basic C# bot template if using the Hackfest starter project. (MSDN)
Follow the steps below to deploy to Azure.
Select the app service created when you created the bot resource.
Make sure to republish after changing settings.
MSDN documentation for configuring channels, speech priming etc here.
Republish, and then you can navigate to https://your-bot-name.azurewebsites.net/ to use the chatbot.
You can also navigate to http://localhost:3979/default.htm to view the same page, although the bot will be connected to the deployed version in Azure. To debug a local version of the bot, use the bot emulator.
Submit your entry at bit.ly/bot-hackfest-submit
You can either publish your bot and let us play with it, or create and upload a video to share with us. Be sure to include a description of the features you have added, the drive behind the bot and why it an improvement in the user experience. Tell us about anything unique you did.
Basically, submit anything that screams "we should win"!
- Is each dialog class tagged with the
[Serializable]
attribute? - Do the service registrations have the
FiberModule.Key_DoNotSerialize
key added?
builder.RegisterType<MessageService>()
.Keyed<IMessageService>(FiberModule.Key_DoNotSerialize)
.AsImplementedInterfaces()
.InstancePerLifetimeScope();