Skip to content

Commit 4846119

Browse files
authored
Fixes issue #3960 [QnAMaker] [DotNet] Implementing QnAMaker's precise answering capability as an additional feature for BotFramework users (#3935)
* AnswerSpanRequest property in Request. * Answer span changes for request and response data sstructures * Request and Result object conversion * datatype definition change from boolean to bool for enable AnswerSpan * Text * spaces * MRCEnable instead of AnswerSpanRequest * Answerspan text set to answer for MRC * Mock changes * Review comments changes * Tests for both and precise * Update GetQnAPromptsCard invocation in tests * GetQnAPromptsContentCard method * Review comments * Request changes * refactoring * Refactoring Tests * Tests refactoring for comments * Added Copyright headers * Modified comment text for review comment. * Update QnAMakerDialog.cs * document text modified. * Copy right header formatting * Remove unused import * Revert "Added Copyright headers" This reverts commit b13d2eb. * Copyright header * Comment modified for grammar and clarity * Review comments * For fixing assembly difference issue * Revert "For fixing assembly difference issue" This reverts commit fdfcbe0. * For assemblies issue * Added purpose of property in summary. * Comments improved * For Review comments * For review comments * Spellings Syntax * Content corrections * Content comments changes * content request * content changes * Content changes for comments * Content review updates * Content changes * content chagnes * Revert enablePreciseAnswer to bool * removing json property for enable preccise * Reverting Recognizer changes * schema change and BoolExpression * Review comments * initialization of EnablePreciseAnswer * schema changes for Display Precise AnswerOnly * BoolExpression for DisplayPreciseAnswerOnly * bool property for displayPreciseAnswer * Updated comments * Null checks and CardEqualityComparer comments * Undo contenttype change. * format * Final Commit * Spaces formating. * Indentation for schema files.
1 parent a4718b6 commit 4846119

16 files changed

+336
-69
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Newtonsoft.Json;
5+
6+
namespace Microsoft.Bot.Builder.AI.QnA
7+
{
8+
/// <summary>
9+
/// This class helps user to opt for precise answer.
10+
/// </summary>
11+
public class AnswerSpanRequest
12+
{
13+
/// <summary>
14+
/// Gets or sets a value indicating whether to enable PreciseAnswer generation.
15+
/// </summary>
16+
/// <value>
17+
/// Choice whether to generate precise answer or not.
18+
/// </value>
19+
[JsonProperty("enable")]
20+
public bool Enable { get; set; }
21+
}
22+
}

libraries/Microsoft.Bot.Builder.AI.QnA/Dialogs/QnAMakerDialog.cs

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,24 @@ public QnAMakerDialog([CallerFilePath] string sourceFilePath = "", [CallerLineNu
290290
[JsonProperty("rankerType")]
291291
public StringExpression RankerType { get; set; } = new StringExpression(RankerTypes.DefaultRankerType);
292292

293+
/// <summary>
294+
/// Gets or sets a value indicating whether to enable PreciseAnswer generation.
295+
/// </summary>
296+
/// <value>
297+
/// Choice whether to generate precise answer or not.
298+
/// </value>
299+
[JsonProperty("enablePreciseAnswer")]
300+
public BoolExpression EnablePreciseAnswer { get; set; } = false;
301+
302+
/// <summary>
303+
/// Gets or sets a value indicating whether the user only wants to receive precise answer.
304+
/// </summary>
305+
/// <value>
306+
/// A value that indicates if user wants to receive full text along with the precise answer or not.
307+
/// </value>
308+
[JsonProperty("displayPreciseAnswerOnly")]
309+
public BoolExpression DisplayPreciseAnswerOnly { get; set; } = true;
310+
293311
/// <summary>
294312
/// Called when the dialog is started and pushed onto the dialog stack.
295313
/// </summary>
@@ -427,8 +445,9 @@ protected virtual Task<QnAMakerOptions> GetQnAMakerOptionsAsync(DialogContext dc
427445
Context = new QnARequestContext(),
428446
QnAId = 0,
429447
RankerType = this.RankerType?.GetValue(dc.State),
430-
IsTest = this.IsTest
431-
});
448+
IsTest = this.IsTest,
449+
EnablePreciseAnswer = this.EnablePreciseAnswer.GetValue(dc.State)
450+
});
432451
}
433452

