This is the second article in a series about Adaptive Dialogs in Bot Framework v4. It’s an open-ended list at this point, so I’ll update it as needed.
- Getting started with Adaptive Dialogs in Bot Framework v4
- Integration Testing with Adaptive Dialogs in Bot Framework v4 (This article)
Source code
TL;DR;
In this article we get started with Adaptive Dialogs, setting a focus on integration testing. This is so, because integration testing can actually help you develop faster with Adaptive Dialogs.
This is the continuation from the previous article but you can also start from a specific branch and complete the exercise just as well.
In this article we convert a simple echo bot created with the Bot Framework Templates for Visual Studio (VS) into an Adaptive Dialogs bot.
In the process we’ll work with:
- Adaptive Expressions
- Language Generation
- Recognizers (Regex)
- Integration testing with Adaptive Dialogs
Overview
If you whish, you can read a little bit of history/context about Adaptive Dialogs in the previous article overview section.
You can think of Adaptive Dialogs like a new framework, built on top of Bot Framework, that uses Dialogs as the base class for a new set of key classes called Actions.
It might seem confusing at first, but the end result is a declarative framework that allows you to work faster, writing less boilerplate code.
In this article we will:
- Convert the echo bot from the previous article to an Adaptive Dialogs bot.
- Extend the initial integration test to use some dialog-specific test classes.
- Add language generators.
- Add a regex recognizer.
- Add a simple dialog to get some user information.
I’ll be omitting the required “usings”, at least most of the times. I’ll also focus on the key parts of the code, you’ll find all the details of each step in the repo, as separate issues, branches, and the related pull requests (PRs).
Let’s get started 😀
Implementation
1 - Switch to using Adaptive Dialogs
To add Adaptive Dialogs to a bot project, we have to:
- Set up the project to use Adaptive Dialogs
- Add dialogs
- Add dialog testing classes
1.1 - Set up the project for Adaptive Dialogs
We’ll mostly follow the Create a bot project for adaptive dialogs article, but on our EchoBot project.
The article above starts by “registering components”, but at this point it looks like this is only needed if you intend to use the Bot Framework Composer with your bot. This is out of the scope of this article, so we can safely skip the component registration section, although I expect to give Composer a spin in at least one article in the series.
Create a branch for the post
Clone the repo and create a branch at tag start-02
.
Add NuGet packages
We’ll be using basic Adaptive Dialogs right now, so you just need to add this package to the bot project:
Microsoft.Bot.Builder.Dialogs.Adaptive
Add a RootDialog
When using Adaptive Dialogs (and regular Dialogs too) it’s a common practice to have a RootDialog
that acts as a sort of top-level menu for the conversation.
There’s also the convention to have all dialogs under the Dialogs
folder, and then a folder for each dialog, with it’s class inside. This dialog folder will be used later to have some dialog-related assets.
So, create a Dialogs
folder with a RootDialog
folder inside, and create the RootDialog.cs
class inside, with the following code:
|
|
In the code above:
- Take care to remove the trailing
.RootDialog
you’ll get in the namespace (line 1), because of theRootDialog
folder we’re using. Otherwise you’ll get conflicts between the class name and the namespace. - Take a look at the
Triggers
property (line 7) and itsActions
property (line 9). These are some of the key features that make developing with Adaptive Dialogs much more productive. We’ll get deeper on this later on. - Though not obvious here, all
Actions
(line 9) are also dialogs, so you can create specialized dialogs and use them just like that in Adaptive Dialogs.
Add a DialogBot
The DialogBot
is another key component of Adaptive Dialogs, because it includes the use of the DialogManager
. The DialogManager
is mandatory for Adaptive Dialogs, and it can also manage regular old-fashioned dialogs.
|
|
In the previous code you can see that:
- The root dialog is whatever dialog is injected into
DialogBot
(line 8). - The
DialogManager
works with this root dialog (line 13).
Update the AdapterWithErrorHandler
The ConversationState
is essential to DialogManager
, and DialogManager
expects to get it from the TurnContext
, so we have to add it in the Adapter, as shown next:
|
|
In the code above:
- You can see that the
Storage
,UserState
andConversationState
are “registered” in theTurnContext
as middlewares (lines 16-18), so they can be used byDialogManager
.
Register the new bot and other support classes
We’ll register all the new components into the dependency injection (DI) container so we can use them in our bot application, so update the ConfigureServices
method in Startup.cs
as follows:
|
|
In the previous code you can see that:
DialogBot<RootDialog>
has replacedEchoBot
as the implementation forIBot
(line 9), and that it does so as aSingleton
, instead of as aTransient
instance.- We’re using
MemoryStorage
as an implementation ofIStorage
(line 15), and this is fine for testing and demos but not for production, where you should use some persistent storage.
Run your new AdaptiveDialogsBot
You should be able to run your bot and test it with the Bot Emulator as shown next:
Keep in mind that, at this point, the bot doesn’t respond on ConversationUpdate
, so you’ll have to take the initiative with a simple “Hi” 😉.
Also, current test don’t pass either but we’ll fix that right now
1.2 - Update the bot to pass current tests
For this step we’ll use the most naïve solution, without using some of the most interesting features of Adaptive Dialogs but that’ll be just to pass the tests, we’ll refactor the bot later.
Update RootDialog
So, to have the tests pass we’ll use the Triggers
and Actions
mentioned before, by updating RootDialog
as follows:
|
|
In the code above:
- Notice that we’re coding on the constructor (line 3), so this is actually a declaration or configuration of the dialog.
- We added the
OnConversationUpdateActivity
trigger (line 6) and the only action is aForeach
“loop” (line 8) on themembersAdded
property of the incoming activity (line 9), and a greeting message is sent to the other members (line 14), those that are different from the recipient, that is, the bot itself (line 12). - The
OnUnknownIntent
is the default intent, that will always be triggered now (line 22), since we haven’t defined any recognizer. - We almost copied, verbatim, the code from the original
EchoBot
in aCodeAction
(line 24), that’s just a lambda function (or delegate) that’ll get invoked when necessary. You can actually set breakpoints in anyCodeAction
. - A
CodeAction
MUST end with areturn await dialogContext.EndDialogAsync(options)
(line 47).
Add the state middleware to the TestAdapter
Update the BotApplicationTestAdapter
as follows, so DialogManager
can do its job:
|
|
In the code above:
- You can see that the state middlewares are added (lines 9-11) just as we did before on the
AdapterWithErrorHandler
.
You should be able to check that all tests complete successfully now.
2 - Add adaptive dialogs
In the previous section we added the adaptive RootDialog
in a rather naïve way, mostly copying code from the EchoBot
, so in this section we’ll add a greeting dialog following “the Adaptive Dialogs way” 😉.
And we’ll do it using TDD 😁
So we’ll:
- Use the dialog test helpers classes.
- Use the Language Generation feature.
- Create a Regex recognizer
- Add a dialog to get user info
2.1 - Use dialog test helper classes
We won’t add any feature here, we’ll just add a new dialog-oriented test class, just to check that our current bot passes a new, equivalent set of test.
We’ll roughly follow the How to unit test bots official documentation although, as of April 2021, still doesn’t cover adaptive dialogs.
Install required packages for testing
For this step we need to install the following packages:
Microsoft.Bot.Builder.Testing
FluentAssertions
Create a new dialog-based test class
Create a DialogBotTests
folder with the test class DialogBotShould
.
The class will implement IClassFixture<BotApplicationFactory>
and IDisposable
the same way as the EchoBotShould
test class, actually the constructor and other private methods are identical, so we’ll only review here the second test case in the class:
|
|
In the code above:
- We get the
TestAdapter
(line 5) and set the locale (line 6), just as with theEchoBot
tests. - We get the
RootDialog
instead of the whole bot (line 8), however, testing the root dialog is (almost) the same as testing the whole bot. - We create a
DialogTestClient
(line 9) instead of theTestFlow
, but we use a different overload forDialogTestClient
from the one in the documentation, to make sure we use ourBotApplicationTestAdapter
that’s already set up to handle adaptive dialogs. - One interesting point of
DialogTestClient
is that it allows you to test dialogs individually, which can make testing easier. - You can also assert for values at the end of each turn separately.
- The
DialogTestClient
API is more verbose (lines 15, 18), but you can easily create extension methods to type less.
2.2 - Use the Language Generation feature
The Language Generation (LG) feature allows you to define templates with the output text, with all of the bot’s replies, outside of the bot code, leveraging the capabilities of Adaptive Expressions.
The best way to edit LG templates is with VS Code and the Bot Framework Adaptive Tools extension and if you also install the Open in Visual Studio Code VS extension, you can just open the Dialogs
folder with VS Code and get LG, LUIS and QnA Maker syntax highlighting, and even syntax checking for Adaptive Expressions in the LG templates.
Remove text from the bot code
Since the idea is to move all of the output text out of the code, we’ll change text to LG templates references. This way we’ll strip all “presentation-specific” code from the bot.
In this step we’ll reference the templates file in the RootDialog
and start using AdaptiveExpressions and adaptive dialog Actions.
Update the RoodDialog.cs
code as follows:
|
|
In the code above:
- Get a reference to the
RootDialog.lg
(line 8), relative to theContentRoot
from the environment (line 7). - Create a template-based generator from the
RootDialog.lg
file (line 9). - Use the
Welcome()
template (line 20). - Check for the “hello” utterance with an adaptive expression (line 31)
- Use te
CodeAction
only to set the value of thedialog.greetingTime
property (line 44) so we can use the value in the LG templates. - Use the
Greeting()
template (line 49) to reply to the “hello” utterance. - Use the
Echo()
template (line 53) to reply to everything else
Create an LG file
As mentioned, the LG templates will only handle “presentation” logic.
Take a look at the .lg file format documentation to learn its features.
Create the file RootDialog.lg
with this content:
|
|
In the template above you can see that:
- Templates are identified with a
#
(lines 1, 4, 13) - State properties can be accessed from the templates (lines 5, 14)
- You can use some logic statements in the templates (line 5)
At this point your tests should complete successfully.
2.3 - Use an intent recognizer and another dialog
The best way to handle intents in adaptive dialogs is using recognizers and handling them with an OnIntent
trigger.
You can create several types of recognizers, but in this article we’ll go with RegexRecognizers
We’ll also add a greeting dialog that will be used when the Greeting
intent is detected, to ask for the user name.
We’ll use TDD to implement this.
Create a new test case
Create a new test case as follows:
|
|
In the code above:
- We’ll check that the user state has the the entered user name (line 33).
At this point the only failing test should be the one above. So we have to make it pass the test.
Create the GreetingDialog
Create the GreetingDialog
class inside the GreetingDialog
folder under Dialogs
as follows:
|
|
In the code above you can see that:
- The actions are executed when the dialog begins (lines 11, 12),
- The user is prompted for their name (line 14) and it’s saved in the
userName
property of the current dialog (line 15). - The return value from the dialog is taken from its
userName
property (line 21).
Create the GreetingDialog LG template
Now create the GreetingDialog.lg
template as follows:
|
|
It the template above you can see that:
- The
userName
property from the dialog is used (line 5).
Create a recognizer for RootDialog
Create the RootDialogRecognizer
in the RootDialog
folder as follows:
|
|
It the code above you can see that:
- A simple regex pattern is used for the
Greeting
intent (line 8).
Update the RootDialog
Update the RootDialog as follows:
|
|
It the code above you can see that:
- The new
RootDialogRecognizer
is used (line 11). - The
OnUnknownIntent
only uses theEcho()
template (line 20). - The new
OnIntent("Greeting")
trigger executes the greeting actions (lines 24, 25). - The
GreetingDialog
is called (line 43) and its result is saved in theconversation.userName
property (line 44). - The
GreetingDialog
is added to the set of dialogs used byRootDialog
(line 50).
Update the RootDialog LG template
We now have to use the user name in the greeting templates.
Update the RootDialog.lg
file as follows:
|
|
It the template above you can see that:
- The
conversation.userName
property is simply used where needed (lines 7, 9, 11). If the property doesn’t exist an empty string is generated (and the final output is trimmed!).
Update the TestAdapter registration
There’s just a minor adjustment to make to the TestAdapter
registration, because a singleton scope configuration causes some test case interferences.
Update the registration as follows:
|
|
It the code above you can see that:
- The scope was changed to
AddScoped
(line 11), to ensure each test case is run with a brand-new adapter.
At this point all test should complete successfully 😊
Takeaways
In this post we’ve learned to:
- Convert a simple echo bot to an adaptive dialogs bot
- Use the Triggers and Actions key components of adaptive dialogs.
- Mix and match declarative dialog object declaration with CodeActions where you can add **and debug ** C# code.
- Use some dialog-based test helper classes with adaptive dialogs.
- Get and set state properties from both adaptive expressions and regular C# code.
- Use language generation templates.
- Use a regex recognizer and handle intents in adaptive dialogs.
I Hope you’ve found this post interesting and useful, and invite you to follow me on Twitter for posts and news.
You are also welcomed to leave a comment or ask a question in the comments section below.
Happy coding!