QnA Maker is a great tool for quickly standing up a knowledge base. It has the ability to scan existing FAQs, upload XLSX files containing Question & Answer pairs, or just manually editing Q&A pairs. Once configured, it's trivial to set up and publish your first chat bot in a matter of minutes.
However, this approach doesn't allow for using some of the cool features that Bot Framework supports, including Rich Cards, Adaptive Cards, Conversation History Logging, or Proactive messaging (e.g. pinging the user when they've been idle for a bit to see if they're still engaged). This tutorial will show how to set up your initial bot from a Q&A knowledge base, and add in some of these features.
- Browse to https://portal.azure.com
- Click the
+ Create Resource
button, then search forWeb App Bot
- Choose
Web App Bot
from the search results, and then click the 'Create' button - Enter in a unique
Bot name
- Choose the
Subscription
you want to host this bot in - Enter a name for your new
Resource Group
, or choose an existing one - Select your
Location
for your bot to be hosted - Choose a
Pricing Tier
- Click on the
Bot template
setting, and choose theQuestion and Answer
tile, then click theSelect
button - Select an App Service Plan (choose either and existing one, or create a new one)
- Leave the defaults for
Azure Storage
andApplication Insights
(you can turn these off if you want, but generally you want them both on) - Click
Create
to create your new bot
Now that you've created your new bot, you need to update the configuration to point to your QnA Knowledge Base
- In the Azure Portal, click on
Resource Groups
, and choose the resource group that you created in Step 1 above. - Find the
Web App Bot
, click on it - Test the bot by clicking on the
Test in Web Chat
page underBot Management
- Enter anything into the input field, and submit. Confirm that the response back is something like
Please set QnAKnowledgebaseId and QnASubscriptionKey in App Settings. Get them at https://qnamaker.ai.
- Choose the
Application Settings
page underApp Service Settings
, then scroll down until you find theQnAKnowledgebaseId
andQnASubscriptionKey
fields underApp Settings
-
Open a new tab, and browse to https://qnamaker.ai
-
Click on
My Services
, then click the Edit (i.e. pencil) icon to choose your QnA knowledge base -
Click "Settings", and then scroll down until you find the section marked
Deployment details
. In it, you'll see something like this: -
On the first line (starting with POST), copy the GUID from the sample URL (e.g. copy d4c9b8de-d4c0-4c5f-8fad-f73eade09340 from
/knowledgebases/d4c9b8de-d4c0-4c5f-8fad-f73eade09340/generateAnswer
-
Return to the Azure, and paste the GUID copied in the previous step into the
QnAKnowledgebaseId
field -
Return to the QnA Maker settings, and locate the line beginning with
Ocp-Apim-Subscription-Key
, and copy the Subscription key (e.g. 19af6e2d4cfa4c2ab28f337d05d7730e fromOcp-Apim-Subscription-Key: 19af6e2d4cfa4c2ab28f337d05d7730e
) -
Return to the Azure Portal, and past the Subscription Key into the
QnASubscriptionKey
field -
Click the
Save
button at the top of theApplication Settings
blade
You've now configured your bot to be able to retrieve answers from your QnA Knowledge Base. You can test your bot by returning to the Test in Web Chat
blade, and sending another message. You should now return an answer that has been pulled from your QnA Maker Knowledge Base.
Now that your bot is configured you can embed it in your website. We're not going to cover everything involved in actually embedding the chat in your site, but for testing purposes, we will create a quick HTML page that you can use locally to test the bot
- Anywhere on your local machine, create a new empty text file called
WebChat.html
, then past in the following code:
<head>
<style type="text/css">
body {
margin: 0;
overflow: hidden;
}
#iframe1 {
position:absolute;
left: 0px;
width: 100%;
top: 0px;
height: 100%;
}
</style>
</head>
<body>
<iframe id="iframe1" frameborder="0" src='https://webchat.botframework.com/embed/pot-bot-deploy?s=YOUR_SECRET_HERE'></iframe>
</body>
</html>
- Browse to your bot app, in the Azure Portal, and choose the
Channels
blade - Find
Web Chat
under theConnect to Channels
section, and click theEdit
link - Under the
Configure Web Chat
section, findSecret keys
, then click onShow
next to either of your secret keys. Copy the value from the field.
- Return to your
WebChat.html
file, and replaceYOUR_SECRET_HERE
with the secret key you copied in the previous step. - Open
WebChat.html
in the browser of your choice, and verify that you send a message, you get the expected response back from the bot.
If at this point, you're not getting expected responses back, you may have missed a step along the way. This is super common (there's a lot of detailed steps here). Just walk back through the steps carefully and make sure you've followed them exactly.
Note: The Bot Framework provides a high quality chat client that you can deploy to your website with no additional coding needed. However, some requirements might dictate that custom styles are applied to your bot. Fortunately, this Web Chat client is open source, and can be customized to your bots requirements. The source code for the web chat client can be found here, and and article showing how to update this client can be found here
Now that we've got all the plumbing in place, we're going to add some code to our app to return rich responses to the user. We'll focus specifically on VideoCard
and AdaptiveCard
, but you can easily extend the code to use any type of Rich Card.
- Browse to the
Build
page on your bot - Click the
Download zip file
to download the source code for your project. - Open the project in VS 2017 (you should have the latest stable release installed)
- Update all of your Nuget packages to the latest
- Add in the following Nuget Packages
- Microsoft.Bot.Builder.CognitiveServices
- Microsoft.AdaptiveCards
- Right click on your bot project, and add in a file called Utils.cs, and paste in the following code (make sure to update your namespace so the rest of your app can find the code)
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Web;
namespace YOUR_NAMESPACE_HERE
{
public class Utils
{
public static string GetAppSetting(string key)
{
#if DEBUG
return ConfigurationManager.AppSettings[key];
#else
return Microsoft.Bot.Builder.Azure.Utils.GetAppSetting(key);
#endif
}
}
}
This code will help you when you're debugging your bot locally, vs running it in Azure. For local testing, you'll compile in Debug
, and the code will read config settings from your web.config
file. When you deploy to Azure, you'll switch to Release
so you the code will instead read from App Settings
.
- Create a new file called
ConversationLogger.cs
, and paste in the following code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using Microsoft.Bot.Builder.History;
using Microsoft.Bot.Connector;
using System.Configuration;
using System.Data.SqlClient;
using System.Diagnostics;
namespace QnABot
{
public class ConversationLogger : IActivityLogger
{
public async Task LogAsync(IActivity activity)
{
try
{
var conversationId = activity.Conversation.Id;
var senderId = activity.From.Id;
var recipientId = activity.Recipient.Id;
var message = activity.AsMessageActivity();
var messageText = message.Text;
//If the response is a Rich/Adaptive card, then grab the appropriate attachment(s) text as well
if (message.Attachments.Count > 0)
{
//Grab the response from the message attachments
foreach (Attachment a in message.Attachments)
{
switch (a.ContentType)
{
case "application/vnd.microsoft.card.video":
messageText += (a.Content as VideoCard)?.Text;
break;
case "application/vnd.microsoft.card.adaptive":
//If you have important information to capture in that's actually inside the Adaptive card, insert logic here to parse that out of each TextBlock.
//messageText += (a.Content as AdaptiveCard)?.FallbackText;
//var adaptiveCard = a.Content as AdaptiveCard;
//foreach (CardElement item in adaptiveCard.Body)
//{
// if(item.GetType().Name == "TextBlock")
// {
// messageText += (item as TextBlock).Text;
// }
//}
break;
default:
break;
}
}
}
var timeStamp = DateTime.Now;
//ToDo: Make this read from Utils.GetSetting so it can be configured in Azure Portal
var connString = ConfigurationManager.ConnectionStrings["ChatHistory"].ConnectionString;
using (var conn = new SqlConnection(connString))
{
var cmd = new SqlCommand("INSERT INTO ConversationHistory(conversationId, senderId, recipientId, messageText, timeStamp) VALUES (@conversationId, @senderId, @recipientId, @messageText, @timeStamp)", conn);
cmd.Parameters.AddWithValue("@conversationId", conversationId);
cmd.Parameters.AddWithValue("@senderId", senderId);
cmd.Parameters.AddWithValue("@recipientId", recipientId);
cmd.Parameters.Add(new SqlParameter()
{
ParameterName = "@messageText",
Value = messageText,
SqlDbType = System.Data.SqlDbType.Text
});
cmd.Parameters.AddWithValue("@timeStamp", timeStamp);
conn.Open();
cmd.ExecuteNonQuery();
}
} catch (Exception ex) {
Debug.WriteLine(ex.ToString());
}
Debug.WriteLine($"From:{activity.From.Id} - To:{activity.Recipient.Id} - Message:{activity.AsMessageActivity()?.Text}");
}
}
}
- Open up your
WebApiConfig.cs
class file, and add the following line inside yourApplication_Start()
method
builder.RegisterType<ConversationLogger>().AsImplementedInterfaces().InstancePerDependency();
When finished, you code should look like this:
{
// Bot Storage: This is a great spot to register the private state storage for your bot.
// We provide adapters for Azure Table, CosmosDb, SQL Azure, or you can implement your own!
// For samples and documentation, see: https://github.com/Microsoft/BotBuilder-Azure
Conversation.UpdateContainer(
builder =>
{
//Inject the ConversationLogger object
builder.RegisterType<ConversationLogger>().AsImplementedInterfaces().InstancePerDependency();
builder.RegisterModule(new AzureModule(Assembly.GetExecutingAssembly()));
// Using Azure Table Storage
var store = new TableBotDataStore(ConfigurationManager.AppSettings["AzureWebJobsStorage"]); // requires Microsoft.BotBuilder.Azure Nuget package
// To use CosmosDb or InMemory storage instead of the default table storage, uncomment the corresponding line below
// var store = new DocumentDbBotDataStore("cosmos db uri", "cosmos db key"); // requires Microsoft.BotBuilder.Azure Nuget package
// var store = new InMemoryDataStore(); // volatile in-memory store
builder.Register(c => store)
.Keyed<IBotDataStore<BotData>>(AzureModule.Key_DataStore)
.AsSelf()
.SingleInstance();
});
GlobalConfiguration.Configure(WebApiConfig.Register);
}
}
Note: there are several different ways to log conversations with Bot Framework. This was specifically designed to make it easy to grab a flat files with all of the conversation history in it.
- Open up your
Dialogs\BasicQnAMakerDialog.cs
, and make sure the replace theRespondFromQnAMakerResultAsync
method with this:
{
// Add code to format QnAMakerResults 'result'
// answer is a string
var answer = result.Answers.First().Answer;
Activity reply = ((Activity)context.Activity).CreateReply();
reply.Text = answer;
try
{
string[] qnaAnswerData = answer.Split(';');
string[] supportedCardTypes = new string[] { "videocard" };
//If the string came back as ; separated, and the first item in the array is one of the supported rich card types
if (qnaAnswerData.Length > 1 && supportedCardTypes.Contains(qnaAnswerData[0].ToLower()))
{
string cardType = qnaAnswerData[0];
string title = qnaAnswerData[1];
string description = qnaAnswerData[2];
string videoUrl = qnaAnswerData[3];
string learnMoreUrl = qnaAnswerData[4];
string thumbnailUrl = qnaAnswerData[5];
switch (cardType)
{
case "VideoCard":
VideoCard videoCard = new VideoCard()
{
Title = title,
Text = description,
Image = new ThumbnailUrl(thumbnailUrl)
};
videoCard.Buttons = new List<CardAction>
{
new CardAction(ActionTypes.OpenUrl, "Learn More", value: learnMoreUrl)
};
videoCard.Media = new List<MediaUrl>
{
new MediaUrl(videoUrl)
};
reply.Attachments.Add(videoCard.ToAttachment());
//Clear out the text, no longer needed since Video Card replied
reply.Text = string.Empty;
break;
}
}
else
{
AdaptiveCard card = new AdaptiveCard();
//Set the fallback text in case someone sends a request from a client that doesn't yet support Adaptive Cards fully
//card.FallbackText = answer;
// Add text to the card.
//card.Body.Add(new TextBlock()
//{
// Text = answer,
// Wrap = true,
//});
// Add text to the card.
card.Body.Add(new TextBlock()
{
Text = "Was this answer helpful?",
Size = TextSize.Small
});
// Add buttons to the card.
card.Actions.Add(new SubmitAction()
{
Title = "Yes",
Data = "Yes, this was helpful"
});
card.Actions.Add(new SubmitAction()
{
Title = "No",
Data = "No, this was not helpful"
});
// Create the attachment.
Attachment attachment = new Attachment()
{
ContentType = AdaptiveCard.ContentType,
Content = card
};
reply.Attachments.Add(attachment);
}
}
catch (Exception ex)
{
reply.Text = ex.ToString();
}
await context.PostAsync(reply);
context.Wait(this.MessageReceivedAsync);
}
Note: If this method isn't there, add it inside the BasicQnAMakerDialog
class.
Make sure that you bot compiles and runs locally. Fix any namespaces, or using statements.
In this tutorial, we won't cover setting up a database in Azure. This will assume you already have an Azure SQL database configured, and have the connection string available
- Open up Sql Server Management Studio, and connect to your Azure SQL instance
- Browse to your database, and click
New query
- Paste the following create script into the query, and run, making sure your database is selected.
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[ConversationHistory](
[conversationId] [nchar](255) NOT NULL,
[senderId] [nchar](255) NOT NULL,
[recipientId] [nchar](255) NOT NULL,
[messageText] [text] NOT NULL,
[timeStamp] [datetime] NOT NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
- Open up the web.config in your project, and add in a section for your connection strings, just under the
</appSettings>
node
<connectionStrings>
<add name="ChatHistory" connectionString="YOUR_CONNECTION_STRING_HERE" />
</connectionStrings>
The code is already written to pull from this connection string, so you don't need to make any further changes here.
- In Visual Studio, right click on your project in the Solution Explorer, and click
Publish...
- The Publish Profile for your app was included in the source code, so all you should need to do is click the
Publish
button - Refresh your
WebChat.html
page in the browser, and confirm that you now see rich cards coming back in your response.
One of the things that is in the code is the ability to format your QnA pair in QnA maker in a way that your bot app can interpret it as a Video Card. How you format your QnA pairs to do this is up to you, but this shows one example.
- Return to your QnA Knowledge Base in https://qnamaker.ai
- In your Knowledge Base, click on the
Add QnA Pair
button, and place the following:
Question: Please show me the video
Answer: VideoCard;Big Buck Bunny;Big Buck Bunny (code-named Peach) is a short computer-animated comedy film by the Blender Institute, part of the Blender Foundation. Like the foundation\'s previous film Elephants Dream, the film was made using Blender, a free software application for animation made by the same foundation. It was released as an open-source film under Creative Commons License Attribution 3.0.;http://download.blender.org/peach/bigbuckbunny_movies/BigBuckBunny_320x180.mp4;https://peach.blender.org/;https://upload.wikimedia.org/wikipedia/commons/thumb/c/c5/Big_buck_bunny_poster_big.jpg/220px-Big_buck_bunny_poster_big.jpg
This string contains 6 parts:
- A directive (i.e.
VideoCard
) of what type of Rich Card to show - The title of the video
- A Description of the video
- The URL for the video
- The URL for a "Learn More" URL
- The URL for a thumbnail
The code in BasicQnAMakerDialog will look for this VideoCard
prefix, and pull out the necessary values.
string cardType = qnaAnswerData[0];
string title = qnaAnswerData[1];
string description = qnaAnswerData[2];
string videoUrl = qnaAnswerData[3];
string learnMoreUrl = qnaAnswerData[4];
string thumbnailUrl = qnaAnswerData[5];
Once these values are put into an array, the code will determine that the intent was to display a Video Card instead of an Adaptive Card, and present that back to the user.
Congratulations! You've gut the beginnings of an enhanced chat bot that uses a QnAMaker knowledge based as the source for it's responses. There is still quite a lot you can do with your bot. I recommend reading through the Bot Framework documentation here to learn more about what other features you can add to your bot.