434453
/// <summary>
@@ -437,14 +456,15 @@ protected virtual Task<QnAMakerOptions> GetQnAMakerOptionsAsync(DialogContext dc
437456
/// <param name="dc">The <see cref="DialogContext"/> for the current turn of conversation.</param>
438457
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
439458
/// <remarks>If the task is successful, the result contains the response options to use.</remarks>
440-
protected async virtual Task<QnADialogResponseOptions> GetQnAResponseOptionsAsync(DialogContext dc)
459+
protected virtual async Task<QnADialogResponseOptions> GetQnAResponseOptionsAsync(DialogContext dc)
441460
{
442461
return new QnADialogResponseOptions
443462
{
444463
NoAnswer = await this.NoAnswer.BindAsync(dc, dc.State).ConfigureAwait(false),
445464
ActiveLearningCardTitle = this.ActiveLearningCardTitle?.GetValue(dc.State) ?? DefaultCardTitle,
446465
CardNoMatchText = this.CardNoMatchText?.GetValue(dc.State) ?? DefaultCardNoMatchText,
447-
CardNoMatchResponse = await this.CardNoMatchResponse.BindAsync(dc).ConfigureAwait(false)
466+
CardNoMatchResponse = await this.CardNoMatchResponse.BindAsync(dc).ConfigureAwait(false),
467+
DisplayPreciseAnswerOnly = this.DisplayPreciseAnswerOnly.GetValue(dc.State)
448468
};
449469
}
450470

@@ -493,7 +513,7 @@ private async Task<DialogTurnResult> CallGenerateAnswerAsync(WaterfallStepContex
493513

494514
// Get active learning suggestion card activity.
495515
var message = QnACardBuilder.GetSuggestionsCard(suggestedQuestions, dialogOptions.ResponseOptions.ActiveLearningCardTitle, dialogOptions.ResponseOptions.CardNoMatchText);
496-
await stepContext.Context.SendActivityAsync(message).ConfigureAwait(false);
516+
await stepContext.Context.SendActivityAsync(message, cancellationToken).ConfigureAwait(false);
497517

498518
ObjectPath.SetPathValue(stepContext.ActiveDialog.State, Options, dialogOptions);
499519
stepContext.State.SetValue($"this.suggestedQuestions", suggestedQuestions);
@@ -508,6 +528,7 @@ private async Task<DialogTurnResult> CallGenerateAnswerAsync(WaterfallStepContex
508528
}
509529

510530
stepContext.Values[ValueProperty.QnAData] = result;
531+
511532
ObjectPath.SetPathValue(stepContext.ActiveDialog.State, Options, dialogOptions);
512533

513534
// If card is not shown, move to next step with top QnA response.
@@ -614,7 +635,6 @@ private async Task<DialogTurnResult> CheckForMultiTurnPromptAsync(WaterfallStepC
614635
if (answer.Context != null && answer.Context.Prompts.Count() > 0)
615636
{
616637
var previousContextData = ObjectPath.GetPathValue(stepContext.ActiveDialog.State, QnAContextData, new Dictionary<string, int>());
617-
var previousQnAId = ObjectPath.GetPathValue<int>(stepContext.ActiveDialog.State, PreviousQnAId, 0);
618638

619639
foreach (var prompt in answer.Context.Prompts)
620640
{
@@ -626,8 +646,8 @@ private async Task<DialogTurnResult> CheckForMultiTurnPromptAsync(WaterfallStepC
626646
ObjectPath.SetPathValue(stepContext.ActiveDialog.State, Options, dialogOptions);
627647

628648
// Get multi-turn prompts card activity.
629-
var message = QnACardBuilder.GetQnAPromptsCard(answer, dialogOptions.ResponseOptions.CardNoMatchText);
630-
await stepContext.Context.SendActivityAsync(message).ConfigureAwait(false);
649+
var message = QnACardBuilder.GetQnADefaultResponse(answer, dialogOptions.ResponseOptions.DisplayPreciseAnswerOnly);
650+
await stepContext.Context.SendActivityAsync(message, cancellationToken).ConfigureAwait(false);
631651

632652
return new DialogTurnResult(DialogTurnStatus.Waiting);
633653
}
@@ -667,7 +687,8 @@ private async Task<DialogTurnResult> DisplayQnAResultAsync(WaterfallStepContext
667687
// If response is present then show that response, else default answer.
668688
if (stepContext.Result is List<QueryResult> response && response.Count > 0)
669689
{
670-
await stepContext.Context.SendActivityAsync(response.First().Answer, cancellationToken: cancellationToken).ConfigureAwait(false);
690+
var message = QnACardBuilder.GetQnADefaultResponse(response.First(), dialogOptions.ResponseOptions.DisplayPreciseAnswerOnly);
691+
await stepContext.Context.SendActivityAsync(message, cancellationToken).ConfigureAwait(false);
671692
}
672693
else
673694
{
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Newtonsoft.Json;
5+
6+
namespace Microsoft.Bot.Builder.AI.QnA
7+
{
8+
/// <summary>
9+
/// This class helps in identifying the precise answer within complete answer text.
10+
/// </summary>
11+
public class AnswerSpanResponse
12+
{
13+
/// <summary>
14+
/// Gets or sets the Precise Answer text.
15+
/// </summary>
16+
/// <value>
17+
/// The precise answer text relevant to the user query.
18+
/// </value>
19+
[JsonProperty("text")]
20+
public string Text { get; set; }
21+
22+
/// <summary>
23+
/// Gets or sets the score of the Precise Answer.
24+
/// </summary>
25+
/// <value>
26+
/// The answer score pertaining to the quality of precise answer text.
27+
/// </value>
28+
[JsonProperty("score")]
29+
public float Score { get; set; }
30+
31+
/// <summary>
32+
/// Gets or sets the startIndex of the Precise Answer within the full answer text.
33+
/// </summary>
34+
/// <value>
35+
/// The starting index for the precise answer generated.
36+
/// </value>
37+
[JsonProperty("startIndex")]
38+
public int StartIndex { get; set; }
39+
40+
/// <summary>
41+
/// Gets or sets the endIndex of PreciseAnswer within the full answer text.
42+
/// </summary>
43+
/// <value>
44+
/// The end index for the precise answer generated.
45+
/// </value>
46+
[JsonProperty("endIndex")]
47+
public int EndIndex { get; set; }
48+
}
49+
}

libraries/Microsoft.Bot.Builder.AI.QnA/Models/QnAMakerTraceInfo.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,19 @@ public class QnAMakerTraceInfo
103103
/// </value>
104104
[JsonProperty("rankerType")]
105105
public string RankerType { get; set; }
106-
106+
107107
[Obsolete("This property is no longer used and will be ignored")]
108108
[JsonIgnore]
109109
public Metadata[] MetadataBoost { get; set; }
110+
111+
/// <summary>
112+
/// Gets or sets AnswerSpanRequest.
113+
/// Users can request PreciseAnswer from QnAMaker using this configuration.
114+
/// </summary>
115+
/// <value>
116+
/// AnswerSpanRequest for requesting PreciseAnswer.
117+
/// </value>
118+
[JsonProperty("answerSpanRequest")]
119+
public AnswerSpanRequest AnswerSpanRequest { get; set; }
110120
}
111121
}

libraries/Microsoft.Bot.Builder.AI.QnA/Models/QueryResult.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,14 @@ public class QueryResult
7676
/// </value>
7777
[JsonProperty(PropertyName = "context")]
7878
public QnAResponseContext Context { get; set; }
79+
80+
/// <summary>
81+
/// Gets or sets details of PreciseAnswer.
82+
/// </summary>
83+
/// <value>
84+
/// The PreciseAnswer related information in the Answer Text.
85+
/// </value>
86+
[JsonProperty("answerSpan")]
87+
public AnswerSpanResponse AnswerSpan { get; set; }
7988
}
8089
}

libraries/Microsoft.Bot.Builder.AI.QnA/QnADialogResponseOptions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,14 @@ public class QnADialogResponseOptions
4141
/// Get or set for Card no match response.
4242
/// </value>
4343
public Activity CardNoMatchResponse { get; set; }
44+
45+
/// <summary>
46+
/// Gets or sets a value indicating whether the Precise Answer is to be displayed or the source text also
47+
/// chosen to be displayed to the user.
48+
/// </summary>
49+
/// <value>
50+
/// Get or set whether to display Precise Answer Only or source text along with Precise Answer.
51+
/// </value>
52+
public bool DisplayPreciseAnswerOnly { get; set; }
4453
}
4554
}

libraries/Microsoft.Bot.Builder.AI.QnA/QnAMakerOptions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,5 +92,14 @@ public QnAMakerOptions()
9292
/// <seealso cref="RankerTypes"/>
9393
[JsonProperty("rankerType")]
9494
public string RankerType { get; set; }
95+
96+
/// <summary>
97+
/// Gets or sets a value indicating whether to enable PreciseAnswer generation.
98+
/// </summary>
99+
/// <value>
100+
/// Choice whether to generate precise answer or not.
101+
/// </value>
102+
[JsonProperty("enablePreciseAnswer")]
103+
public bool EnablePreciseAnswer { get; set; }
95104
}
96105
}

