diff --git a/libraries/Microsoft.Bot.Builder.AI.QnA/Dialogs/QnAMakerDialog.cs b/libraries/Microsoft.Bot.Builder.AI.QnA/Dialogs/QnAMakerDialog.cs
index c777c3b02..c98b8374f 100644
--- a/libraries/Microsoft.Bot.Builder.AI.QnA/Dialogs/QnAMakerDialog.cs
+++ b/libraries/Microsoft.Bot.Builder.AI.QnA/Dialogs/QnAMakerDialog.cs
@@ -102,6 +102,7 @@ namespace Microsoft.Bot.Builder.AI.QnA.Dialogs
/// of the source file that contains the caller.
/// The line number, for debugging. Defaults to the line number
/// in the source file at which the method is called.
+ /// Boolean value to determine whether an Adaptive card formatted for Teams should be used for responses.
public QnAMakerDialog(
string dialogId,
string knowledgeBaseId,
@@ -118,7 +119,8 @@ namespace Microsoft.Bot.Builder.AI.QnA.Dialogs
ServiceType qnAServiceType = ServiceType.QnAMaker,
HttpClient httpClient = null,
[CallerFilePath] string sourceFilePath = "",
- [CallerLineNumber] int sourceLineNumber = 0)
+ [CallerLineNumber] int sourceLineNumber = 0,
+ bool useTeamsAdaptiveCard = false)
: base(dialogId)
{
this.RegisterSourceLocation(sourceFilePath, sourceLineNumber);
@@ -135,6 +137,7 @@ namespace Microsoft.Bot.Builder.AI.QnA.Dialogs
Filters = filters;
QnAServiceType = qnAServiceType;
this.HttpClient = httpClient;
+ this.UseTeamsAdaptiveCard = useTeamsAdaptiveCard;
// add waterfall steps
this.AddStep(CallGenerateAnswerAsync);
@@ -169,6 +172,7 @@ namespace Microsoft.Bot.Builder.AI.QnA.Dialogs
/// of the source file that contains the caller.
/// The line number, for debugging. Defaults to the line number
/// in the source file at which the method is called.
+ /// Boolean value to determine whether an Adaptive card formatted for Teams should be used for responses.
public QnAMakerDialog(
string knowledgeBaseId,
string endpointKey,
@@ -184,7 +188,8 @@ namespace Microsoft.Bot.Builder.AI.QnA.Dialogs
ServiceType qnAServiceType = ServiceType.QnAMaker,
HttpClient httpClient = null,
[CallerFilePath] string sourceFilePath = "",
- [CallerLineNumber] int sourceLineNumber = 0)
+ [CallerLineNumber] int sourceLineNumber = 0,
+ bool useTeamsAdaptiveCard = false)
: this(
nameof(QnAMakerDialog),
knowledgeBaseId,
@@ -201,7 +206,8 @@ namespace Microsoft.Bot.Builder.AI.QnA.Dialogs
qnAServiceType,
httpClient,
sourceFilePath,
- sourceLineNumber)
+ sourceLineNumber,
+ useTeamsAdaptiveCard)
{
}
@@ -395,6 +401,15 @@ namespace Microsoft.Bot.Builder.AI.QnA.Dialogs
[JsonProperty("displayPreciseAnswerOnly")]
public BoolExpression DisplayPreciseAnswerOnly { get; set; } = false;
+ ///
+ /// Gets or sets a value indicating whether the dialog response should use a MS Teams formatted Adaptive Card instead of a Hero Card.
+ ///
+ ///
+ /// True/False, defaults to False.
+ ///
+ [JsonProperty("useTeamsAdaptiveCard")]
+ public BoolExpression UseTeamsAdaptiveCard { get; set; } = false;
+
///
/// Gets or sets QnA Service type to query either QnAMaker or Custom Question Answering Knowledge Base.
///
@@ -614,7 +629,7 @@ namespace Microsoft.Bot.Builder.AI.QnA.Dialogs
var response = (List)stepContext.Result;
if (response.Count > 0 && response[0].Id != -1)
{
- var message = QnACardBuilder.GetQnADefaultResponse(response.First(), dialogOptions.ResponseOptions.DisplayPreciseAnswerOnly);
+ var message = QnACardBuilder.GetQnADefaultResponse(response.First(), dialogOptions.ResponseOptions.DisplayPreciseAnswerOnly, UseTeamsAdaptiveCard);
await stepContext.Context.SendActivityAsync(message).ConfigureAwait(false);
}
else
@@ -629,7 +644,7 @@ namespace Microsoft.Bot.Builder.AI.QnA.Dialogs
if (response.Count == 1 && response[0].Id == -1)
{
// Nomatch Response from service.
- var message = QnACardBuilder.GetQnADefaultResponse(response.First(), dialogOptions.ResponseOptions.DisplayPreciseAnswerOnly);
+ var message = QnACardBuilder.GetQnADefaultResponse(response.First(), dialogOptions.ResponseOptions.DisplayPreciseAnswerOnly, UseTeamsAdaptiveCard);
await stepContext.Context.SendActivityAsync(message).ConfigureAwait(false);
}
else
@@ -839,7 +854,7 @@ namespace Microsoft.Bot.Builder.AI.QnA.Dialogs
ObjectPath.SetPathValue(stepContext.ActiveDialog.State, Options, dialogOptions);
// Get multi-turn prompts card activity.
- var message = QnACardBuilder.GetQnADefaultResponse(answer, dialogOptions.ResponseOptions.DisplayPreciseAnswerOnly);
+ var message = QnACardBuilder.GetQnADefaultResponse(answer, dialogOptions.ResponseOptions.DisplayPreciseAnswerOnly, UseTeamsAdaptiveCard);
await stepContext.Context.SendActivityAsync(message).ConfigureAwait(false);
return new DialogTurnResult(DialogTurnStatus.Waiting);
diff --git a/libraries/Microsoft.Bot.Builder.AI.QnA/Utils/QnACardBuilder.cs b/libraries/Microsoft.Bot.Builder.AI.QnA/Utils/QnACardBuilder.cs
index 1b2b41622..8001a31e4 100644
--- a/libraries/Microsoft.Bot.Builder.AI.QnA/Utils/QnACardBuilder.cs
+++ b/libraries/Microsoft.Bot.Builder.AI.QnA/Utils/QnACardBuilder.cs
@@ -94,8 +94,9 @@ namespace Microsoft.Bot.Builder.AI.QnA
///
/// Result to be dispalyed as prompts.
/// Choice to render precise answer.
+ /// Choose whether to use a Teams-formatted Adaptive card.
/// IMessageActivity.
- public static IMessageActivity GetQnADefaultResponse(QueryResult result, BoolExpression displayPreciseAnswerOnly)
+ public static IMessageActivity GetQnADefaultResponse(QueryResult result, BoolExpression displayPreciseAnswerOnly, BoolExpression useTeamsAdaptiveCard = null)
{
if (result == null)
{
@@ -126,7 +127,7 @@ namespace Microsoft.Bot.Builder.AI.QnA
}
}
- string heroCardText = null;
+ string cardText = null;
if (!string.IsNullOrWhiteSpace(result?.AnswerSpan?.Text))
{
chatActivity.Text = result.AnswerSpan.Text;
@@ -134,31 +135,122 @@ namespace Microsoft.Bot.Builder.AI.QnA
// For content choice Precise only
if (displayPreciseAnswerOnly.Value == false)
{
- heroCardText = result.Answer;
+ cardText = result.Answer;
}
}
- if (buttonList != null || !string.IsNullOrWhiteSpace(heroCardText))
+ if (buttonList != null || !string.IsNullOrWhiteSpace(cardText))
{
- var plCard = new HeroCard();
+ var useAdaptive = useTeamsAdaptiveCard == null ? false : useTeamsAdaptiveCard.Value;
+ var cardAttachment = useAdaptive ? CreateAdaptiveCardAttachment(cardText, buttonList) : CreateHeroCardAttachment(cardText, buttonList);
- if (buttonList != null)
- {
- plCard.Buttons = buttonList;
- }
-
- if (!string.IsNullOrWhiteSpace(heroCardText))
- {
- plCard.Text = heroCardText;
- }
-
- // Create the attachment.
- var attachment = plCard.ToAttachment();
-
- chatActivity.Attachments.Add(attachment);
+ chatActivity.Attachments.Add(cardAttachment);
}
return chatActivity;
}
+
+ ///
+ /// Get a Teams-formatted Adaptive Card as Attachment to be returned in the QnA response. Max width and height of response are controlled by Teams.
+ ///
+ /// String of text to be added to the card.
+ /// List of CardAction representing buttons to be added to the card.
+ /// Attachment.
+ private static Attachment CreateAdaptiveCardAttachment(string cardText, List buttonList)
+ {
+ // If there are buttons, create an array of buttons for the card.
+ // Each button is represented by a Dictionary containing the required fields for each button.
+ var cardButtons = buttonList?.Select(button =>
+ new Dictionary
+ {
+ { "type", "Action.Submit" },
+ { "title", button.Title },
+ {
+ "data",
+ new Dictionary
+ {
+ {
+ "msteams",
+ new Dictionary
+ {
+ { "type", "messageBack" },
+ { "displayText", button.DisplayText },
+ { "text", button.Text },
+ { "value", button.Value }
+ }
+ }
+ }
+ }
+ }).ToArray();
+
+ // Create a dictionary to represent the completed Adaptive card
+ // msteams field is also a dictionary
+ // body field is an array containing a dictionary
+ var card = new Dictionary
+ {
+ { "$schema", "http://adaptivecards.io/schemas/adaptive-card.json" },
+ { "type", "AdaptiveCard" },
+ { "version", "1.3" },
+ {
+ "msteams",
+ new Dictionary
+ {
+ { "width", "full" },
+ { "height", "full" }
+ }
+ },
+ {
+ "body",
+ new Dictionary[]
+ {
+ new Dictionary
+ {
+ { "type", "TextBlock" },
+ { "text", (!string.IsNullOrWhiteSpace(cardText) ? cardText : string.Empty) }
+ }
+ }
+ }
+ };
+
+ // If there are buttons, add the buttons array to the card. "actions" must be formatted as an array.
+ if (cardButtons != null)
+ {
+ card.Add("actions", cardButtons);
+ }
+
+ // Create and return the card as an attachment
+ var adaptiveCard = new Attachment()
+ {
+ ContentType = "application/vnd.microsoft.card.adaptive",
+ Content = card
+ };
+
+ return adaptiveCard;
+ }
+
+ ///
+ /// Get a Hero Card as Attachment to be returned in the QnA response.
+ ///
+ /// string of text to be added to the card.
+ /// List of CardAction representing buttons to be added to the card.
+ /// Attachment.
+ private static Attachment CreateHeroCardAttachment(string cardText, List buttonList)
+ {
+ // Create a new hero card, add the text and buttons if they exist
+ var card = new HeroCard();
+
+ if (buttonList != null)
+ {
+ card.Buttons = buttonList;
+ }
+
+ if (!string.IsNullOrWhiteSpace(cardText))
+ {
+ card.Text = cardText;
+ }
+
+ // Return the card as an attachment
+ return card.ToAttachment();
+ }
}
}
diff --git a/tests/Microsoft.Bot.Builder.AI.QnA.Tests/QnAMakerTests.cs b/tests/Microsoft.Bot.Builder.AI.QnA.Tests/QnAMakerTests.cs
index 1f3758f53..01e9c27c4 100644
--- a/tests/Microsoft.Bot.Builder.AI.QnA.Tests/QnAMakerTests.cs
+++ b/tests/Microsoft.Bot.Builder.AI.QnA.Tests/QnAMakerTests.cs
@@ -189,6 +189,145 @@ namespace Microsoft.Bot.Builder.AI.Tests
.StartTestAsync();
}
+ ///
+ /// The QnAMakerAction_ActiveLearningDialogBase_AdaptiveCard. Tests QnAMakerDialog with Adaptive Cards.
+ ///
+ /// The .
+ public AdaptiveDialog QnAMakerAction_ActiveLearningDialogBase_AdaptiveCard()
+ {
+ var mockHttp = new MockHttpMessageHandler();
+ mockHttp.When(HttpMethod.Post, GetTrainRequestUrl())
+ .Respond(HttpStatusCode.NoContent, "application/json", "{ }");
+ mockHttp.When(HttpMethod.Post, GetRequestUrl()).WithContent("{\"question\":\"Q12\",\"top\":3,\"strictFilters\":[],\"scoreThreshold\":30.0,\"context\":{\"previousQnAId\":0,\"previousUserQuery\":\"\"},\"qnaId\":0,\"isTest\":false,\"rankerType\":\"Default\",\"StrictFiltersCompoundOperationType\":0}")
+ .Respond("application/json", GetResponse("QnaMaker_ReturnsAnswer_WhenNoAnswerFoundInKb.json"));
+ mockHttp.When(HttpMethod.Post, GetRequestUrl()).WithContent("{\"question\":\"Q11\",\"top\":3,\"strictFilters\":[],\"scoreThreshold\":30.0,\"context\":{\"previousQnAId\":0,\"previousUserQuery\":\"\"},\"qnaId\":0,\"isTest\":false,\"rankerType\":\"Default\",\"StrictFiltersCompoundOperationType\":0}")
+ .Respond("application/json", GetResponse("QnaMaker_TopNAnswer.json"));
+ return CreateQnAMakerActionDialog_AdaptiveCard(mockHttp);
+ }
+
+ ///
+ /// The QnAMakerAction_ActiveLearningDialog_WithProperResponse_AdaptiveCard. Tests QnAMakerDialog with Adaptive Cards.
+ ///
+ /// The .
+ [Fact]
+ public async Task QnAMakerAction_ActiveLearningDialog_WithProperResponse_AdaptiveCard()
+ {
+ var rootDialog = QnAMakerAction_ActiveLearningDialogBase_AdaptiveCard();
+
+ var suggestionList = new List { "Q1", "Q2", "Q3" };
+ var suggestionActivity = QnACardBuilder.GetSuggestionsCard(suggestionList, "Did you mean:", "None of the above.");
+ var qnAMakerCardEqualityComparer = new QnAMakerCardEqualityComparer();
+
+ await CreateFlow(rootDialog, "QnAMakerAction_ActiveLearningDialog_WithProperResponse")
+ .Send("Q11")
+ .AssertReply(suggestionActivity, equalityComparer: qnAMakerCardEqualityComparer)
+ .Send("Q1")
+ .AssertReply("A1")
+ .StartTestAsync();
+ }
+
+ ///
+ /// The QnAMakerAction_ActiveLearningDialog_WithNoResponse_AdaptiveCard. Tests QnAMakerDialog with Adaptive Cards.
+ ///
+ /// The .
+ [Fact]
+ public async Task QnAMakerAction_ActiveLearningDialog_WithNoResponse_AdaptiveCard()
+ {
+ var rootDialog = QnAMakerAction_ActiveLearningDialogBase_AdaptiveCard();
+
+ const string noAnswerActivity = "No match found, please ask another question.";
+
+ var suggestionList = new List { "Q1", "Q2", "Q3" };
+ var suggestionActivity = QnACardBuilder.GetSuggestionsCard(suggestionList, "Did you mean:", "None of the above.");
+ var qnAMakerCardEqualityComparer = new QnAMakerCardEqualityComparer();
+
+ await CreateFlow(rootDialog, "QnAMakerAction_ActiveLearningDialog_WithNoResponse")
+ .Send("Q11")
+ .AssertReply(suggestionActivity, equalityComparer: qnAMakerCardEqualityComparer)
+ .Send("Q12")
+ .AssertReply(noAnswerActivity)
+ .StartTestAsync();
+ }
+
+ ///
+ /// The QnAMakerAction_ActiveLearningDialog_WithNoneOfAboveQuery_AdaptiveCard. Tests QnAMakerDialog with Adaptive Cards.
+ ///
+ /// The .
+ [Fact]
+ public async Task QnAMakerAction_ActiveLearningDialog_WithNoneOfAboveQuery_AdaptiveCard()
+ {
+ var rootDialog = QnAMakerAction_ActiveLearningDialogBase_AdaptiveCard();
+
+ var suggestionList = new List { "Q1", "Q2", "Q3" };
+ var suggestionActivity = QnACardBuilder.GetSuggestionsCard(suggestionList, "Did you mean:", "None of the above.");
+ var qnAMakerCardEqualityComparer = new QnAMakerCardEqualityComparer();
+
+ await CreateFlow(rootDialog, "QnAMakerAction_ActiveLearningDialog_WithNoneOfAboveQuery")
+ .Send("Q11")
+ .AssertReply(suggestionActivity, equalityComparer: qnAMakerCardEqualityComparer)
+ .Send("None of the above.")
+ .AssertReply("Thanks for the feedback.")
+ .StartTestAsync();
+ }
+
+ ///
+ /// The QnAMakerAction_MultiTurnDialogBase_AdaptiveCard. Tests QnAMakerDialog with Adaptive Cards.
+ ///
+ /// The .
+ public AdaptiveDialog QnAMakerAction_MultiTurnDialogBase_AdaptiveCard()
+ {
+ var mockHttp = new MockHttpMessageHandler();
+
+ mockHttp.When(HttpMethod.Post, GetRequestUrl()).WithContent("{\"question\":\"I have issues related to KB\",\"top\":3,\"strictFilters\":[],\"scoreThreshold\":30.0,\"context\":{\"previousQnAId\":0,\"previousUserQuery\":\"\"},\"qnaId\":0,\"isTest\":false,\"rankerType\":\"Default\",\"StrictFiltersCompoundOperationType\":0}")
+ .Respond("application/json", GetResponse("QnaMaker_ReturnAnswer_withPrompts.json"));
+ mockHttp.When(HttpMethod.Post, GetRequestUrl()).WithContent("{\"question\":\"Accidently deleted KB\",\"top\":3,\"strictFilters\":[],\"scoreThreshold\":30.0,\"context\":{\"previousQnAId\":27,\"previousUserQuery\":\"\"},\"qnaId\":1,\"isTest\":false,\"rankerType\":\"Default\",\"StrictFiltersCompoundOperationType\":0}")
+ .Respond("application/json", GetResponse("QnaMaker_ReturnAnswer_MultiTurnLevel1.json"));
+
+ return CreateQnAMakerActionDialog_AdaptiveCard(mockHttp);
+ }
+
+ ///
+ /// The QnAMakerAction_MultiTurnDialogBase_WithAnswer_AdaptiveCard. Tests QnAMakerDialog with Adaptive Cards.
+ ///
+ /// The .
+ [Fact]
+ public async Task QnAMakerAction_MultiTurnDialogBase_WithAnswer_AdaptiveCard()
+ {
+ var rootDialog = QnAMakerAction_MultiTurnDialogBase_AdaptiveCard();
+
+ var response = JsonConvert.DeserializeObject(File.ReadAllText(GetFilePath("QnaMaker_ReturnAnswer_withPrompts.json")));
+ var promptsActivity = QnACardBuilder.GetQnAPromptsCard(response.Answers[0]);
+ var qnAMakerCardEqualityComparer = new QnAMakerCardEqualityComparer();
+
+ await CreateFlow(rootDialog, nameof(QnAMakerAction_MultiTurnDialogBase_WithAnswer_AdaptiveCard))
+ .Send("I have issues related to KB")
+ .AssertReply(promptsActivity, equalityComparer: qnAMakerCardEqualityComparer)
+ .Send("Accidently deleted KB")
+ .AssertReply("All deletes are permanent, including question and answer pairs, files, URLs, custom questions and answers, knowledge bases, or Azure resources. Make sure you export your knowledge base from the Settings**page before deleting any part of your knowledge base.")
+ .StartTestAsync();
+ }
+
+ ///
+ /// The QnAMakerAction_MultiTurnDialogBase_WithNoAnswer_AdaptiveCard. Tests QnAMakerDialog with Adaptive Cards.
+ ///
+ /// The .
+ [Fact]
+ public async Task QnAMakerAction_MultiTurnDialogBase_WithNoAnswer_AdaptiveCard()
+ {
+ var rootDialog = QnAMakerAction_MultiTurnDialogBase_AdaptiveCard();
+
+ var response = JsonConvert.DeserializeObject(File.ReadAllText(GetFilePath("QnaMaker_ReturnAnswer_withPrompts.json")));
+ var promptsActivity = QnACardBuilder.GetQnAPromptsCard(response.Answers[0]);
+ var qnAMakerCardEqualityComparer = new QnAMakerCardEqualityComparer();
+
+ await CreateFlow(rootDialog, nameof(QnAMakerAction_MultiTurnDialogBase_WithNoAnswer_AdaptiveCard))
+ .Send("I have issues related to KB")
+ .AssertReply(promptsActivity, equalityComparer: qnAMakerCardEqualityComparer)
+ .Send("None of the above.")
+ .AssertReply("Thanks for the feedback.")
+ .StartTestAsync();
+ }
+
///
/// The QnaMaker_TraceActivity.
///
@@ -1972,6 +2111,72 @@ namespace Microsoft.Bot.Builder.AI.Tests
return rootDialog;
}
+ ///
+ /// The CreateQnAMakerActionDialog_AdaptiveCard. Tests Adaptive Cards in QnAMakerDialog.
+ ///
+ /// The mockHttp.
+ /// The .
+ private AdaptiveDialog CreateQnAMakerActionDialog_AdaptiveCard(MockHttpMessageHandler mockHttp)
+ {
+ var client = new HttpClient(mockHttp);
+
+ var noAnswerActivity = new ActivityTemplate("No match found, please ask another question.");
+ const string host = "https://dummy-hostname.azurewebsites.net/qnamaker";
+ const string knowledgeBaseId = "dummy-id";
+ const string endpointKey = "dummy-key";
+ const string activeLearningCardTitle = "QnAMaker Active Learning";
+
+ var outerDialog = new AdaptiveDialog("outer")
+ {
+ AutoEndDialog = false,
+ Triggers = new List
+ {
+ new OnBeginDialog
+ {
+ Actions = new List