QnA Maker with rich cards in .NET

If you are unfamiliar with the QnA Maker, please click here to learn more about it. A good bot is one that is actually useful to users, seems intuitive right?  Some bots may need to process payments, handle forms, track location, or perform some other sort of complex or custom operation. We also have LUIS, which can be utilized to provide bots with natural language understanding, so that the proper actions can be called by a user’s intent. However, often your bot will simply need to provide well-defined answers to questions that users may have. Visiting the QnA Maker portal, you can discover why it is such a useful tool for creating FAQ knowledge based question-answer functionality for your bot. In this article, we’ll be exploring different ways to utilize the service to help create better bots.

First we will create a new QnA service, then manually provision the knowledge base by creating some question-answer pairs before deploying it. Then, we’ll create a new bot and configure it to connect to the QnA service. Lastly, we’ll demonstrate how we can override the default QnA response implementation to format our answers as rich UI cards to display to users.

Prerequisites:

Creating a QnA Service

Login to your Microsoft account, and from the QnA portal, select ‘Create new service’ from the menu to begin. You may notice that there are other options to provide an FAQ URL, or upload an FAQ file (formats supported include .tsv, .pdf, .doc, .docx, .xlsx, up to 2 MB), although these are very flexible and powerful options, we’ll be creating our knowledge base from scratch so you can ignore these for now. Simply create a name for your service, then scroll to the bottom and click on ‘Create’ to start a new service, as shown below.

Once your service is created, you will be re-directed to your service’s knowledge base where we will add some question-answer pairs, train the service, and publish.

Training our Knowledge Base

In your service’s knowledge base editor, you can add question and answer pairs, and when you are satisfied, click on ‘save and retrain.’ Behind the scenes, QnA maker leverages LUIS and other cognitive services to create a model for your service.

Note: Simple Markdown formatting for answers is currently supported 

To deploy the service, simply click ‘publish’ in the upper right as shown below.

After your service is deployed, you’ll be re-directed to a page view which provides a sample HTTP request for another application to use the service. Also provided are the Knowledgebase Id and the Subscription key to access the service. Copy and paste these unique values and save them for later, as we’ll need to pass these to the bot later.

That’s it for creating and deploying our QnA service. You can always go back to add more question-answer pairs and retrain and publish your QnA knowledge base at any time.

Creating a new Bot

In this step we’ll create a new bot using the .NET SDK. You can do this by opening Visual Studio, then selecting ‘File –> New Project –> Bot Application’ as shown below.

Note: Ensure you have the Bot Application template

Once your project is created, you’ll need to install one new NuGet package – Microsoft.Bot.Builder.CognitiveServices. Open your project’s NuGet package manager, where you can search for this package and install it to your project. This package will allow you to connect your bot with a QnA service.

This library is open source and available on Github here and also supports the Node.js SDK.

Connecting to the QnA service

In your project’s Web.config, in appSettings add the following highlighted keys – QnaSubscriptionKey and QnaKnowledgebaseId. The values for these keys are be the Id and Key provided by your QnA service upon successful deployment (see above) which we saved earlier when we created QnA service.

Next we’ll create a new Dialog for our bot – QnaDialog.cs

using System;
using System.Threading.Tasks;
using System.Linq;
using System.Configuration;
using Microsoft.Bot.Connector;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.CognitiveServices.QnAMaker;

namespace Qna_Rich_Cards.Dialogs
{
    [Serializable]
    public class QnaDialog : QnAMakerDialog
    {
        public QnaDialog() : base(new QnAMakerService(new QnAMakerAttribute(ConfigurationManager.AppSettings["QnaSubscriptionKey"], ConfigurationManager.AppSettings["QnaKnowledgebaseId"],"Sorry, I couldn't find an answer for that", 0.5)))
        {
        }

    }
}

This new dialog will inherit from the QnAMakerDialog class from the bot builder cognitive services NuGet package we installed earlier. Next, go into  MessagesController.cs and edit the Post method as shown:

public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
    if (activity.Type == ActivityTypes.Message)
    {
        await Conversation.SendAsync(activity, () => new Dialogs.QnaDialog());
        //  await Conversation.SendAsync(activity, () => new Dialogs.RootDialog());
    }
    else
    {
        HandleSystemMessage(activity);
    }
    var response = Request.CreateResponse(HttpStatusCode.OK);
    return response;
}