libraries/Microsoft.Bot.Builder.AI.QnA/QnAMakerRecognizer.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,15 @@ public QnAMakerRecognizer()
140140
[JsonProperty("logPersonalInformation")]
141141
public BoolExpression LogPersonalInformation { get; set; } = "=settings.telemetry.logPersonalInformation";
142142

143+
/// <summary>
144+
/// Gets or sets a value indicating whether to enable PreciseAnswer generation.
145+
/// </summary>
146+
/// <value>
147+
/// Choice whether to generate precise answer or not.
148+
/// </value>
149+
[JsonProperty("enablePreciseAnswer")]
150+
public BoolExpression EnablePreciseAnswer { get; set; } = false;
151+
143152
public override async Task<RecognizerResult> RecognizeAsync(DialogContext dialogContext, Activity activity, CancellationToken cancellationToken, Dictionary<string, string> telemetryProperties = null, Dictionary<string, double> telemetryMetrics = null)
144153
{
145154
// Identify matched intents
@@ -180,7 +189,8 @@ public override async Task<RecognizerResult> RecognizeAsync(DialogContext dialog
180189
Top = this.Top.GetValue(dialogContext.State),
181190
QnAId = this.QnAId.GetValue(dialogContext.State),
182191
RankerType = this.RankerType.GetValue(dialogContext.State),
183-
IsTest = this.IsTest
192+
IsTest = this.IsTest,
193+
EnablePreciseAnswer = this.EnablePreciseAnswer.GetValue(dialogContext.State)
184194
},
185195
null).ConfigureAwait(false);
186196

