Workshop at ilionx's DevDays
In this lab you'll get to know the Microsoft Bot Framework by creating a chat bot which can recognize a returning user, and can perform a few simple actions.
The finished solutions can be found here and are available for you to compare your work with. You can take a look at them when you're having difficulties executing the assignments.
- Visual Studio 2017 or newer
- The Microsoft Bot Framework SDK v4 template is installed
- The Microsoft Bot Framework Emulator is installed
1.1 New project)
- Create a new project in Visual Studio, select the
Empty Bot
template. EnterDevDaysBot
as the project name. You can choose any other name you like here, but the workshop will follow said name.
-
The template we use to create the project does not generate the name entered. Therefore, when the project has been created, navigate to the following two files and change the following:
- In
DevDaysBot.cs
change the class name toDevDaysBot
- In
Startup.cs
, replace line 36 withservices.AddTransient<IBot, DevDaysBot>();
- In
-
Start the project and verify that it runs correctly, it should open up a browser window.
1.2 Setting up the Emulator)
Now that the project in running we can use the Bot Framework Emulator to test our bot, here we will set this up.
- In the browser window that popped up, notice the url under the 'Your bot is ready!' text. Copy this url.
- Open the Bot Framework Emulator, select
Create a new bot configuration
.- For the name, enter
DevDaysBot
. - For the Endpoint URL, enter the url you copied.
- The rest can be kept as it is.
- For the name, enter
- Click
Save and connect
, save the .bot file in the project you created. - You are now able to communicate with the bot and see the 'Hello world!' message. The bot doesn't do much at this moment though, lets change that.
Our bot can't do much yet, lets add some functionality. In this assignment we will add the following:
- When meeting a new user, ask their name.
- Recognize a returning user and refer to them by name.
- The ability to respond to:
- a greeting
- a goodbye
2.1 Setting up state)
- Currently we are receiving a 'Hello world!' message when entering a conversation with the bot. Here we want to ask for the name of the user. In
DevDaysBot.cs
change the text in theSendActivityAsync
function inOnMembersAddedAsync
to something like:Hi there, I am the DevDaysBot! What's your name?
.
Ok, so the user is now prompted to enter their name, but how do we store the response? For this we will use the state.
In this workshop we will use memory storage to store the state, in real world projects you want to use a different type of storage. The reason for this is that the memory storage is cleared each restart. It is built for testing purposes.
-
Create a new folder in the project called
Models
. Create two files in this folder:UserProfile.cs
- Add a string property called
Name
with a default getter and setter.
- Add a string property called
ConversationData.cs
- Add a bool property called
PromptedForName
with a default getter and setter.
- Add a bool property called
-
In
Startup.cs
add the following code in theConfigureServices
function:services.AddSingleton<IStorage, MemoryStorage>(); services.AddSingleton<UserState>(); services.AddSingleton<ConversationState>();
-
In
DevDaysBot.cs
implement the constructor and define two private properties:private BotState _conversationState; private BotState _userState; public DevDaysBot(ConversationState conversationState, UserState userState) { _conversationState = conversationState; _userState = userState; }
-
Add the OnTurnAsync function to save changes that might have occured.
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default) { await base.OnTurnAsync(turnContext, cancellationToken); // Save any state changes that might have occured during the turn. await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken); await _userState.SaveChangesAsync(turnContext, false, cancellationToken); }
2.2 Meeting a new user)
Alright, so we set up the state, now it's time to interact with it and store the name of the user.
In the OnMembersAddedAsync
function we ask the user for their name, because of this we have to set our PromptedForName
property in our conversationState
. To do this we need to access the state, for this we have accessors.
-
at the top of the
OnMembersAddedAsync
function, implement the code below. This allows us to interact with theconversationData
.var conversationStateAccessors = _conversationState.CreateProperty<ConversationData>(nameof(ConversationData)); var conversationData = await conversationStateAccessors.GetAsync(turnContext, () => new ConversationData());
-
Set the
PromptedForName
property totrue
after you ask the user for their name in theOnMembersAddedAsync
function.
The next step is to handle the response of the user. In this case this means storing the response as the name. When the user sends a message the function OnMessageActivityAsync
is triggered, in this function we will create our logic.
-
Add the following function to the
DevDaysBot
class inDevDaysBot.cs
.protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken) { // Extract the conversationData from the state accessors var conversationStateAccessors = _conversationState.CreateProperty<ConversationData>(nameof(ConversationData)); var conversationData = await conversationStateAccessors.GetAsync(turnContext, () => new ConversationData()); // Extract the userProfile from the state accessors var userStateAccessors = _userState.CreateProperty<UserProfile>(nameof(UserProfile)); var userProfile = await userStateAccessors.GetAsync(turnContext, () => new UserProfile()); }
In this function we want to check whether the name of the user is already known, and if the user has been prompted for it.
-
Add the following code at the bottom of the
OnMessageActivityAsync
function. This code will store the name after the bot has asked for the name of the user.if (string.IsNullOrEmpty(userProfile.Name) && conversationData.PromptedForName) { userProfile.Name = turnContext.Activity.Text?.Trim(); conversationData.PromptedForName = false; // Acknowledge that we got their name. await turnContext.SendActivityAsync($"Nice to meet you {userProfile.Name}!"); // End the turn here. return; }
-
Run your bot to check if it now asks for the name of the user, and handles the user's response as expected.
2.3 Recognize a returning user)
-
After the if-statement in the
OnMessageActivityAsync
in function inDevDaysBot.cs
, add a switch-statement for the value ofturnContext.Activity.Text.ToLower()
. In the default of this switch-statement, useawait turnContext.SendActivityAsync()
to send a message from the bot which says something likeI'm sorry {userProfile.Name}, I don't understand that.
For now this default statement is the only statement in the switch.
-
In the Bot Emulator, enter your name when the bot asks for it, after that, select
Restart with same user ID
to simulate a new conversation with the bot as the same user. Notice how the the bot starts the conversation by asking our name again. Since our name is already known, we don't want this to happen, we want the bot to welcome us back instead. Lets add that.
Remember: The state of the bot is cleared each time the bot is turned off since we're using MemoryStorage to store it.
How to restart with the same user ID. Select the dropdown next to
Restart conversation
to see the option to restart with the same user ID.
-
Replace the implementation of the
OnMembersAddedAsync
function inDevDaysBot.cs
with the following, take your time to understand the code:// Extract the conversationData from the state accessors var conversationStateAccessors = _conversationState.CreateProperty<ConversationData>(nameof(ConversationData)); var conversationData = await conversationStateAccessors.GetAsync(turnContext, () => new ConversationData()); // Extract the userProfile from the state accessors var userStateAccessors = _userState.CreateProperty<UserProfile>(nameof(UserProfile)); var userProfile = await userStateAccessors.GetAsync(turnContext, () => new UserProfile()); foreach (var member in membersAdded) { if (member.Id != turnContext.Activity.Recipient.Id) { if (string.IsNullOrEmpty(userProfile.Name)) { await turnContext.SendActivityAsync(MessageFactory.Text($"Hi there, I am the DevDaysBot! What's your name?"), cancellationToken); conversationData.PromptedForName = true; } else { await turnContext.SendActivityAsync(MessageFactory.Text($"Hi {userProfile.Name}, welcome back!"), cancellationToken); } } }
Now our bot will ask for our name if this is not yet known, otherwise it will greet the returning user. Nice work!
2.4 Add responses)
- In the switch-statement in the
OnMessageActivityAsync
function, add two cases:- A case for
hello
, greet the user by sending a message here - A case for
bye
, say goodbye to the user by sending a message here
- A case for
The bot will now understand these 'commands' and will know how to act on them. Later on we will add more complex commands.
- Run your bot, enter your name, and verify that the bot responds as expected when you enter
hello
orbye
In this lab you created a chat bot which can recognize a returning user and handle a few simple commands. Nice job!
In the next lab we will integrate LUIS with our bot.
Nice work! Take a look at dialogs within the Bot Framework and how to implement them. Try to implement it in your project.
Dialogs are a central concept within the Bot Framework, using dialogs is a way to manage the conversations your bot has with its users, and keep it more organized. Read more about dialogs by following the links above!