The only thing done in the code snippet above is we commented out the default call to the RootDialog (provided by the Bot Application Template), and we’re passing all incoming message activity to the QnaDialog.

Note: It is up to the developer to determine how their bot dialogs and conversation flow are managed, this is simply a quick way for our bot demo to call the QnA service but it is not necessarily how you should manage this dialog in production. 

At this point, everything we need to run the bot and interact with our QnA service is ready. Launching our bot and running the emulator locally, we can interact with the bot to verify that our QnA service is working properly.

So far, so good! Since our demo bot only is only using one dialog (QnADialog), we can be certain that response activity that we see in the emulator is coming from the QnA service. The service also provides natural language processing behind the scenes, note that the last post to the bot (tell me about bot framework) is not the exact question in the knowledge base. You’ll even notice that basic markdown formatting and hyper links are supported for web chat, although this may vary depending on the channel your bot is connected to. While this is great, having simple text and markdown formatting may not be enough. As modern bot developers, we also want to provide a good experience for our users.

Adding Rich Cards

Click here to read more about cards and attachments.

In order to add UI elements such as cards to responses from the QnA service, we need a way to intercept the response from the bot before it is posted back to the user in the channel. This must be done by the bot, as the QnA service can only respond with a QnAMakerResult. Recall that the QnaDialog we created for this bot inherits from the QnAMakerDialog class, shown below.

using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;

namespace Microsoft.Bot.Builder.CognitiveServices.QnAMaker
{
    // Summary:
    //     A dialog specialized to handle QnA response from QnA Maker.
    public class QnAMakerDialog : IDialog<IMessageActivity>
    {
        protected readonly IQnAService[] services;

        public QnAMakerDialog(params IQnAService[] services);

        public IQnAService[] MakeServicesFromAttributes();

        public Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> argument);
        protected virtual Task DefaultWaitNextMessageAsync(IDialogContext context, IMessageActivity message, QnAMakerResults result);
        protected virtual bool IsConfidentAnswer(QnAMakerResults qnaMakerResults);
        protected virtual Task QnAFeedbackStepAsync(IDialogContext context, QnAMakerResults qnaMakerResults);
        protected virtual Task RespondFromQnAMakerResultAsync(IDialogContext context, IMessageActivity message, QnAMakerResults result);
    }
}

RespondFromQnAMakerResultAsync is the method which handles the response from the QnA service, and in order to access that response, we’ll need to implement an override. Back in the QnA Dialog, we can set it up like this:

protected override async Task RespondFromQnAMakerResultAsync(IDialogContext context, IMessageActivity message, QnAMakerResults result)
{
    // Add code to format QnAMakerResults 'result' 
}

Before adding any more code, we need to revisit our QnA service. The initial question answer pairs were we entered into the knowledge base were very direct – we entered a question, and provided the exact answer that we wanted a user to see from the service. Since our goal is to format the response into a more visually appealing UI card for users, we need to re-think the way we are storing answers. An answer we store can be a simple text string, but a UI card is often composed of more elements of data. There could be a title, description, image, links, etc. all of which together provide the data we need to create a card attachment. So, what we can do is use our knowledge base to not just store an answer, but we can also use the QnA service to store data. Below, we’ll train and re-publish our knowledge base with one more question-answer pair, only this time we’ll be formatting our answer to store more data.

In our new question-answer pair, we are storing a title, description, URL, and image link to be rendered to our card, separated by semicolons. This is simply one format which is possible, you could also store your data in JSON format. However, it is recommended that you maintain a consistent format for your data in the knowledge base, for reasons which will become apparent soon as we develop the code in our demo bot.

Back in our bot’s code, we were about to override the RespondFromQnAMakerResultAsync method inherited from the QnAMakerDialog class. Provided below is an implementation of a hero card, using the data format we chose in our knowledge base.

