How to Build a Chatbot FAQ with the Azure Bot Service

Here’s a guest post I did on the Microsoft MVP Award Program site:

How To Build A Chatbot FAQ With The Azure Bot Service

@JoeMayo

Posted in Uncategorized | Leave a comment

Free Book Chapter

Microsoft Press has released a free chapter from my book, “Fine-Tuning Your Chatbot”: http://bit.ly/2BJNcsZ

@JoeMayo

Posted in Uncategorized | Leave a comment

Programming the Microsoft Bot Framework Errata

My latest book, Programming the Microsoft Bot Framework, published recently. It was a joy to write and I hoped it would be error free. However, the reality is that books are rarely perfect and we need to publish their errata.

The focus of this post is to highlight a problem that people will encounter when running sample code. There are three primary applications that the book uses to illustrate Bot Framework features: Rock-Paper-Scissors Chatbot, Wine Chatbot, and Music Chatbot. Rock-Paper-Scissors stands alone as a game someone could play. However, both Wine Chatbot and Music Chatbot use 3rd-party APIs. Wine Chatbot was built with the Wine.com API and Music Chatbot was built with the Groove API. Unfortunately, both the Wine.com and Groove APIs have shut down. This means that the code for the demos, as described in the book, won’t run.

Therefore, instead of relying on those APIs, I refactored and updated the code on my MS Bot Framework Book GitHub site. Here are the changes:

  • Wine Chatbot: No longer uses a 3rd-party API. It consumes sample dummy data and runs fine.
  • Music Chatbot: Microsoft partnered with Spotify for handling Groove customers. Therefore, I refactored the code to use the Spotify API. There are minor changes, but the code runs the same as the original, except the data source is different.

In the book, I explain how the back-end code communicates with each API. However, the code has a separate data layer, abstracting most of the details of the data from the rest of the code. This also means that the Bot Framework and associated features don’t change. The only change is how the data is consumed, which doesn’t affect the material on how the Bot Framework works. This is similar to how we build systems in real life – we use separation of concerns and build abstractions that minimize the impact of inevitable changes.