@@ -208,6 +218,13 @@ public override async Task<RecognizerResult> RecognizeAsync(DialogContext dialog
208218
answerArray.Add(topAnswer.Answer);
209219
ObjectPath.SetPathValue(recognizerResult, "entities.answer", answerArray);
210220

221+
if (!string.IsNullOrEmpty(topAnswer.AnswerSpan?.Text))
222+
{
223+
var answerSpanArray = new JArray();
224+
answerSpanArray.Add(topAnswer.AnswerSpan.Text);
225+
ObjectPath.SetPathValue(recognizerResult, "entities.answerspan", answerSpanArray);
226+
}
227+
211228
var instance = new JArray();
212229
instance.Add(JObject.FromObject(topAnswer));
213230
ObjectPath.SetPathValue(recognizerResult, "entities.$instance.answer", instance);

libraries/Microsoft.Bot.Builder.AI.QnA/Schemas/Microsoft.QnAMakerDialog.schema

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,16 @@
9292
"title": "IsTest",
9393
"description": "True, if pointing to Test environment, else false.",
9494
"default": false
95+
},
96+
"enablePreciseAnswer": {
97+
"$ref": "schema:#/definitions/booleanExpression",
98+
"title": "EnablePreciseAnswer",
99+
"description": "True, if PreciseAnswer is opted, else false."
100+
},
101+
"displayPreciseAnswerOnly": {
102+
"$ref": "schema:#/definitions/booleanExpression",
103+
"title": "DisplayPreciseAnswerOnly",
104+
"description": "True, if PreciseAnswer text only is opted, else false."
95105
},
96106
"rankerType": {
97107
"$ref": "schema:#/definitions/stringExpression",

libraries/Microsoft.Bot.Builder.AI.QnA/Schemas/Microsoft.QnAMakerRecognizer.schema

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@
137137
"$ref": "schema:#/definitions/integerExpression",
138138
"title": "QnAId",
139139
"description": "A number or expression which is the QnAId to paass to QnAMaker API."
140+
},
141+
"enablePreciseAnswer": {
142+
"$ref": "schema:#/definitions/booleanExpression",
143+
"title": "EnablePreciseAnswer",
144+
"description": "True, if PreciseAnswer is opted, else false."
140145
}
141146
},
142147
"required": [

libraries/Microsoft.Bot.Builder.AI.QnA/Utils/GenerateAnswerUtils.cs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -153,28 +153,29 @@ private static void ValidateOptions(QnAMakerOptions options)
153153
private QnAMakerOptions HydrateOptions(QnAMakerOptions queryOptions)
154154
{
155155
var hydratedOptions = JsonConvert.DeserializeObject<QnAMakerOptions>(JsonConvert.SerializeObject(this.Options));
156-
156+
157157
if (queryOptions != null)
158158
{
159-
if (queryOptions.ScoreThreshold != hydratedOptions.ScoreThreshold && queryOptions.ScoreThreshold != 0)
159+
if (queryOptions.ScoreThreshold != 0)
160160
{
161161
hydratedOptions.ScoreThreshold = queryOptions.ScoreThreshold;
162162
}
163163

164-
if (queryOptions.Top != hydratedOptions.Top && queryOptions.Top != 0)
164+
if (queryOptions.Top != 0)
165165
{
166166
hydratedOptions.Top = queryOptions.Top;
167167
}
168168

169169
if (queryOptions.StrictFilters?.Length > 0)
170170
{
171171
hydratedOptions.StrictFilters = queryOptions.StrictFilters;
172-
}
172+
}
173173

174174
hydratedOptions.Context = queryOptions.Context;
175175
hydratedOptions.QnAId = queryOptions.QnAId;
176176
hydratedOptions.IsTest = queryOptions.IsTest;
177177
hydratedOptions.RankerType = queryOptions.RankerType != null ? queryOptions.RankerType : RankerTypes.DefaultRankerType;
178+
hydratedOptions.EnablePreciseAnswer = queryOptions.EnablePreciseAnswer;
178179
}
179180

180181
return hydratedOptions;
@@ -183,17 +184,24 @@ private QnAMakerOptions HydrateOptions(QnAMakerOptions queryOptions)
183184
private async Task<QueryResults> QueryQnaServiceAsync(Activity messageActivity, QnAMakerOptions options)
184185
{
185186
var requestUrl = $"{_endpoint.Host}/knowledgebases/{_endpoint.KnowledgeBaseId}/generateanswer";
187+
var answerSpanRequest = new AnswerSpanRequest();
188+
if (options.EnablePreciseAnswer)
189+
{
190+
answerSpanRequest.Enable = options.EnablePreciseAnswer;
191+
}
192+
186193
var jsonRequest = JsonConvert.SerializeObject(
187194
new
188195
{
189196
question = messageActivity.Text,
190197
top = options.Top,
191-
strictFilters = options.StrictFilters,
198+
strictFilters = options.StrictFilters,
192199
scoreThreshold = options.ScoreThreshold,
193200
context = options.Context,
194201
qnaId = options.QnAId,
195202
isTest = options.IsTest,
196-
rankerType = options.RankerType
203+
rankerType = options.RankerType,
204+
answerSpanRequest = answerSpanRequest
197205
}, Formatting.None);
198206

199207
var httpRequestHelper = new HttpRequestUtils(httpClient);
@@ -206,18 +214,25 @@ private async Task<QueryResults> QueryQnaServiceAsync(Activity messageActivity,
206214

207215
private async Task EmitTraceInfoAsync(ITurnContext turnContext, Activity messageActivity, QueryResult[] result, QnAMakerOptions options)
208216
{
217+
var answerSpanRequest = new AnswerSpanRequest();
218+
if (options.EnablePreciseAnswer)
219+
{
220+
answerSpanRequest.Enable = options.EnablePreciseAnswer;
221+
}
222+
209223
var traceInfo = new QnAMakerTraceInfo
210224
{
211225
Message = (Activity)messageActivity,
212226
QueryResults = result,
213227
KnowledgeBaseId = _endpoint.KnowledgeBaseId,
214228
ScoreThreshold = options.ScoreThreshold,
215229
Top = options.Top,
216-
StrictFilters = options.StrictFilters,
230+
StrictFilters = options.StrictFilters,
217231
Context = options.Context,
218232
QnAId = options.QnAId,
219233
IsTest = options.IsTest,
220-
RankerType = options.RankerType
234+
RankerType = options.RankerType,
235+
AnswerSpanRequest = answerSpanRequest
221236
};
222237
var traceActivity = Activity.CreateTraceActivity(QnAMaker.QnAMakerName, QnAMaker.QnAMakerTraceType, traceInfo, QnAMaker.QnAMakerTraceLabel);
223238
await turnContext.SendActivityAsync(traceActivity).ConfigureAwait(false);

0 commit comments

Comments
 (0)