protected override async Task RespondFromQnAMakerResultAsync(IDialogContext context, IMessageActivity message, QnAMakerResults result)
{
    // answer is a string
    var answer = result.Answers.First().Answer;

    Activity reply = ((Activity)context.Activity).CreateReply();

    string[] qnaAnswerData = answer.Split(';');

    string title = qnaAnswerData[0];
    string description = qnaAnswerData[1];
    string url = qnaAnswerData[2];
    string imageURL = qnaAnswerData[3];

    HeroCard card = new HeroCard
    {
        Title = title,
        Subtitle = description,
    };

    card.Buttons = new List<CardAction>
    {
        new CardAction(ActionTypes.OpenUrl, "Learn More", value: url)
    };

    card.Images = new List<CardImage>
    {
        new CardImage( url = imageURL)
    };

    reply.Attachments.Add(card.ToAttachment());

    await context.PostAsync(reply);
}

Here’s a breakdown of this implementation:

  1. The QnA service will respond with a result, which we use to obtain a text answer and store it in a string variable, ‘answer’
  2. We create an Activity ‘reply’ by using the current bot context, and invoking the CreateReply() method, which will create a reply message for the existing message on the dialog stack.
  3. Because we defined our answer format previously in our knowledge base, we know that the data we want to capture is stored a string elements separated by semicolons (‘;’), so we can use a simple string split to store each property separately.
  4.  A new HeroCard object called ‘card‘ is created, the card is then formatted using the information from the QnA answer.
  5.  The card is added to the reply activity as an attachment, before being posted back to the user.

Re-start the bot and emulator, and ask the latest question added to the knowledge base.

Awesome! We’re now successfully using our QnA service to not only serve answers, but to store data which we can use in a meaningful way. As mentioned before, it is recommended that a consistent data format is used in the QnA service. The card was format defined in our bot was specific to the format we used in the knowledge base. If multiple data formats were defined, more logic would need to be implemented to handle differences in data formats the QnA service could respond with.

JSON data in QnA

As mentioned above, while the answers stored in a QnA knowledge base are text based you may still choose whichever data format you’d like. To demonstrate this we’ll add one more question-answer pair to the QnA service, this time using JSON format for our answer data.

Back in our bot, we can define a new helper class to store our answer data per the following:

public class JsonQnaAnswer
{
    public string title { get; set; }
    public string desc { get; set; }
    public string url { get; set; }
}

In the override implementation of RespondFromQnAMakerResultAsync, since we are expecting a JSON format, we can leverage the Newtonsoft.Json library to parse the answer from the QnA service, and map the values to an instance of the helper class.

protected override async Task RespondFromQnAMakerResultAsync(IDialogContext context, IMessageActivity message, QnAMakerResults result)
{
    JsonQnaAnswer qnaAnswer = new JsonQnaAnswer();
    Activity reply = ((Activity)context.Activity).CreateReply();
    var response = JObject.Parse(answer);

    qnaAnswer.title = response.Value<string>("title");
    qnaAnswer.desc = response.Value<string>("desc");
    qnaAnswer.url = response.Value<string>("url");

    ThumbnailCard card = new ThumbnailCard()
    {
        Title = qnaAnswer.title,
        Subtitle = qnaAnswer.desc,
        Buttons = new List<CardAction>
        {
            new CardAction(ActionTypes.OpenUrl, "Click Here", value: qnaAnswer.url)
        }
    };

    reply.Attachments.Add(card.ToAttachment());

    await context.PostAsync(reply);
}

In this sample we used a thumbnail card, and added it to the reply activity before posting the response back to the user. Re-starting our bot and the emulator to try out our new card implementation to verify that everything works.

Great! Hopefully, we’ve demonstrated that you can store your data in your knowledge base however you’d wish, and that there are many ways you can customize that data to display to users. We encourage you to experiment with the QnA service and not only different types of cards, but also different types of rich media attachments such as speech and video.

Summary

In this article we provided an introduction to the QnA maker, and how you can use it to easily train and publish a service with knowledge base of question-answer pairs for your bots. We also demonstrated how  leverage the bot builder cognitive services NuGet package to connect a bot to a QnA service. The QnA knowledge base is flexible enough to allow you to store not only answers, but data as different text formats. Overriding the existing QnA maker dialog implementation, your bot can use the data from the knowledge base in the service to create a richer experience for your users.

Click here to view the sample code for this demo on Github

Happy Making!

Matthew Shim from the Bot Framework Team.