Please download the code (https://github.com/JoeMayo/MSBotFrameworkBook), inspect the changes, and you’ll see what I mean.

@JoeMayo

Posted in Uncategorized | Leave a comment

eBook Deal of the Week

Microsoft Press has Programming the Microsoft Bot Framework as the eBook deal of the week, with 50% off:

BotFrameworkBookCover

https://www.microsoftpressstore.com/deals/

@JoeMayo

Posted in Uncategorized | Leave a comment

Free Azure Accounts

Azure recently expanded their free trial to up to 12 months:

Create your Azure free account today

As far back as I can remember, Azure had some type of free offering. e.g. 10 Free Web apps, which I’ve used for a long time for low-traffic sites or just presentation demos.

My latest book, Programming the Microsoft Bot Framework: A Multiplatform Approach to Building Chatbots, uses Azure as the platform to deploy chatbots on, so the timing on the free Azure account offer couldn’t be better.

@JoeMayo

Bot Framework Video + Book

Save 50% on Bot Framework eBook + video

 

Posted in Azure, bot framework | Leave a comment

Personalizing User Experience with the Bot Framework State Service

For many chatbots, personalization can enhance the user experience. With the Bot Framework State Service, a chatbot can save and retrieve user preferences. This can let the chatbot avoid asking the user the same question across sessions. The particular example in this post shows how to keep track of a user’s visits. That way, if this is the user’s first visit, they’ll receive an initial hello message. On subsequent visits, the chatbot knows that the user has visited before and will send a welcome back message.

Code on GitHub

Revisiting PC Bot

In my previous post on PC Bot, I described how to handle all activity types, in addition to ActivityTypes.Message. The Post method started by calling ReactAsync, like this:

Activity reply = await new MessageHandler().ReactAsync(activity);

This passed the incoming Activity instance to the ReactAsync method, handing the ActivityTypes.ConversationUpdate activity type, like this:

public async Task<Activity> ReactAsync(Activity activity)
{
    Activity responseActivity;

    switch (activity.Type)
    {
        //...
        case ActivityTypes.ConversationUpdate:
            responseActivity = await ReactToConversationUpdateAsync(activity);
            break;
        //...
    }

    return responseActivity;
}

All the other ActivityTypes cases were left out so this can show the ConversationUpdate case, calling ReactToConversationUpdateAsync, discussed next.

Overview of Managing User Data

Logically, the process this example uses to manage user state is as follows:

  1. Get user state from the State service.
  2. Read, evaluate the data.
  3. Create new data or update existing data.
  4. Save user state to the State service.

More specifically, this method will get the following UserInfo object from the State service:

[Serializable]
public class UserInfo
{
    public string UserName { get; set; }
    public DateTime Joined { get; set; }
    public DateTime LastVisit { get; set; }
}

This is a custom type, that I created, for saving data in the State service. Notice the Serializable attribute, which is required because the Bot Framework automatically serializes the data for us when querying and writing to the State service.

The UserName holds the user’s name. Joined holds the DateTime of the user’s first visit and LastVisit holds the DateTime of the user’s most recent message.

The next section explains how to code works to manage this process.

The ReactToConversationUpdateAsync Method

The ReactToConversationUpdateAsync method performs the tasks described in the previous section. This example uses user state to hold the data, meaning that regardless of how many conversations a user has, this will always be available to just that user.

Note: There’s also Conversation state, which is available for a single conversation, and Private state, which is available for a single user in a single conversation.

async Task<Activity> ReactToConversationUpdateAsync(Activity activity)
{
    StateClient stateClient = activity.GetStateClient();
    BotData userData = await stateClient.BotState.GetUserDataAsync(activity.ChannelId, activity.From.Id);

    UserInfo user;

    if (userData.Data != null)
    {
        user = userData.GetProperty<UserInfo>(UserInfoProperty);
    }
    else
    {
        user = new UserInfo
        {
            UserName = activity.From.Name,
            Joined = DateTime.Now
        };
    }

    // if no record of previous visit, treat it as the first time.
    bool IsFirstVisit(UserInfo usr) => usr.LastVisit == default(DateTime);

    // is the message addressed to the chatbot?
    bool IsChatbot(ChannelAccount channelAcct) => channelAcct.Id == activity.Recipient.Id;

    string response;
    if (activity.MembersAdded?.Any(IsChatbot) ?? false)
        response = IsFirstVisit(user) ? "Hi, welcome to PC Bot." : "Welcome back.";
    else
        return null;

    Activity returnActivity = activity.CreateReply(response);

    user.LastVisit = DateTime.Now;
    userData.SetProperty<UserInfo>(UserInfoProperty, user);

    try
    {
        await stateClient.BotState.SetUserDataAsync(activity.ChannelId, activity.From.Id, userData);
    }
    catch (HttpOperationException)
    {
        userData = await stateClient.BotState.GetUserDataAsync(activity.ChannelId, activity.From.Id);
        user = userData.GetProperty<UserInfo>(UserInfoProperty);
        user.LastVisit = DateTime.Now;
        userData.SetProperty<UserInfo>(UserInfoProperty, user);
        await stateClient.BotState.SetUserDataAsync(activity.ChannelId, activity.From.Id, userData);
    }

    return returnActivity;
}

As the first step, the method gets UserInfo from the State service, as follows:

StateClient stateClient = activity.GetStateClient();
BotData userData = await stateClient.BotState.GetUserDataAsync(activity.ChannelId, activity.From.Id);

Once you call GetStateClient, you can use the BotState property to get the type of data you want. This example was GetUserDataAsync, but you could also call GetConversationDataAsync for Conversation state and GetPrivateConversationDataAsync for Private state.

If the user has never visited this chatbot, userData.Data is null. We handle that scenario by either reading UserInfo if it’s there or instantiating a new UserInfo instance, like this:

    UserInfo user;

    if (userData.Data != null)
    {
        user = userData.GetProperty<UserInfo>(UserInfoProperty);
    }
    else
    {
        user = new UserInfo
        {
            UserName = activity.From.Name,
            Joined = DateTime.Now
        };
    }

The GetProperty method reads either a simple or complex type from the State service. UserInfoProperty is a string constant, serving as a key for that property. You can have multiple properties, indexed by key. When this is the first time the user visited, the new UserInfo instance populates with the activity.From.Name and current DateTime.

Rather than use a null value to determine if this was the first visit, the code checks to see if the LastVisit is the default value. Here’s the code that uses that to determine what type of response to send to the user:

    // if no record of previous visit, treat it as the first time.
    bool IsFirstVisit(UserInfo usr) => usr.LastVisit == default(DateTime);

    // is the message addressed to the chatbot?
    bool IsChatbot(ChannelAccount channelAcct) => channelAcct.Id == activity.Recipient.Id;

    string response;
    if (activity.MembersAdded?.Any(IsChatbot) ?? false)
        response = IsFirstVisit(user) ? "Hi, welcome to PC Bot." : "Welcome back.";
    else
        return null;

    Activity returnActivity = activity.CreateReply(response);

As with any welcome message, the code makes sure that it processes the MembersAdded message (there are two and the other is MembersRemoved). If the chatbot is the one added, the code can prepare a response. If the first visit, the chatbot says “Hi, welcome to PC Bot.”. Otherwise, the user must have conversed with the chatbot and says “Welcome Back.”

When this wasn’t an activity for adding the chatbot, the method returns null, which calling code understands that it should not reply to the user. Otherwise, the code calls CreateReply to build a new Acitivity to send to the user.

Finally, the method needs to update the LastVisit and save the updated data to the State service. In addition to saving the data, the code below shows how to perform error handling for concurrency errors in saving data:

    user.LastVisit = DateTime.Now;
    userData.SetProperty<UserInfo>(UserInfoProperty, user);

    try
    {
        await stateClient.BotState.SetUserDataAsync(activity.ChannelId, activity.From.Id, userData);
    }
    catch (HttpOperationException)
    {
        userData = await stateClient.BotState.GetUserDataAsync(activity.ChannelId, activity.From.Id);
        user = userData.GetProperty<UserInfo>(UserInfoProperty);
        user.LastVisit = DateTime.Now;
        userData.SetProperty<UserInfo>(UserInfoProperty, user);
        await stateClient.BotState.SetUserDataAsync(activity.ChannelId, activity.From.Id, userData);
    }

    return returnActivity;

After setting LastVisit to the current DateTime, the code calls SetProperty. It uses the UserInfoProperty key and passes the UserInfo instance to save.

Similar to how we got the data, the code calls SetUserDataAsync with the userData that we called SetUserDataAsync on.

Note: Notice that we passed channel and user ids as arguments to both GetUserDataAsync and SetUserDataAsync. This indicates that the affinity of the data is to a user on a channel. So, the user data saved is only for that channel and won’t be available for the same user on a different channel. The way to correlate channels is by having the user sign in so you know who they are and add your own logic to correlate user data across channels.

Notice that the SetUserDataAsync is in a try/catch block. There’s a possibility of a concurrency exception, where a user sends multiple messages to a chatbot and one message could arrive and finish in the middle of another message that was already underway. This would cause an HttpOperationException. You can see how this code attempts to mitigate that situation by getting and setting values again. The assumption is that if there was a concurrency exception, the UserInfo already exists and all we should do is make sure the LastVisit gets updated.

Summary

The purpose of this post was to show how to leverage the Bot Framework State Service to handle user data and personalize a chatbot. The example showed how to keep track of whether a user is new or re-visiting. We read a UserInfo instance from the state service, determined if the user is new, updated LastVisit, and saved the UserInfo back into State service. You also learned how to handle data concurrency issues by catching HttpOperationException, when saving user data back to the State service.

@JoeMayo on Twitter

Bot Framework Video + Book

Save 50% on Bot Framework eBook + video

Posted in AI, bot framework, bots, chatbots, Uncategorized | Leave a comment

Registering New Dependencies with Autofac in the Microsoft Bot Framework

A while back, Autofac moved to deprecate the ContainerBuilder.Update method, explaining the decision in the Github issue, “Discussion: ContainerBuilder.Update Marked Obsolete”. You might have seen this surface as a warning while resolving types in a chatbot’s Global.asax.cs. It looks like this:

Warning    CS0618    ‘ContainerBuilder.Update(IContainer)’ is obsolete: ‘Containers should generally be considered immutable. Register all of your dependencies before building/resolving. If you need to change the contents of a container, you technically should rebuild the container. This method may be removed in a future major release.

The Bot Framework team accepted this as an issue, [Feature Request] Enable ability/guidance to add registrations to Autofac container without Update. This issue has now been updated in recent Bot Framework versions and the solution is to use the static UpdateContainer method off the Conversation type.

To see how this works, I’ll reference a recent blog post from the Bot Framework Team, Saving State data with BotBuilder-Azure. The purpose of that post is to explain how to customize the Bot State Service, using a custom storage solution, rather than the default. Here’s the code:

    var builder = new ContainerBuilder();

    var store = new TableBotDataStore(
        ConfigurationManager
            .ConnectionStrings["StorageConnectionString"]
            .ConnectionString);

    builder.Register(c => store)
        .Keyed<IBotDataStore<BotData>>(AzureModule.Key_DataStore)
        .AsSelf()
        .SingleInstance();

    builder.Register(c => new CachingBotDataStore(store,
        CachingBotDataStoreConsistencyPolicy
        .ETagBasedConsistency))
        .As<IBotDataStore<BotData>>()
        .AsSelf()
        .InstancePerLifetimeScope();

    builder.Update(Conversation.Container);

What’s unique about this code is that is explicitly instantiates ContainerBuilder first and then calls Update after registering types. That generates a compiler warning, described above, which is what this post demonstrates how to resolve.  Here’s how to rewrite that with the new ConversationUpdate method:

    Conversation.UpdateContainer(builder =>
    {
        var store = new TableBotDataStore(
            ConfigurationManager
                .ConnectionStrings["StorageConnectionString"]
                .ConnectionString);

        builder.Register(c => store)
            .Keyed<IBotDataStore<BotData>>(AzureModule.Key_DataStore)
            .AsSelf()
            .SingleInstance();

        builder.Register(c => new CachingBotDataStore(store,
            CachingBotDataStoreConsistencyPolicy
            .ETagBasedConsistency))
            .As<IBotDataStore<BotData>>()
            .AsSelf()
            .InstancePerLifetimeScope();
    });

The parameter type of UpdateContainer is Action<T>, where T is ContainerBuilder. So, you don’t need to instantiate ContainerBuilder because Bot Framework passes it to you. Further, you no longer need to call Update, solving the compiler warning problem.

@JoeMayo

Posted in bot framework, bots, Uncategorized | Leave a comment

Unit Converter Bot

Unit Converter Bot converts from one unit to another. You can use it by asking questions like “How many liters in 3 quarts?” or “Please convert 5 pounds to kilograms.”

A conversation with Unit Converter in the Emulator

Talking to Unit Converter

My goals for Unit Converter Bot were to have a limited scope, use natural language processing, and handle unknown conditions.

Limited Scope

The reason I wanted limited scope is to be able to create a bot in a decent amount of time, make building it manageable, and give potential users a definitive reason to use the bot. I think there are concepts out there where people are building bots with a wide array of features. If I had embarked on something like that, I wouldn’t be writing this blog post right now because I would still be building the bot. Limiting scope also increases the quality of Unit Converter Bot because it reduces the number of possibilities of what people can say to the bot. What I’ve learned so far is that even with simple conversions, there are many ways to request the same thing. Combined with the number of possible conversions, the complexity increases. Limiting scope reduces this complexity. Users would benefit from the bot’s limited scope because they know that this bot does one thing and won’t be confused in what’s it’s purpose is. By focusing on one thing, my bot has the potential to be awesome for conversions only and thus more useful.

Natural Language

Engaging in conversation is the sweet spot for bots. In the future, people won’t want to constantly learn new UI’s, they’ll just want to ask a question and get an answer. Many current bots aren’t much more than a messaging based imitation of an app or Web site – and that’s okay because I’m not advocating a one-size-fits-all approach to bots. What I am saying is that the conversational aspect of bots is so compelling that it should be strongly considered in addition to buttons and menus. Unit Converter Bot uses natural language processing and is pretty decent at understanding the question. Considering the vast number of possibilities of how someone could ask a conversion question, there’s undoubtedly questions that the bot won’t understand. However, it will continue to improve to the point that not understanding a conversion question will be a rare occurrence.

Graceful Recovery

People ask bots all kinds of things. If someone were to ask about the weather, Unit Converter Bot will politely let it know that it doesn’t understand the request. However, stopping at the point of misunderstanding is a missed opportunity. You have to think, “what is the goal of the bot?” In this case it’s unit conversions and the bot needs to do everything it can to help the person reach their goal. So, Unit Converter Bot has a backup strategy where it takes the user step-by-step through a set of questions to perform the conversion. This increases the possibility that the user will have a successful experience with the bot. My goal was to not just leave people hanging when the bot doesn’t understand them, but to take extra measures to help them achieve their goal.

Parting Thoughts

This was a great exercise in designing and building a bot that I thought should stand up to higher standards than the demo’s I’ve been blogging about. It’s been a good learning experience and I expect to learn more from it. I built this on the Microsoft Bot Framework. Though it resides on all supported channels, a few channels are pending final approval.

@JoeMayo

Posted in bot framework, bots | Leave a comment

LUIS and the Bot Framework: A Natural Language Match

My previous Bot Framework posts have concentrated on the sophisticated conversation management facilities of Bot Connector and Bot Builder. While those are amazing capabilities, they don’t offer much more in user interaction than could be handled by a command line utility. For a bot to be truly conversational, it needs to move beyond primitive single word grunts and understand more naturally spoken language.

Language Understanding Intelligent Service (LUIS) is a natural language service that helps your bot be more conversational. It works by building models that classify utterances (language statements) and extracts data from those utterances, providing actionable results for your bot code. The Bot Framework has types that integrate smoothly with LUIS to help your bot understand natural language statements. This post will describe a model I created with LUIS and show you how to integrate that model into a bot.

The LUIS Model

To get started with LUIS, you need to build a model. A LUIS model is a service that uses machine learning to recognize utterances and provide a probability score of how close it thinks that utterance is to Intents and Entities. Intents are things that a user wants to accomplish or obtain information on. Entities are the data points associated with those intents. In your Bot Framework code, you map Intents to methods and use Entities like they were parameters. The LUIS Help pages has an excellent video that explains how to get started, building a LUIS model.

For this demo, I created a ContactInfo model. The purpose of the model is to support a contact management bot. The BotDemos source code for this post includes a LUIS Models folder that contains the ContactInfo.json file that contains the ContactInfo model. This model has a couple intents: None and ChangeInfo. The None intent is the default for catching unrecognized utterances. The ChangeInfo intent supports utterances for when the user wants to change some of their contact information. The ChangeInfo model has a single entity named ContactType, which could be an address, name, phone number, or email. Make sure your train and publish your model before trying to access it via the Bot Framework. After you publish your model, the App Settings on your page will contain an App Id and Subscription Key, which are necessary to add to your Bot code that integrates with LUIS, which is covered next.

Implementing a LuisDialog

To get started with using LUIS, I created a new Bot Framework project. If you don’t know how to do this yet, you can learn from one of my earlier posts, Pig Latin Bot. To start, I created a ContactInfoDialog class that derives from LuisDialog<object> like this:

    [Serializable]
    [LuisModel(
        "<your App ID goes here>",
        "<your subscription key goes here>")]
    public class ContactInfoDialog : LuisDialog<object>
    {
        public const string ContactType = "ContactType";

        string currentEntity = "";
    }

As is typical of Bot Framework dialogs, you need to make it Serializable if you wish to retain state. The LuisModel attribute allows logic in LuisDialog to communicate with LUIS, using the App ID and Subscription Key from your model, ContactInfo in this case. The LuisDialog type parameter is the result type of the dialog, which contains collected information for a stateful bot. Since we aren’t maintaining state in this simple demo bot, I set the LuisDialog type to object. ContactType is the entity we’ll work with and it’s a const to avoid working with strings. Since later code navigates between methods, I’ll keep the name of the current ContactType entity in the currentEntity field. The ContactInfoDialog class has methods that handle intents and you’ll see what happens when LUIS doesn’t recognize an utterance in the next section.

Handling Unrecognized Utterances

You can set up a method in a LuisDialog to handle cases where your LUIS model doesn’t recognize an utterance. Just create the method and decorate it with a LuisIntent attribute containing an empty string. Here’s an example:

        [LuisIntent("")]
        public async Task None(
             IDialogContext context,
             LuisResult result)
        {
            string userUtterance = result.Query;
            await context.PostAsync(
                $"Sorry, I didn't understand "{userUtterance}".");
            context.Wait(MessageReceived);
        }

As mentioned, the LuisIntent attribute with the empty string parameter indicates that LUIS didn’t understand what the user said. The None method has IDialogContext and LuisResult parameters. The IDialogContext parameter is the same as used in all dialogs and you can see how the method uses it to call context.PostAsync to let the user know that the bot couldn’t understand them and then context.Wait to wait for the next message. LuisDialog has an implementation of MessageReceived that it uses to interact with LUIS when the next message from the user arrives. You can use LuisResult to extract information about the user’s utterance, as None does by reading result.Query.

Note: You can visit the LUIS model, click on Review Labels, and classify utterances that aren’t part of your training set. This lets you continuously improve your LUIS model with real data.

Next, we’ll handle an utterance that LUIS does recognize.

Handling Valid Intents

To match methods to intents, decorate the method with a LuisIntent attribute containing a parameter matching the intent name from the LUIS model to handle. Then add the logic you need to handle what the user asks for. Here’s an example for the ChangeInfo intent:

        [LuisIntent("ChangeInfo")]
        public async Task ChangeInfo(
            IDialogContext context,
            LuisResult result)
        {
            EntityRecommendation entityRec;
            result.TryFindEntity(ContactType, out entityRec);

            currentEntity = entityRec.Entity;

            PromptDialog.Text(
                context: context,
                resume: ResumeAndHandleTextAsync,
                prompt: 
                    $"What would you like to change your” +
                    $” {currentEntity} to?",
                retry: "I didn't understand. Please try again.");
        }

        public async Task ResumeAndHandleTextAsync(
            IDialogContext context,
            IAwaitable<string> argument)
        {
            string newEntityValue = await argument;

            await context.PostAsync(
                $"Your {currentEntity} is now {newEntityValue}");

            context.Wait(MessageReceived);
        }

The ChangeInfo method handles the LUIS ChangeInfo intent. The IDialogContext and LuisResult parameters serve the same purpose as described earlier, except that this time we need to read the associated entity to implement appropriate logic. Here, the code uses result.TryFIndEntity to obtain a reference to the ContactType entity. In real code, you would want to have more logic for recognition and validation of the entity.

The code performs a prompt for the text to change the bit of contact info to and then waterfalls to ResumeAndHandleTextAsync to work with the user’s response. Again, this code calls context.Wait(MessageRecieved) to continually wait for the user’s input.

Here’s an interaction with ContactInfo bot, showing how it responds to various utterances.

Emulator showing a discussion with a bot that uses LUIS

LUIS Powered Conversations

Summary

Now you know how to integrate a LUIS model with the Microsoft Bot Framework. Create and publish the model, create a class derived from LuisDialog, add App ID and Subscription Key to the LuisModel attribute, and use the LuisIntent attribute to decorate methods that handle LUIS model intents. For more information on running and testing a bot, you can visit previous posts in my blog. You can also find the code (including the ContactInfo model) on my GitHub site.

@JoeMayo

Posted in AI, bot framework, bots, LUIS | Leave a comment

Dynamic FormFlow Forms in Bot Builder

In a previous post on Bot Builder FormFlow, I described how to automatically construct a dialog with just a class, properties, and a few other methods. The demo was a bit rigid because I used enums to define choices. In practice, you won’t always know ahead of time what choices will be available or the choices could change over time. So, you need a data-driven approach. This post shows how to dynamically define those choices and still have the benefits of FormFlow. This is nearly identical to my previous FormFlow post, except for a couple items that I’ll describe in this post.

Specifying the Dynamic Property

The previous FormFlow example defined the Product property as a Product enum. This is a good candidate for a dynamic data-driven property because over time a bug report will need to accommodate all of the products you support. Here’s the updated BugReport class.

    [Serializable]
    public class BugReport
    {
        public string Product { get; set; }

        public List<PlatformOptions> Platform { get; set; }

        public string ProblemDescription { get; set; }
    }

Here you can see the Product is a string so that it can hold the value that the user enters. The following method simulates a data-driven approach to obtaining the values that can be assigned to Product.

        static List<string> GetProducts()
        {
            return new List<string>
            {
                "Office",
                "SQL Server",
                "Visual Studio"
            };
        }

The GetProducts method returns a collection of strings that can be assigned to the Product property. Now, let’s see how to use these strings in a FormFlow dialog.

Adding a Dynamic Field

In this demo, we’ll modify the BuildForm method, which is a member of the BugReport class. In the previous example, the BuildForm implementation was pretty simple by showing a message, specifying an OnCompletionAsync handler, and initiating the dialog. The difference in this demo is that the code specifically directs which fields/properties to display. Here’s the changed BuildForm method.

        public static IForm<BugReport> BuildForm()
        {
            return new FormBuilder<BugReport>()
                    .Message("Welcome to Bug Report bot!")
                    .Field(new FieldReflector<BugReport>(nameof(Product))
                            .SetType(null)
                            .SetDefine((state, field) =>
                            {
                                foreach (var prod in GetProducts())
                                    field
                                        .AddDescription(prod, prod)
                                        .AddTerms(prod, prod);

                                return Task.FromResult(true);
                            }))
                    .Field(nameof(Platform))
                    .AddRemainingFields()
                    .OnCompletionAsync(async (context, bugReport) => 
                     {
                        await context.PostAsync("Thanks for the report!");
                     })
                    .Build();
        }

In this code, the Field methods describe exactly which properties to display and in what order. FormFlow uses reflection, which the FieldReflector class helps with. The SetDefine method dynamically defines the field. It uses the GetProducts method, shown earlier, and iterates through the list of strings. This is just a demo, so AddDescription and AddTerms use the same string as the parameter key and value. If this was a real bot, GetProducts would read from a database table and return a custom class that contained all of the meta-data required to populate description and terms with more useful values.

Since FormBuilder is a fluent interface, you can continue specifying fields to add and optionally use AddRemainingFields for FormFlow to work normally on any following fields. The rest of the program works the same ways as my previous post on FormFlow.

Summary

This post explained why the default FormFlow wasn’t flexible enough for all scenarios. I defined a string type property and a method that returned all the values that could be assigned to that property. Then you saw how to modify the BuildForm method to specify fields and dynamically define a field with custom data. Now, you’re able to make a bot more adaptable to changes with dynamic fields.

The code for this post is available via my BotDemos GitHub repository.

@JoeMayo

Posted in AI, bot framework, bots | 5 Comments