diff --git a/etc/bot-checkstyle.xml b/etc/bot-checkstyle.xml
index 337417b9e..da9b3f275 100644
--- a/etc/bot-checkstyle.xml
+++ b/etc/bot-checkstyle.xml
@@ -184,6 +184,8 @@
+
+
diff --git a/libraries/bot-ai-qna/README.md b/libraries/bot-ai-qna/README.md
new file mode 100644
index 000000000..72f1506a9
--- /dev/null
+++ b/libraries/bot-ai-qna/README.md
@@ -0,0 +1,14 @@
+
+# Contributing
+
+This project welcomes contributions and suggestions. Most contributions require you to agree to a
+Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
+the rights to use your contribution. For details, visit https://cla.microsoft.com.
+
+When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
+a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
+provided by the bot. You will only need to do this once across all repos using our CLA.
+
+This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
+For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
+contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
diff --git a/libraries/bot-ai-qna/pom.xml b/libraries/bot-ai-qna/pom.xml
index 133a60559..4de263b57 100644
--- a/libraries/bot-ai-qna/pom.xml
+++ b/libraries/bot-ai-qna/pom.xml
@@ -49,15 +49,46 @@
junit
junit
-
org.slf4j
slf4j-api
-
com.microsoft.bot
bot-builder
+ ${project.version}
+
+
+ com.microsoft.bot
+ bot-dialogs
+
+
+ com.microsoft.bot
+ bot-builder
+ ${project.version}
+ test-jar
+ test
+
+
+ com.microsoft.bot
+ bot-integration-spring
+ 4.6.0-preview8
+ compile
+
+
+ com.squareup.okhttp3
+ okhttp
+ 4.9.0
+
+
+ com.squareup.okhttp3
+ mockwebserver
+ 4.9.0
+ test
+
+
+ org.mockito
+ mockito-core
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/JoinOperator.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/JoinOperator.java
new file mode 100644
index 000000000..cbba1cc6c
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/JoinOperator.java
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna;
+
+/**
+ * Join Operator for Strict Filters.
+ */
+public enum JoinOperator {
+ /**
+ * Default Join Operator, AND.
+ */
+ AND,
+
+ /**
+ * Join Operator, OR.
+ */
+ OR
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnADialogResponseOptions.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnADialogResponseOptions.java
new file mode 100644
index 000000000..be9bcf3ff
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnADialogResponseOptions.java
@@ -0,0 +1,88 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna;
+
+import com.microsoft.bot.schema.Activity;
+
+/**
+ * QnA dialog response options class.
+ */
+public class QnADialogResponseOptions {
+ private String activeLearningCardTitle;
+ private String cardNoMatchText;
+ private Activity noAnswer;
+ private Activity cardNoMatchResponse;
+
+ /**
+ * Gets the active learning card title.
+ *
+ * @return The active learning card title
+ */
+ public String getActiveLearningCardTitle() {
+ return activeLearningCardTitle;
+ }
+
+ /**
+ * Sets the active learning card title.
+ *
+ * @param withActiveLearningCardTitle The active learning card title.
+ */
+ public void setActiveLearningCardTitle(String withActiveLearningCardTitle) {
+ this.activeLearningCardTitle = withActiveLearningCardTitle;
+ }
+
+ /**
+ * Gets the card no match text.
+ *
+ * @return The card no match text.
+ */
+ public String getCardNoMatchText() {
+ return cardNoMatchText;
+ }
+
+ /**
+ * Sets the card no match text.
+ *
+ * @param withCardNoMatchText The card no match text.
+ */
+ public void setCardNoMatchText(String withCardNoMatchText) {
+ this.cardNoMatchText = withCardNoMatchText;
+ }
+
+ /**
+ * Gets the no answer activity.
+ *
+ * @return The no answer activity.
+ */
+ public Activity getNoAnswer() {
+ return noAnswer;
+ }
+
+ /**
+ * Sets the no answer activity.
+ *
+ * @param withNoAnswer The no answer activity.
+ */
+ public void setNoAnswer(Activity withNoAnswer) {
+ this.noAnswer = withNoAnswer;
+ }
+
+ /**
+ * Gets the card no match response.
+ *
+ * @return The card no match response.
+ */
+ public Activity getCardNoMatchResponse() {
+ return cardNoMatchResponse;
+ }
+
+ /**
+ * Sets the card no match response.
+ *
+ * @param withCardNoMatchResponse The card no match response.
+ */
+ public void setCardNoMatchResponse(Activity withCardNoMatchResponse) {
+ this.cardNoMatchResponse = withCardNoMatchResponse;
+ }
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMaker.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMaker.java
new file mode 100644
index 000000000..b6b5d3d1d
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMaker.java
@@ -0,0 +1,355 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna;
+
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.Multimap;
+import com.microsoft.bot.ai.qna.models.FeedbackRecords;
+import com.microsoft.bot.ai.qna.models.QueryResult;
+import com.microsoft.bot.ai.qna.models.QueryResults;
+import com.microsoft.bot.ai.qna.utils.ActiveLearningUtils;
+import com.microsoft.bot.ai.qna.utils.GenerateAnswerUtils;
+import com.microsoft.bot.ai.qna.utils.QnATelemetryConstants;
+import com.microsoft.bot.ai.qna.utils.TrainUtils;
+
+import com.microsoft.bot.builder.BotTelemetryClient;
+import com.microsoft.bot.builder.NullBotTelemetryClient;
+import com.microsoft.bot.builder.TurnContext;
+import com.microsoft.bot.restclient.serializer.JacksonAdapter;
+import com.microsoft.bot.schema.Activity;
+import com.microsoft.bot.schema.ActivityTypes;
+import com.microsoft.bot.schema.Pair;
+
+import javax.annotation.Nullable;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.CompletableFuture;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.google.common.base.Strings;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Provides access to a QnA Maker knowledge base.
+ */
+public class QnAMaker implements QnAMakerClient, TelemetryQnAMaker {
+
+ private QnAMakerEndpoint endpoint;
+
+ private GenerateAnswerUtils generateAnswerHelper;
+ private TrainUtils activeLearningTrainHelper;
+ private Boolean logPersonalInformation;
+ @JsonIgnore
+ private BotTelemetryClient telemetryClient;
+
+ /**
+ * The name of the QnAMaker class.
+ */
+ public static final String QNA_MAKER_NAME = "QnAMaker";
+ /**
+ * The type used when logging QnA Maker trace.
+ */
+ public static final String QNA_MAKER_TRACE_TYPE = "https://www.qnamaker.ai/schemas/trace";
+ /**
+ * The label used when logging QnA Maker trace.
+ */
+ public static final String QNA_MAKER_TRACE_LABEL = "QnAMaker Trace";
+
+ /**
+ * Initializes a new instance of the QnAMaker class.
+ *
+ * @param withEndpoint The endpoint of the knowledge base to
+ * query.
+ * @param options The options for the QnA Maker knowledge
+ * base.
+ * @param withTelemetryClient The IBotTelemetryClient used for logging
+ * telemetry events.
+ * @param withLogPersonalInformation Set to true to include personally
+ * identifiable information in telemetry
+ * events.
+ */
+ public QnAMaker(QnAMakerEndpoint withEndpoint, QnAMakerOptions options,
+ BotTelemetryClient withTelemetryClient, Boolean withLogPersonalInformation) {
+ if (withLogPersonalInformation == null) {
+ withLogPersonalInformation = false;
+ }
+
+ if (withEndpoint == null) {
+ throw new IllegalArgumentException("endpoint");
+ }
+ this.endpoint = withEndpoint;
+
+ if (Strings.isNullOrEmpty(this.endpoint.getKnowledgeBaseId())) {
+ throw new IllegalArgumentException("knowledgeBaseId");
+ }
+
+ if (Strings.isNullOrEmpty(this.endpoint.getHost())) {
+ throw new IllegalArgumentException("host");
+ }
+
+ if (Strings.isNullOrEmpty(this.endpoint.getEndpointKey())) {
+ throw new IllegalArgumentException("endpointKey");
+ }
+
+ if (this.endpoint.getHost().endsWith("v2.0") || this.endpoint.getHost().endsWith("v3.0")) {
+ throw new UnsupportedOperationException(
+ "v2.0 and v3.0 of QnA Maker service" + " is no longer supported in the QnA Maker.");
+ }
+
+ this.telemetryClient = withTelemetryClient != null ? withTelemetryClient : new NullBotTelemetryClient();
+ this.logPersonalInformation = withLogPersonalInformation;
+
+ this.generateAnswerHelper = new GenerateAnswerUtils(this.endpoint, options);
+ this.activeLearningTrainHelper = new TrainUtils(this.endpoint);
+ }
+
+ /**
+ * Initializes a new instance of the {@link QnAMaker} class.
+ *
+ * @param withEndpoint The endpoint of the knowledge base to query.
+ * @param options The options for the QnA Maker knowledge base.
+ */
+ public QnAMaker(QnAMakerEndpoint withEndpoint, @Nullable QnAMakerOptions options) {
+ this(withEndpoint, options, null, null);
+ }
+
+ /**
+ * Gets a value indicating whether determines whether to log personal
+ * information that came from the user.
+ *
+ * @return If true, will log personal information into the
+ * IBotTelemetryClient.TrackEvent method; otherwise the properties will
+ * be filtered.
+ */
+ public Boolean getLogPersonalInformation() {
+ return this.logPersonalInformation;
+ }
+
+ /**
+ * Gets the currently configured {@link BotTelemetryClient}.
+ *
+ * @return {@link BotTelemetryClient} being used to log events.
+ */
+ public BotTelemetryClient getTelemetryClient() {
+ return this.telemetryClient;
+ }
+
+ /**
+ * Generates an answer from the knowledge base.
+ *
+ * @param turnContext The Turn Context that contains the user question to be
+ * queried against your knowledge base.
+ * @param options The options for the QnA Maker knowledge base. If null,
+ * constructor option is used for this instance.
+ * @return A list of answers for the user query, sorted in decreasing order of
+ * ranking score.
+ */
+ public CompletableFuture getAnswers(TurnContext turnContext, @Nullable QnAMakerOptions options) {
+ return this.getAnswers(turnContext, options, null, null);
+ }
+
+ /**
+ * Generates an answer from the knowledge base.
+ *
+ * @param turnContext The Turn Context that contains the user question
+ * to be queried against your knowledge base.
+ * @param options The options for the QnA Maker knowledge base. If
+ * null, constructor option is used for this
+ * instance.
+ * @param telemetryProperties Additional properties to be logged to telemetry
+ * with the QnaMessage event.
+ * @param telemetryMetrics Additional metrics to be logged to telemetry with
+ * the QnaMessage event.
+ * @return A list of answers for the user query, sorted in decreasing order of
+ * ranking score.
+ */
+ public CompletableFuture getAnswers(TurnContext turnContext, QnAMakerOptions options,
+ Map telemetryProperties, @Nullable Map telemetryMetrics) {
+ return this.getAnswersRaw(turnContext, options, telemetryProperties, telemetryMetrics)
+ .thenApply(result -> result.getAnswers());
+ }
+
+ /**
+ * Generates an answer from the knowledge base.
+ *
+ * @param turnContext The Turn Context that contains the user question
+ * to be queried against your knowledge base.
+ * @param options The options for the QnA Maker knowledge base. If
+ * null, constructor option is used for this
+ * instance.
+ * @param telemetryProperties Additional properties to be logged to telemetry
+ * with the QnaMessage event.
+ * @param telemetryMetrics Additional metrics to be logged to telemetry with
+ * the QnaMessage event.
+ * @return A list of answers for the user query, sorted in decreasing order of
+ * ranking score.
+ */
+ public CompletableFuture getAnswersRaw(TurnContext turnContext, QnAMakerOptions options,
+ @Nullable Map telemetryProperties, @Nullable Map telemetryMetrics) {
+ if (turnContext == null) {
+ throw new IllegalArgumentException("turnContext");
+ }
+ if (turnContext.getActivity() == null) {
+ throw new IllegalArgumentException(
+ String.format("The %1$s property for %2$s can't be null.", "Activity", "turnContext"));
+ }
+ Activity messageActivity = turnContext.getActivity();
+ if (messageActivity == null || !messageActivity.isType(ActivityTypes.MESSAGE)) {
+ throw new IllegalArgumentException("Activity type is not a message");
+ }
+
+ if (Strings.isNullOrEmpty(turnContext.getActivity().getText())) {
+ throw new IllegalArgumentException("Null or empty text");
+ }
+
+ return this.generateAnswerHelper.getAnswersRaw(turnContext, messageActivity, options).thenCompose(result -> {
+ try {
+ this.onQnaResults(result.getAnswers(), turnContext, telemetryProperties, telemetryMetrics);
+ } catch (IOException e) {
+ LoggerFactory.getLogger(QnAMaker.class).error("getAnswersRaw");
+ }
+ return CompletableFuture.completedFuture(result);
+ });
+ }
+
+ /**
+ * Filters the ambiguous question for active learning.
+ *
+ * @param queryResult User query output.
+ * @return Filtered array of ambiguous question.
+ */
+ public QueryResult[] getLowScoreVariation(QueryResult[] queryResult) {
+ List queryResults = ActiveLearningUtils.getLowScoreVariation(Arrays.asList(queryResult));
+ return queryResults.toArray(new QueryResult[queryResults.size()]);
+ }
+
+ /**
+ * Send feedback to the knowledge base.
+ *
+ * @param feedbackRecords Feedback records.
+ * @return Representing the asynchronous operation.
+ * @throws IOException Throws an IOException if there is any.
+ */
+ public CompletableFuture callTrain(FeedbackRecords feedbackRecords) throws IOException {
+ return this.activeLearningTrainHelper.callTrain(feedbackRecords);
+ }
+
+ /**
+ * Executed when a result is returned from QnA Maker.
+ *
+ * @param queryResults An array of {@link QueryResult}
+ * @param turnContext The {@link TurnContext}
+ * @param telemetryProperties Additional properties to be logged to telemetry
+ * with the LuisResult event.
+ * @param telemetryMetrics Additional metrics to be logged to telemetry with
+ * the LuisResult event.
+ * @return A Task representing the work to be executed.
+ * @throws IOException Throws an IOException if there is any.
+ */
+ protected CompletableFuture onQnaResults(QueryResult[] queryResults, TurnContext turnContext,
+ @Nullable Map telemetryProperties, @Nullable Map telemetryMetrics)
+ throws IOException {
+ return fillQnAEvent(queryResults, turnContext, telemetryProperties, telemetryMetrics).thenAccept(eventData -> {
+ // Track the event
+ this.telemetryClient.trackEvent(QnATelemetryConstants.QNA_MSG_EVENT, eventData.getLeft(),
+ eventData.getRight());
+ });
+ }
+
+ /**
+ * Fills the event properties and metrics for the QnaMessage event for
+ * telemetry. These properties are logged when the QnA GetAnswers method is
+ * called.
+ *
+ * @param queryResults QnA service results.
+ * @param turnContext Context object containing information for a single
+ * turn of conversation with a user.
+ * @param telemetryProperties Properties to add/override for the event.
+ * @param telemetryMetrics Metrics to add/override for the event.
+ * @return A tuple of Properties and Metrics that will be sent to the
+ * IBotTelemetryClient. TrackEvent method for the QnAMessage event. The
+ * properties and metrics returned the standard properties logged with
+ * any properties passed from the GetAnswersAsync method.
+ * @throws IOException Throws an IOException if there is any.
+ */
+ protected CompletableFuture, Map>> fillQnAEvent(QueryResult[] queryResults,
+ TurnContext turnContext, @Nullable Map telemetryProperties,
+ @Nullable Map telemetryMetrics) throws IOException {
+ Map properties = new HashMap();
+ Map metrics = new HashMap();
+
+ properties.put(QnATelemetryConstants.KNOWLEDGE_BASE_ID_PROPERTY, this.endpoint.getKnowledgeBaseId());
+
+ String text = turnContext.getActivity().getText();
+ String userName = turnContext.getActivity().getFrom() != null
+ ? turnContext.getActivity().getFrom().getName() : null;
+
+ // Use the LogPersonalInformation flag to toggle logging PII data, text and user
+ // name are common examples
+ if (this.logPersonalInformation) {
+ if (!StringUtils.isBlank(text)) {
+ properties.put(QnATelemetryConstants.QUESTION_PROPERTY, text);
+ }
+
+ if (!StringUtils.isBlank(userName)) {
+ properties.put(QnATelemetryConstants.USERNAME_PROPERTY, userName);
+ }
+ }
+
+ // Fill in QnA Results (found or not)
+ if (queryResults.length > 0) {
+ JacksonAdapter jacksonAdapter = new JacksonAdapter();
+ QueryResult queryResult = queryResults[0];
+ properties.put(QnATelemetryConstants.MATCHED_QUESTION_PROPERTY,
+ jacksonAdapter.serialize(queryResult.getQuestions()));
+ properties.put(QnATelemetryConstants.QUESTION_ID_PROPERTY, queryResult.getId().toString());
+ properties.put(QnATelemetryConstants.ANSWER_PROPERTY, queryResult.getAnswer());
+ metrics.put(QnATelemetryConstants.SCORE_PROPERTY, queryResult.getScore().doubleValue());
+ properties.put(QnATelemetryConstants.ARTICLE_FOUND_PROPERTY, "true");
+ } else {
+ properties.put(QnATelemetryConstants.MATCHED_QUESTION_PROPERTY, "No Qna Question matched");
+ properties.put(QnATelemetryConstants.QUESTION_ID_PROPERTY, "No QnA Question Id matched");
+ properties.put(QnATelemetryConstants.ANSWER_PROPERTY, "No Qna Answer matched");
+ properties.put(QnATelemetryConstants.ARTICLE_FOUND_PROPERTY, "false");
+ }
+
+ // Additional Properties can override "stock" properties.
+ if (telemetryProperties != null) {
+ Multimap multiMapTelemetryProperties = LinkedListMultimap.create();
+ for (Entry entry: telemetryProperties.entrySet()) {
+ multiMapTelemetryProperties.put(entry.getKey(), entry.getValue());
+ }
+ for (Entry entry: properties.entrySet()) {
+ multiMapTelemetryProperties.put(entry.getKey(), entry.getValue());
+ }
+ for (Entry> entry: multiMapTelemetryProperties.asMap().entrySet()) {
+ telemetryProperties.put(entry.getKey(), entry.getValue().iterator().next());
+ }
+ }
+
+ // Additional Metrics can override "stock" metrics.
+ if (telemetryMetrics != null) {
+ Multimap multiMapTelemetryMetrics = LinkedListMultimap.create();
+ for (Entry entry: telemetryMetrics.entrySet()) {
+ multiMapTelemetryMetrics.put(entry.getKey(), entry.getValue());
+ }
+ for (Entry entry: metrics.entrySet()) {
+ multiMapTelemetryMetrics.put(entry.getKey(), entry.getValue());
+ }
+ for (Entry> entry: multiMapTelemetryMetrics.asMap().entrySet()) {
+ telemetryMetrics.put(entry.getKey(), entry.getValue().iterator().next());
+ }
+ }
+
+ Map telemetryPropertiesResult = telemetryProperties != null ? telemetryProperties : properties;
+ Map telemetryMetricsResult = telemetryMetrics != null ? telemetryMetrics : metrics;
+ return CompletableFuture.completedFuture(new Pair<>(telemetryPropertiesResult, telemetryMetricsResult));
+ }
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMakerClient.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMakerClient.java
new file mode 100644
index 000000000..55da459d9
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMakerClient.java
@@ -0,0 +1,73 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+import javax.annotation.Nullable;
+
+import com.microsoft.bot.ai.qna.models.FeedbackRecords;
+import com.microsoft.bot.ai.qna.models.QueryResult;
+import com.microsoft.bot.ai.qna.models.QueryResults;
+import com.microsoft.bot.builder.TurnContext;
+
+/**
+ * Client to access a QnA Maker knowledge base.
+ */
+public interface QnAMakerClient {
+ /**
+ * Generates an answer from the knowledge base.
+ *
+ * @param turnContext The Turn Context that contains the user question
+ * to be queried against your knowledge base.
+ * @param options The options for the QnA Maker knowledge base. If
+ * null, constructor option is used for this
+ * instance.
+ * @param telemetryProperties Additional properties to be logged to telemetry
+ * with the QnaMessage event.
+ * @param telemetryMetrics Additional metrics to be logged to telemetry with
+ * the QnaMessage event.
+ * @return A list of answers for the user query, sorted in decreasing order of
+ * ranking score.
+ */
+ CompletableFuture getAnswers(TurnContext turnContext, QnAMakerOptions options,
+ Map telemetryProperties, @Nullable Map telemetryMetrics);
+
+ /**
+ * Generates an answer from the knowledge base.
+ *
+ * @param turnContext The Turn Context that contains the user question
+ * to be queried against your knowledge base.
+ * @param options The options for the QnA Maker knowledge base. If
+ * null, constructor option is used for this
+ * instance.
+ * @param telemetryProperties Additional properties to be logged to telemetry
+ * with the QnaMessage event.
+ * @param telemetryMetrics Additional metrics to be logged to telemetry with
+ * the QnaMessage event.
+ * @return A list of answers for the user query, sorted in decreasing order of
+ * ranking score.
+ */
+ CompletableFuture getAnswersRaw(TurnContext turnContext, QnAMakerOptions options,
+ @Nullable Map telemetryProperties, @Nullable Map telemetryMetrics);
+
+ /**
+ * Filters the ambiguous question for active learning.
+ *
+ * @param queryResults User query output.
+ * @return Filtered array of ambiguous question.
+ */
+ QueryResult[] getLowScoreVariation(QueryResult[] queryResults);
+
+ /**
+ * Send feedback to the knowledge base.
+ *
+ * @param feedbackRecords Feedback records.
+ * @return A Task representing the asynchronous operation.
+ * @throws IOException Throws an IOException if there is any.
+ */
+ CompletableFuture callTrain(FeedbackRecords feedbackRecords) throws IOException;
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMakerEndpoint.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMakerEndpoint.java
new file mode 100644
index 000000000..c185124b7
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMakerEndpoint.java
@@ -0,0 +1,83 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Defines an endpoint used to connect to a QnA Maker Knowledge base.
+ */
+public class QnAMakerEndpoint {
+ @JsonProperty("knowledgeBaseId")
+ private String knowledgeBaseId;
+
+ @JsonProperty("endpointKey")
+ private String endpointKey;
+
+ @JsonProperty("host")
+ private String host;
+
+ /**
+ * Gets the knowledge base ID.
+ *
+ * @return The knowledge base ID.
+ */
+ public String getKnowledgeBaseId() {
+ return knowledgeBaseId;
+ }
+
+ /**
+ * Sets the knowledge base ID.
+ *
+ * @param withKnowledgeBaseId The knowledge base ID.
+ */
+ public void setKnowledgeBaseId(String withKnowledgeBaseId) {
+ this.knowledgeBaseId = withKnowledgeBaseId;
+ }
+
+ /**
+ * Gets the endpoint key for the knowledge base.
+ *
+ * @return The endpoint key for the knowledge base.
+ */
+ public String getEndpointKey() {
+ return endpointKey;
+ }
+
+ /**
+ * Sets the endpoint key for the knowledge base.
+ *
+ * @param withEndpointKey The endpoint key for the knowledge base.
+ */
+ public void setEndpointKey(String withEndpointKey) {
+ this.endpointKey = withEndpointKey;
+ }
+
+ /**
+ * Gets the host path. For example
+ * "https://westus.api.cognitive.microsoft.com/qnamaker/v2.0".
+ *
+ * @return The host path.
+ */
+ public String getHost() {
+ return host;
+ }
+
+ /**
+ * Sets the host path. For example
+ * "https://westus.api.cognitive.microsoft.com/qnamaker/v2.0".
+ *
+ * @param withHost The host path.
+ */
+ public void setHost(String withHost) {
+ this.host = withHost;
+ }
+
+ /**
+ * Initializes a new instance of the {@link QnAMakerEndpoint} class.
+ */
+ public QnAMakerEndpoint() {
+
+ }
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMakerOptions.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMakerOptions.java
new file mode 100644
index 000000000..d1d91e3d7
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMakerOptions.java
@@ -0,0 +1,235 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.microsoft.bot.ai.qna.models.Metadata;
+import com.microsoft.bot.ai.qna.models.QnARequestContext;
+
+/**
+ * Defines options for the QnA Maker knowledge base.
+ */
+public class QnAMakerOptions {
+ @JsonProperty("scoreThreshold")
+ private Float scoreThreshold;
+
+ @JsonProperty("timeout")
+ private Double timeout = 0d;
+
+ @JsonProperty("top")
+ private Integer top = 0;
+
+ @JsonProperty("context")
+ private QnARequestContext context;
+
+ @JsonProperty("qnAId")
+ private Integer qnAId;
+
+ @JsonProperty("strictFilters")
+ private Metadata[] strictFilters;
+
+ @Deprecated
+ @JsonIgnore
+ private Metadata[] metadataBoost;
+
+ @JsonProperty("isTest")
+ private Boolean isTest;
+
+ @JsonProperty("rankerType")
+ private String rankerType;
+
+ @JsonProperty("strictFiltersJoinOperator")
+ private JoinOperator strictFiltersJoinOperator;
+
+ private static final Float SCORE_THRESHOLD = 0.3f;
+
+ /**
+ * Gets the minimum score threshold, used to filter returned results. Scores are
+ * normalized to the range of 0.0 to 1.0 before filtering.
+ *
+ * @return The minimum score threshold, used to filter returned results.
+ */
+ public Float getScoreThreshold() {
+ return scoreThreshold;
+ }
+
+ /**
+ * Sets the minimum score threshold, used to filter returned results. Scores are
+ * normalized to the range of 0.0 to 1.0 before filtering.
+ *
+ * @param withScoreThreshold The minimum score threshold, used to filter
+ * returned results.
+ */
+ public void setScoreThreshold(Float withScoreThreshold) {
+ this.scoreThreshold = withScoreThreshold;
+ }
+
+ /**
+ * Gets the time in milliseconds to wait before the request times out.
+ *
+ * @return The time in milliseconds to wait before the request times out.
+ * Default is 100000 milliseconds. This property allows users to set
+ * Timeout without having to pass in a custom HttpClient to QnAMaker
+ * class constructor. If using custom HttpClient, then set Timeout value
+ * in HttpClient instead of QnAMakerOptions.Timeout.
+ */
+ public Double getTimeout() {
+ return timeout;
+ }
+
+ /**
+ * Sets the time in milliseconds to wait before the request times out.
+ *
+ * @param withTimeout The time in milliseconds to wait before the request times
+ * out. Default is 100000 milliseconds. This property allows
+ * users to set Timeout without having to pass in a custom
+ * HttpClient to QnAMaker class constructor. If using custom
+ * HttpClient, then set Timeout value in HttpClient instead
+ * of QnAMakerOptions.Timeout.
+ */
+ public void setTimeout(Double withTimeout) {
+ this.timeout = withTimeout;
+ }
+
+ /**
+ * Gets the number of ranked results you want in the output.
+ *
+ * @return The number of ranked results you want in the output.
+ */
+ public Integer getTop() {
+ return top;
+ }
+
+ /**
+ * Sets the number of ranked results you want in the output.
+ *
+ * @param withTop The number of ranked results you want in the output.
+ */
+ public void setTop(Integer withTop) {
+ this.top = withTop;
+ }
+
+ /**
+ * Gets context of the previous turn.
+ *
+ * @return The context of previous turn.
+ */
+ public QnARequestContext getContext() {
+ return context;
+ }
+
+ /**
+ * Sets context of the previous turn.
+ *
+ * @param withContext The context of previous turn.
+ */
+ public void setContext(QnARequestContext withContext) {
+ this.context = withContext;
+ }
+
+ /**
+ * Gets QnA Id of the current question asked (if availble).
+ *
+ * @return Id of the current question asked.
+ */
+ public Integer getQnAId() {
+ return qnAId;
+ }
+
+ /**
+ * Sets QnA Id of the current question asked (if availble).
+ *
+ * @param withQnAId Id of the current question asked.
+ */
+ public void setQnAId(Integer withQnAId) {
+ this.qnAId = withQnAId;
+ }
+
+ /**
+ * Gets the {@link Metadata} collection to be sent when calling QnA Maker to
+ * filter results.
+ *
+ * @return An array of {@link Metadata}
+ */
+ public Metadata[] getStrictFilters() {
+ return strictFilters;
+ }
+
+ /**
+ * Sets the {@link Metadata} collection to be sent when calling QnA Maker to
+ * filter results.
+ *
+ * @param withStrictFilters An array of {@link Metadata}
+ */
+ public void setStrictFilters(Metadata[] withStrictFilters) {
+ this.strictFilters = withStrictFilters;
+ }
+
+ /**
+ * Gets a value indicating whether to call test or prod environment of knowledge
+ * base to be called.
+ *
+ * @return A value indicating whether to call test or prod environment of
+ * knowledge base.
+ */
+ public Boolean getIsTest() {
+ return isTest;
+ }
+
+ /**
+ * Sets a value indicating whether to call test or prod environment of knowledge
+ * base to be called.
+ *
+ * @param withIsTest A value indicating whether to call test or prod environment
+ * of knowledge base.
+ */
+ public void setIsTest(Boolean withIsTest) {
+ isTest = withIsTest;
+ }
+
+ /**
+ * Gets the QnA Maker ranker type to use.
+ *
+ * @return The QnA Maker ranker type to use.
+ */
+ public String getRankerType() {
+ return rankerType;
+ }
+
+ /**
+ * Sets the QnA Maker ranker type to use.
+ *
+ * @param withRankerType The QnA Maker ranker type to use.
+ */
+ public void setRankerType(String withRankerType) {
+ this.rankerType = withRankerType;
+ }
+
+ /**
+ * Gets Strict Filters join operator.
+ *
+ * @return A value indicating choice for Strict Filters Join Operation.
+ */
+ public JoinOperator getStrictFiltersJoinOperator() {
+ return strictFiltersJoinOperator;
+ }
+
+ /**
+ * Sets Strict Filters join operator.
+ *
+ * @param withStrictFiltersJoinOperator A value indicating choice for Strict
+ * Filters Join Operation.
+ */
+ public void setStrictFiltersJoinOperator(JoinOperator withStrictFiltersJoinOperator) {
+ this.strictFiltersJoinOperator = withStrictFiltersJoinOperator;
+ }
+
+ /**
+ * Initializes a new instance of the {@link QnAMakerOptions} class.
+ */
+ public QnAMakerOptions() {
+ this.scoreThreshold = SCORE_THRESHOLD;
+ }
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMakerRecognizer.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMakerRecognizer.java
new file mode 100644
index 000000000..b93e7f788
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/QnAMakerRecognizer.java
@@ -0,0 +1,542 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.base.Strings;
+import com.microsoft.bot.ai.qna.models.Metadata;
+import com.microsoft.bot.ai.qna.models.QnARequestContext;
+import com.microsoft.bot.ai.qna.models.QueryResult;
+import com.microsoft.bot.ai.qna.models.RankerTypes;
+import com.microsoft.bot.builder.IntentScore;
+import com.microsoft.bot.builder.RecognizerResult;
+import com.microsoft.bot.dialogs.DialogContext;
+import com.microsoft.bot.dialogs.Recognizer;
+import com.microsoft.bot.schema.Activity;
+import com.microsoft.bot.schema.Serialization;
+
+/**
+ * IRecognizer implementation which uses QnAMaker KB to identify intents.
+ */
+public class QnAMakerRecognizer extends Recognizer {
+
+ private static final Integer TOP_DEFAULT_VALUE = 3;
+ private static final Float THRESHOLD_DEFAULT_VALUE = 0.3f;
+
+ private final String qnAMatchIntent = "QnAMatch";
+
+ private final String intentPrefix = "intent=";
+
+ @JsonProperty("knowledgeBaseId")
+ private String knowledgeBaseId;
+
+ @JsonProperty("hostname")
+ private String hostName;
+
+ @JsonProperty("endpointKey")
+ private String endpointKey;
+
+ @JsonProperty("top")
+ private Integer top = TOP_DEFAULT_VALUE;
+
+ @JsonProperty("threshold")
+ private Float threshold = THRESHOLD_DEFAULT_VALUE;
+
+ @JsonProperty("isTest")
+ private Boolean isTest;
+
+ @JsonProperty("rankerType")
+ private String rankerType = RankerTypes.DEFAULT_RANKER_TYPE;
+
+ @JsonProperty("strictFiltersJoinOperator")
+ private JoinOperator strictFiltersJoinOperator;
+
+ @JsonProperty("includeDialogNameInMetadata")
+ private Boolean includeDialogNameInMetadata = true;
+
+ @JsonProperty("metadata")
+ private Metadata[] metadata;
+
+ @JsonProperty("context")
+ private QnARequestContext context;
+
+ @JsonProperty("qnaId")
+ private Integer qnAId = 0;
+
+ @JsonProperty("logPersonalInformation")
+ private Boolean logPersonalInformation = false;
+
+ /**
+ * Gets key used when adding the intent to the {@link RecognizerResult} intents
+ * collection.
+ *
+ * @return Key used when adding the intent to the {@link RecognizerResult}
+ * intents collection.
+ */
+ public String getQnAMatchIntent() {
+ return qnAMatchIntent;
+ }
+
+ /**
+ * Gets the KnowledgeBase Id of your QnA Maker KnowledgeBase.
+ *
+ * @return The knowledgebase Id.
+ */
+ public String getKnowledgeBaseId() {
+ return knowledgeBaseId;
+ }
+
+ /**
+ * Sets the KnowledgeBase Id of your QnA Maker KnowledgeBase.
+ *
+ * @param withKnowledgeBaseId The knowledgebase Id.
+ */
+ public void setKnowledgeBaseId(String withKnowledgeBaseId) {
+ this.knowledgeBaseId = withKnowledgeBaseId;
+ }
+
+ /**
+ * Gets the Hostname for your QnA Maker service.
+ *
+ * @return The host name of the QnA Maker knowledgebase.
+ */
+ public String getHostName() {
+ return hostName;
+ }
+
+ /**
+ * Sets the Hostname for your QnA Maker service.
+ *
+ * @param withHostName The host name of the QnA Maker knowledgebase.
+ */
+ public void setHostName(String withHostName) {
+ this.hostName = withHostName;
+ }
+
+ /**
+ * Gets the Endpoint key for the QnA Maker KB.
+ *
+ * @return The endpoint key for the QnA service.
+ */
+ public String getEndpointKey() {
+ return endpointKey;
+ }
+
+ /**
+ * Sets the Endpoint key for the QnA Maker KB.
+ *
+ * @param withEndpointKey The endpoint key for the QnA service.
+ */
+ public void setEndpointKey(String withEndpointKey) {
+ this.endpointKey = withEndpointKey;
+ }
+
+ /**
+ * Gets the number of results you want.
+ *
+ * @return The number of results you want.
+ */
+ public Integer getTop() {
+ return top;
+ }
+
+ /**
+ * Sets the number of results you want.
+ *
+ * @param withTop The number of results you want.
+ */
+ public void setTop(Integer withTop) {
+ this.top = withTop;
+ }
+
+ /**
+ * Gets the threshold score to filter results.
+ *
+ * @return The threshold for the results.
+ */
+ public Float getThreshold() {
+ return threshold;
+ }
+
+ /**
+ * Sets the threshold score to filter results.
+ *
+ * @param withThreshold The threshold for the results.
+ */
+ public void setThreshold(Float withThreshold) {
+ this.threshold = withThreshold;
+ }
+
+ /**
+ * Gets a value indicating whether gets or sets environment of knowledgebase to
+ * be called.
+ *
+ * @return A value indicating whether to call test or prod environment of
+ * knowledgebase.
+ */
+ public Boolean getIsTest() {
+ return isTest;
+ }
+
+ /**
+ * Sets a value indicating whether gets or sets environment of knowledgebase to
+ * be called.
+ *
+ * @param withIsTest A value indicating whether to call test or prod environment of
+ * knowledgebase.
+ */
+ public void setIsTest(Boolean withIsTest) {
+ this.isTest = withIsTest;
+ }
+
+ /**
+ * Gets ranker Type.
+ *
+ * @return The desired RankerType.
+ */
+ public String getRankerType() {
+ return rankerType;
+ }
+
+ /**
+ * Sets ranker Type.
+ *
+ * @param withRankerType The desired RankerType.
+ */
+ public void setRankerType(String withRankerType) {
+ this.rankerType = withRankerType;
+ }
+
+ /**
+ * Gets {@link Metadata} join operator.
+ *
+ * @return A value used for Join operation of Metadata .
+ */
+ public JoinOperator getStrictFiltersJoinOperator() {
+ return strictFiltersJoinOperator;
+ }
+
+ /**
+ * Sets {@link Metadata} join operator.
+ *
+ * @param withStrictFiltersJoinOperator A value used for Join operation of Metadata
+ * {@link Metadata}.
+ */
+ public void setStrictFiltersJoinOperator(JoinOperator withStrictFiltersJoinOperator) {
+ this.strictFiltersJoinOperator = withStrictFiltersJoinOperator;
+ }
+
+ /**
+ * Gets the whether to include the dialog name metadata for QnA context.
+ *
+ * @return A bool or boolean expression.
+ */
+ public Boolean getIncludeDialogNameInMetadata() {
+ return includeDialogNameInMetadata;
+ }
+
+ /**
+ * Sets the whether to include the dialog name metadata for QnA context.
+ *
+ * @param withIncludeDialogNameInMetadata A bool or boolean expression.
+ */
+ public void setIncludeDialogNameInMetadata(Boolean withIncludeDialogNameInMetadata) {
+ this.includeDialogNameInMetadata = withIncludeDialogNameInMetadata;
+ }
+
+ /**
+ * Gets an expression to evaluate to set additional metadata name value pairs.
+ *
+ * @return An expression to evaluate for pairs of metadata.
+ */
+ public Metadata[] getMetadata() {
+ return metadata;
+ }
+
+ /**
+ * Sets an expression to evaluate to set additional metadata name value pairs.
+ *
+ * @param withMetadata An expression to evaluate for pairs of metadata.
+ */
+ public void setMetadata(Metadata[] withMetadata) {
+ this.metadata = withMetadata;
+ }
+
+ /**
+ * Gets an expression to evaluate to set the context.
+ *
+ * @return An expression to evaluate to QnARequestContext to pass as context.
+ */
+ public QnARequestContext getContext() {
+ return context;
+ }
+
+ /**
+ * Sets an expression to evaluate to set the context.
+ *
+ * @param withContext An expression to evaluate to QnARequestContext to pass as
+ * context.
+ */
+ public void setContext(QnARequestContext withContext) {
+ this.context = withContext;
+ }
+
+ /**
+ * Gets an expression or numberto use for the QnAId paratemer.
+ *
+ * @return The expression or number.
+ */
+ public Integer getQnAId() {
+ return qnAId;
+ }
+
+ /**
+ * Sets an expression or numberto use for the QnAId paratemer.
+ *
+ * @param withQnAId The expression or number.
+ */
+ public void setQnAId(Integer withQnAId) {
+ this.qnAId = withQnAId;
+ }
+
+ /**
+ * Gets the flag to determine if personal information should be logged in
+ * telemetry.
+ *
+ * @return The flag to indicate in personal information should be logged in
+ * telemetry.
+ */
+ public Boolean getLogPersonalInformation() {
+ return logPersonalInformation;
+ }
+
+ /**
+ * Sets the flag to determine if personal information should be logged in
+ * telemetry.
+ *
+ * @param withLogPersonalInformation The flag to indicate in personal information
+ * should be logged in telemetry.
+ */
+ public void setLogPersonalInformation(Boolean withLogPersonalInformation) {
+ this.logPersonalInformation = withLogPersonalInformation;
+ }
+
+ /**
+ * Return results of the call to QnA Maker.
+ *
+ * @param dialogContext Context object containing information for a single
+ * turn of conversation with a user.
+ * @param activity The incoming activity received from the user. The
+ * Text property value is used as the query text for
+ * QnA Maker.
+ * @param telemetryProperties Additional properties to be logged to telemetry
+ * with the LuisResult event.
+ * @param telemetryMetrics Additional metrics to be logged to telemetry with
+ * the LuisResult event.
+ * @return A {@link RecognizerResult} containing the QnA Maker result.
+ */
+ @Override
+ public CompletableFuture recognize(DialogContext dialogContext, Activity activity,
+ Map telemetryProperties, Map telemetryMetrics) {
+ // Identify matched intents
+ RecognizerResult recognizerResult = new RecognizerResult();
+ recognizerResult.setText(activity.getText());
+ recognizerResult.setIntents(new HashMap());
+ if (Strings.isNullOrEmpty(activity.getText())) {
+ recognizerResult.getIntents().put("None", new IntentScore());
+ return CompletableFuture.completedFuture(recognizerResult);
+ }
+
+ List filters = new ArrayList();
+ // TODO this should be uncommented as soon as Expression is added in Java
+ /* if (this.includeDialogNameInMetadata.getValue(dialogContext.getState())) {
+ filters.add(new Metadata() {
+ {
+ setName("dialogName");
+ setValue(dialogContext.getActiveDialog().getId());
+ }
+ });
+ } */
+
+ // if there is $qna.metadata set add to filters
+ Metadata[] externalMetadata = this.metadata;
+ if (externalMetadata != null) {
+ filters.addAll(Arrays.asList(externalMetadata));
+ }
+
+ QnAMakerOptions options = new QnAMakerOptions() {
+ {
+ setContext(context);
+ setThreshold(threshold);
+ setStrictFilters(filters.toArray(new Metadata[filters.size()]));
+ setTop(top);
+ setQnAId(qnAId);
+ setIsTest(isTest);
+ setStrictFiltersJoinOperator(strictFiltersJoinOperator);
+ }
+ };
+ // Calling QnAMaker to get response.
+ return this.getQnAMakerClient(dialogContext).thenCompose(qnaClient -> {
+ return qnaClient.getAnswers(dialogContext.getContext(), options, null, null).thenApply(answers -> {
+ if (answers.length > 0) {
+ QueryResult topAnswer = null;
+ for (QueryResult answer : answers) {
+ if (topAnswer == null || answer.getScore() > topAnswer.getScore()) {
+ topAnswer = answer;
+ }
+ }
+ Float internalTopAnswer = topAnswer.getScore();
+ if (topAnswer.getAnswer().trim().toUpperCase().startsWith(intentPrefix.toUpperCase())) {
+ recognizerResult.getIntents().put(
+ topAnswer.getAnswer().trim().substring(intentPrefix.length()).trim(),
+ new IntentScore() {
+ {
+ setScore(internalTopAnswer);
+ }
+ });
+ } else {
+ recognizerResult.getIntents().put(this.qnAMatchIntent, new IntentScore() {
+ {
+ setScore(internalTopAnswer);
+ }
+ });
+ }
+ ObjectMapper mapper = new ObjectMapper();
+ ObjectNode entitiesNode = mapper.createObjectNode();
+ List answerArray = new ArrayList();
+ answerArray.add(topAnswer.getAnswer());
+ ArrayNode entitiesArrayNode = entitiesNode.putArray("answer");
+ entitiesArrayNode.add(topAnswer.getAnswer());
+
+ ObjectNode instance = entitiesNode.putObject("$instance");
+ ArrayNode instanceArrayNode = instance.putArray("answer");
+ ObjectNode data = instanceArrayNode.addObject();
+ data.setAll((ObjectNode) mapper.valueToTree(topAnswer));
+ data.put("startIndex", 0);
+ data.put("endIndex", activity.getText().length());
+
+ recognizerResult.setEntities(entitiesNode);
+ recognizerResult.getProperties().put("answers", mapper.valueToTree(answers));
+ } else {
+ recognizerResult.getIntents().put("None", new IntentScore() {
+ {
+ setScore(1.0f);
+ }
+ });
+ }
+
+ this.trackRecognizerResult(dialogContext, "QnAMakerRecognizerResult", this
+ .fillRecognizerResultTelemetryProperties(recognizerResult, telemetryProperties, dialogContext),
+ telemetryMetrics);
+ return recognizerResult;
+ });
+ });
+ }
+
+ /**
+ * Gets an instance of {@link QnAMakerClient}.
+ *
+ * @param dc The {@link DialogContext} used to access state.
+ * @return An instance of {@link QnAMakerClient}.
+ */
+ protected CompletableFuture getQnAMakerClient(DialogContext dc) {
+ QnAMakerClient qnaClient = dc.getContext().getTurnState().get(QnAMakerClient.class);
+ if (qnaClient != null) {
+ // return mock client
+ return CompletableFuture.completedFuture(qnaClient);
+ }
+
+ String epKey = this.endpointKey;
+ String hn = this.hostName;
+ String kbId = this.knowledgeBaseId;
+ Boolean logPersonalInfo = this.logPersonalInformation;
+ QnAMakerEndpoint endpoint = new QnAMakerEndpoint() {
+ {
+ setEndpointKey(epKey);
+ setHost(hn);
+ setKnowledgeBaseId(kbId);
+ }
+ };
+
+ return CompletableFuture.completedFuture(
+ new QnAMaker(endpoint, new QnAMakerOptions(), this.getTelemetryClient(), logPersonalInfo));
+ }
+
+ /**
+ * Uses the RecognizerResult to create a list of properties to be included when
+ * tracking the result in telemetry.
+ *
+ * @param recognizerResult Recognizer Result.
+ * @param telemetryProperties A list of properties to append or override the
+ * properties created using the RecognizerResult.
+ * @param dialogContext Dialog Context.
+ * @return A dictionary that can be included when calling the TrackEvent method
+ * on the TelemetryClient.
+ */
+ @Override
+ protected Map fillRecognizerResultTelemetryProperties(RecognizerResult recognizerResult,
+ Map telemetryProperties, @Nullable DialogContext dialogContext) {
+ if (dialogContext == null) {
+ throw new IllegalArgumentException(
+ "dialogContext: DialogContext needed for state in "
+ + "AdaptiveRecognizer.FillRecognizerResultTelemetryProperties method.");
+ }
+
+ Map properties = new HashMap() {
+ {
+ put("TopIntent",
+ !recognizerResult.getIntents().isEmpty()
+ ? (String) recognizerResult.getIntents().keySet().toArray()[0]
+ : null);
+ put("TopIntentScore",
+ !recognizerResult.getIntents().isEmpty()
+ ? Double.toString(
+ ((IntentScore) recognizerResult.getIntents().values().toArray()[0]).getScore())
+ : null);
+ put("Intents",
+ !recognizerResult.getIntents().isEmpty()
+ ? Serialization.toStringSilent(recognizerResult.getIntents())
+ : null);
+ put("Entities",
+ recognizerResult.getEntities() != null
+ ? Serialization.toStringSilent(recognizerResult.getEntities())
+ : null);
+ put("AdditionalProperties",
+ !recognizerResult.getProperties().isEmpty()
+ ? Serialization.toStringSilent(recognizerResult.getProperties())
+ : null);
+ }
+ };
+
+ if (logPersonalInformation && !Strings.isNullOrEmpty(recognizerResult.getText())) {
+ properties.put("Text", recognizerResult.getText());
+ properties.put("AlteredText", recognizerResult.getAlteredText());
+ }
+
+ // Additional Properties can override "stock properties".
+ if (telemetryProperties != null) {
+ telemetryProperties.putAll(properties);
+ Map> telemetryPropertiesMap = telemetryProperties.entrySet().stream().collect(
+ Collectors.groupingBy(Entry::getKey, Collectors.mapping(Entry::getValue, Collectors.toList())));
+ return telemetryPropertiesMap.entrySet().stream()
+ .collect(Collectors.toMap(Entry::getKey, e -> e.getValue().get(0)));
+ }
+
+ return properties;
+ }
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/TelemetryQnAMaker.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/TelemetryQnAMaker.java
new file mode 100644
index 000000000..84152e768
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/TelemetryQnAMaker.java
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna;
+
+import com.microsoft.bot.ai.qna.models.QueryResult;
+import com.microsoft.bot.builder.BotTelemetryClient;
+import com.microsoft.bot.builder.TurnContext;
+
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+import javax.annotation.Nullable;
+
+/**
+ * Interface for adding telemetry logging capabilities to {@link QnAMaker}/>.
+ */
+public interface TelemetryQnAMaker {
+
+ /**
+ * Gets a value indicating whether determines whether to log personal
+ * information that came from the user.
+ *
+ * @return If true, will log personal information into the
+ * IBotTelemetryClient.TrackEvent method; otherwise the properties will
+ * be filtered.
+ */
+ Boolean getLogPersonalInformation();
+
+ /**
+ * Gets the currently configured {@link BotTelemetryClient} that logs the
+ * QnaMessage event.
+ *
+ * @return The {@link BotTelemetryClient} being used to log events.
+ */
+ BotTelemetryClient getTelemetryClient();
+
+ /**
+ * Generates an answer from the knowledge base.
+ *
+ * @param turnContext The Turn Context that contains the user question
+ * to be queried against your knowledge base.
+ * @param options The options for the QnA Maker knowledge base. If
+ * null, constructor option is used for this
+ * instance.
+ * @param telemetryProperties Additional properties to be logged to telemetry
+ * with the QnaMessage event.
+ * @param telemetryMetrics Additional metrics to be logged to telemetry with
+ * the QnaMessage event.
+ * @return A list of answers for the user query, sorted in decreasing order of
+ * ranking score.
+ */
+ CompletableFuture getAnswers(TurnContext turnContext, QnAMakerOptions options,
+ Map telemetryProperties, @Nullable Map telemetryMetrics);
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/dialogs/QnAMakerDialog.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/dialogs/QnAMakerDialog.java
new file mode 100644
index 000000000..3a41b72cd
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/dialogs/QnAMakerDialog.java
@@ -0,0 +1,972 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna.dialogs;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+import javax.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.microsoft.bot.ai.qna.QnADialogResponseOptions;
+import com.microsoft.bot.ai.qna.QnAMaker;
+import com.microsoft.bot.ai.qna.QnAMakerClient;
+import com.microsoft.bot.ai.qna.QnAMakerEndpoint;
+import com.microsoft.bot.ai.qna.QnAMakerOptions;
+import com.microsoft.bot.ai.qna.models.FeedbackRecord;
+import com.microsoft.bot.ai.qna.models.FeedbackRecords;
+import com.microsoft.bot.ai.qna.models.Metadata;
+import com.microsoft.bot.ai.qna.models.QnARequestContext;
+import com.microsoft.bot.ai.qna.models.QueryResult;
+import com.microsoft.bot.ai.qna.models.QueryResults;
+import com.microsoft.bot.ai.qna.models.RankerTypes;
+import com.microsoft.bot.ai.qna.utils.ActiveLearningUtils;
+import com.microsoft.bot.ai.qna.utils.BindToActivity;
+import com.microsoft.bot.ai.qna.utils.QnACardBuilder;
+import com.microsoft.bot.builder.MessageFactory;
+import com.microsoft.bot.dialogs.DialogContext;
+import com.microsoft.bot.dialogs.DialogEvent;
+import com.microsoft.bot.dialogs.DialogReason;
+import com.microsoft.bot.dialogs.DialogTurnResult;
+import com.microsoft.bot.dialogs.DialogTurnStatus;
+import com.microsoft.bot.dialogs.ObjectPath;
+import com.microsoft.bot.dialogs.TurnPath;
+import com.microsoft.bot.dialogs.WaterfallDialog;
+import com.microsoft.bot.dialogs.WaterfallStepContext;
+import com.microsoft.bot.schema.Activity;
+import com.microsoft.bot.schema.ActivityTypes;
+
+import okhttp3.OkHttpClient;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A dialog that supports multi-step and adaptive-learning QnA Maker services.
+ * An instance of this class targets a specific QnA Maker knowledge base. It
+ * supports knowledge bases that include follow-up prompt and active learning
+ * features.
+ */
+public class QnAMakerDialog extends WaterfallDialog {
+
+ @JsonIgnore
+ private OkHttpClient httpClient;
+
+ @JsonProperty("knowledgeBaseId")
+ private String knowledgeBaseId;
+
+ @JsonProperty("hostName")
+ private String hostName;
+
+ @JsonProperty("endpointKey")
+ private String endpointKey;
+
+ @JsonProperty("threshold")
+ private Float threshold = DEFAULT_THRESHOLD;
+
+ @JsonProperty("top")
+ private Integer top = DEFAULT_TOP_N;
+
+ @JsonProperty("noAnswer")
+ private BindToActivity noAnswer = new BindToActivity(MessageFactory.text(DEFAULT_NO_ANSWER));
+
+ @JsonProperty("activeLearningCardTitle")
+ private String activeLearningCardTitle;
+
+ @JsonProperty("cardNoMatchText")
+ private String cardNoMatchText;
+
+ @JsonProperty("cardNoMatchResponse")
+ private BindToActivity cardNoMatchResponse = new BindToActivity(
+ MessageFactory.text(DEFAULT_CARD_NO_MATCH_RESPONSE));
+
+ @JsonProperty("strictFilters")
+ private Metadata[] strictFilters;
+
+ @JsonProperty("logPersonalInformation")
+ private Boolean logPersonalInformation = false;
+
+ @JsonProperty("isTest")
+ private Boolean isTest;
+
+ @JsonProperty("rankerType")
+ private String rankerType = RankerTypes.DEFAULT_RANKER_TYPE;
+
+ /**
+ * The path for storing and retrieving QnA Maker context data. This represents
+ * context about the current or previous call to QnA Maker. It is stored within
+ * the current step's {@link WaterfallStepContext}. It supports QnA Maker's
+ * follow-up prompt and active learning features.
+ */
+ protected static final String QNA_CONTEXT_DATA = "qnaContextData";
+
+ /**
+ * The path for storing and retrieving the previous question ID. This represents
+ * the QnA question ID from the previous turn. It is stored within the current
+ * step's {@link WaterfallStepContext}. It supports QnA Maker's follow-up prompt
+ * and active learning features.
+ */
+ protected static final String PREVIOUS_QNA_ID = "prevQnAId";
+
+ /**
+ * The path for storing and retrieving the options for this instance of the
+ * dialog. This includes the options with which the dialog was started and
+ * options expected by the QnA Maker service. It is stored within the current
+ * step's {@link WaterfallStepContext}. It supports QnA Maker and the dialog
+ * system.
+ */
+ protected static final String OPTIONS = "options";
+
+ // Dialog Options parameters
+
+ /**
+ * The default threshold for answers returned, based on score.
+ */
+ protected static final Float DEFAULT_THRESHOLD = 0.3F;
+
+ /**
+ * The default maximum number of answers to be returned for the question.
+ */
+ protected static final Integer DEFAULT_TOP_N = 3;
+
+ private static final String DEFAULT_NO_ANSWER = "No QnAMaker answers found.";
+
+ // Card parameters
+ private static final String DEFAULT_CARD_TITLE = "Did you mean:";
+ private static final String DEFAULT_CARD_NO_MATCH_TEXT = "None of the above.";
+ private static final String DEFAULT_CARD_NO_MATCH_RESPONSE = "Thanks for the feedback.";
+ private static final Integer PERCENTAGE_DIVISOR = 100;
+
+ /**
+ * Gets the OkHttpClient instance to use for requests to the QnA Maker service.
+ *
+ * @return The HTTP client.
+ */
+ public OkHttpClient getHttpClient() {
+ return this.httpClient;
+ }
+
+ /**
+ * Gets the OkHttpClient instance to use for requests to the QnA Maker service.
+ *
+ * @param withHttpClient The HTTP client.
+ */
+ public void setHttpClient(OkHttpClient withHttpClient) {
+ this.httpClient = withHttpClient;
+ }
+
+ /**
+ * Gets the QnA Maker knowledge base ID to query.
+ *
+ * @return The knowledge base ID or an expression which evaluates to the
+ * knowledge base ID.
+ */
+ public String getKnowledgeBaseId() {
+ return this.knowledgeBaseId;
+ }
+
+ /**
+ * Sets the QnA Maker knowledge base ID to query.
+ *
+ * @param withKnowledgeBaseId The knowledge base ID or an expression which
+ * evaluates to the knowledge base ID.
+ */
+ public void setKnowledgeBaseId(String withKnowledgeBaseId) {
+ this.knowledgeBaseId = withKnowledgeBaseId;
+ }
+
+ /**
+ * Gets the QnA Maker host URL for the knowledge base.
+ *
+ * @return The QnA Maker host URL or an expression which evaluates to the host
+ * URL.
+ */
+ public String getHostName() {
+ return this.hostName;
+ }
+
+ /**
+ * Sets the QnA Maker host URL for the knowledge base.
+ *
+ * @param withHostName The QnA Maker host URL or an expression which evaluates
+ * to the host URL.
+ */
+ public void setHostName(String withHostName) {
+ this.hostName = withHostName;
+ }
+
+ /**
+ * Gets the QnA Maker endpoint key to use to query the knowledge base.
+ *
+ * @return The QnA Maker endpoint key to use or an expression which evaluates to
+ * the endpoint key.
+ */
+ public String getEndpointKey() {
+ return this.endpointKey;
+ }
+
+ /**
+ * Sets the QnA Maker endpoint key to use to query the knowledge base.
+ *
+ * @param withEndpointKey The QnA Maker endpoint key to use or an expression
+ * which evaluates to the endpoint key.
+ */
+ public void setEndpointKey(String withEndpointKey) {
+ this.endpointKey = withEndpointKey;
+ }
+
+ /**
+ * Gets the threshold for answers returned, based on score.
+ *
+ * @return The threshold for answers returned or an expression which evaluates
+ * to the threshold.
+ */
+ public Float getThreshold() {
+ return this.threshold;
+ }
+
+ /**
+ * Sets the threshold for answers returned, based on score.
+ *
+ * @param withThreshold The threshold for answers returned or an expression
+ * which evaluates to the threshold.
+ */
+ public void setThreshold(Float withThreshold) {
+ this.threshold = withThreshold;
+ }
+
+ /**
+ * Gets the maximum number of answers to return from the knowledge base.
+ *
+ * @return The maximum number of answers to return from the knowledge base or an
+ * expression which evaluates to the maximum number to return.
+ */
+ public Integer getTop() {
+ return this.top;
+ }
+
+ /**
+ * Sets the maximum number of answers to return from the knowledge base.
+ *
+ * @param withTop The maximum number of answers to return from the knowledge
+ * base or an expression which evaluates to the maximum number to
+ * return.
+ */
+ public void setTop(Integer withTop) {
+ this.top = withTop;
+ }
+
+ /**
+ * Gets the template to send the user when QnA Maker does not find an answer.
+ *
+ * @return The template to send the user when QnA Maker does not find an answer.
+ */
+ public BindToActivity getNoAnswer() {
+ return this.noAnswer;
+ }
+
+ /**
+ * Sets the template to send the user when QnA Maker does not find an answer.
+ *
+ * @param withNoAnswer The template to send the user when QnA Maker does not
+ * find an answer.
+ */
+ public void setNoAnswer(BindToActivity withNoAnswer) {
+ this.noAnswer = withNoAnswer;
+ }
+
+ /**
+ * Gets the card title to use when showing active learning options to the user,
+ * if active learning is enabled.
+ *
+ * @return The path card title to use when showing active learning options to
+ * the user or an expression which evaluates to the card title.
+ */
+ public String getActiveLearningCardTitle() {
+ return this.activeLearningCardTitle;
+ }
+
+ /**
+ * Sets the card title to use when showing active learning options to the user,
+ * if active learning is enabled.
+ *
+ * @param withActiveLearningCardTitle The path card title to use when showing
+ * active learning options to the user or an
+ * expression which evaluates to the card
+ * title.
+ */
+ public void setActiveLearningCardTitle(String withActiveLearningCardTitle) {
+ this.activeLearningCardTitle = withActiveLearningCardTitle;
+ }
+
+ /**
+ * Gets the button text to use with active learning options, allowing a user to
+ * indicate none of the options are applicable.
+ *
+ * @return The button text to use with active learning options or an expression
+ * which evaluates to the button text.
+ */
+ public String getCardNoMatchText() {
+ return this.cardNoMatchText;
+ }
+
+ /**
+ * Sets the button text to use with active learning options, allowing a user to
+ * indicate none of the options are applicable.
+ *
+ * @param withCardNoMatchText The button text to use with active learning
+ * options or an expression which evaluates to the
+ * button text.
+ */
+ public void setCardNoMatchText(String withCardNoMatchText) {
+ this.cardNoMatchText = withCardNoMatchText;
+ }
+
+ /**
+ * Gets the template to send the user if they select the no match option on an
+ * active learning card.
+ *
+ * @return The template to send the user if they select the no match option on
+ * an active learning card.
+ */
+ public BindToActivity getCardNoMatchResponse() {
+ return this.cardNoMatchResponse;
+ }
+
+ /**
+ * Sets the template to send the user if they select the no match option on an
+ * active learning card.
+ *
+ * @param withCardNoMatchResponse The template to send the user if they select
+ * the no match option on an active learning
+ * card.
+ */
+ public void setCardNoMatchResponse(BindToActivity withCardNoMatchResponse) {
+ this.cardNoMatchResponse = withCardNoMatchResponse;
+ }
+
+ /**
+ * Gets the QnA Maker metadata with which to filter or boost queries to the
+ * knowledge base; or null to apply none.
+ *
+ * @return The QnA Maker metadata with which to filter or boost queries to the
+ * knowledge base or an expression which evaluates to the QnA Maker
+ * metadata.
+ */
+ public Metadata[] getStrictFilters() {
+ return this.strictFilters;
+ }
+
+ /**
+ * Sets the QnA Maker metadata with which to filter or boost queries to the
+ * knowledge base; or null to apply none.
+ *
+ * @param withStrictFilters The QnA Maker metadata with which to filter or boost
+ * queries to the knowledge base or an expression which
+ * evaluates to the QnA Maker metadata.
+ */
+ public void setStrictFilters(Metadata[] withStrictFilters) {
+ this.strictFilters = withStrictFilters;
+ }
+
+ /**
+ * Gets the flag to determine if personal information should be logged in
+ * telemetry.
+ *
+ * @return The flag to indicate in personal information should be logged in
+ * telemetry.
+ */
+ public Boolean getLogPersonalInformation() {
+ return this.logPersonalInformation;
+ }
+
+ /**
+ * Sets the flag to determine if personal information should be logged in
+ * telemetry.
+ *
+ * @param withLogPersonalInformation The flag to indicate in personal
+ * information should be logged in telemetry.
+ */
+ public void setLogPersonalInformation(Boolean withLogPersonalInformation) {
+ this.logPersonalInformation = withLogPersonalInformation;
+ }
+
+ /**
+ * Gets a value indicating whether gets or sets environment of knowledgebase to
+ * be called.
+ *
+ * @return A value indicating whether to call test or prod environment of
+ * knowledge base.
+ */
+ public Boolean getIsTest() {
+ return this.isTest;
+ }
+
+ /**
+ * Sets a value indicating whether gets or sets environment of knowledgebase to
+ * be called.
+ *
+ * @param withIsTest A value indicating whether to call test or prod environment
+ * of knowledge base.
+ */
+ public void setIsTest(Boolean withIsTest) {
+ this.isTest = withIsTest;
+ }
+
+ /**
+ * Gets the QnA Maker ranker type to use.
+ *
+ * @return The QnA Maker ranker type to use or an expression which evaluates to
+ * the ranker type.
+ */
+ public String getRankerType() {
+ return this.rankerType;
+ }
+
+ /**
+ * Sets the QnA Maker ranker type to use.
+ *
+ * @param withRankerType The QnA Maker ranker type to use or an expression which
+ * evaluates to the ranker type.
+ */
+ public void setRankerType(String withRankerType) {
+ this.rankerType = withRankerType;
+ }
+
+ /**
+ * Initializes a new instance of the @{link QnAMakerDialog} class.
+ *
+ * @param dialogId The ID of the @{link Dialog}.
+ * @param withKnowledgeBaseId The ID of the QnA Maker knowledge base to
+ * query.
+ * @param withEndpointKey The QnA Maker endpoint key to use to query
+ * the knowledge base.
+ * @param withHostName The QnA Maker host URL for the knowledge
+ * base, starting with "https://" and ending
+ * with "/qnamaker".
+ * @param withNoAnswer The activity to send the user when QnA
+ * Maker does not find an answer.
+ * @param withThreshold The threshold for answers returned, based
+ * on score.
+ * @param withActiveLearningCardTitle The card title to use when showing active
+ * learning options to the user, if active
+ * learning is enabled.
+ * @param withCardNoMatchText The button text to use with active
+ * learning options, allowing a user to
+ * indicate none of the options are
+ * applicable.
+ * @param withTop The maximum number of answers to return
+ * from the knowledge base.
+ * @param withCardNoMatchResponse The activity to send the user if they
+ * select the no match option on an active
+ * learning card.
+ * @param withStrictFilters QnA Maker metadata with which to filter or
+ * boost queries to the knowledge base; or
+ * null to apply none.
+ * @param withHttpClient An HTTP client to use for requests to the
+ * QnA Maker Service; or `null` to use a
+ * default client.
+ */
+ @SuppressWarnings("checkstyle:ParameterNumber")
+ public QnAMakerDialog(String dialogId, String withKnowledgeBaseId, String withEndpointKey, String withHostName,
+ @Nullable Activity withNoAnswer, Float withThreshold, String withActiveLearningCardTitle,
+ String withCardNoMatchText, Integer withTop, @Nullable Activity withCardNoMatchResponse,
+ @Nullable Metadata[] withStrictFilters, @Nullable OkHttpClient withHttpClient) {
+ super(dialogId, null);
+ if (knowledgeBaseId == null) {
+ throw new IllegalArgumentException("knowledgeBaseId");
+ }
+ this.knowledgeBaseId = withKnowledgeBaseId;
+ if (hostName == null) {
+ throw new IllegalArgumentException("hostName");
+ }
+ this.hostName = withHostName;
+ if (withEndpointKey == null) {
+ throw new IllegalArgumentException("endpointKey");
+ }
+ this.endpointKey = withEndpointKey;
+ this.threshold = withThreshold != null ? withThreshold : DEFAULT_THRESHOLD;
+ this.top = withTop != null ? withTop : DEFAULT_TOP_N;
+ this.activeLearningCardTitle = withActiveLearningCardTitle != null ? withActiveLearningCardTitle
+ : DEFAULT_CARD_TITLE;
+ this.cardNoMatchText = withCardNoMatchText != null ? withCardNoMatchText : DEFAULT_CARD_NO_MATCH_TEXT;
+ this.strictFilters = withStrictFilters;
+ this.noAnswer = new BindToActivity(
+ withNoAnswer != null ? withNoAnswer : MessageFactory.text(DEFAULT_NO_ANSWER));
+ this.cardNoMatchResponse = new BindToActivity(withCardNoMatchResponse != null ? withCardNoMatchResponse
+ : MessageFactory.text(DEFAULT_CARD_NO_MATCH_RESPONSE));
+ this.httpClient = withHttpClient;
+
+ // add waterfall steps
+ this.addStep(this::callGenerateAnswer);
+ this.addStep(this::callTrain);
+ this.addStep(this::checkForMultiTurnPrompt);
+ this.addStep(this::displayQnAResult);
+ }
+
+ /**
+ * Initializes a new instance of the {@link QnAMakerDialog} class.
+ *
+ * @param withKnowledgeBaseId The ID of the QnA Maker knowledge base to
+ * query.
+ * @param withEndpointKey The QnA Maker endpoint key to use to query
+ * the knowledge base.
+ * @param withHostName The QnA Maker host URL for the knowledge
+ * base, starting with "https://" and ending
+ * with "/qnamaker".
+ * @param withNoAnswer The activity to send the user when QnA
+ * Maker does not find an answer.
+ * @param withThreshold The threshold for answers returned, based
+ * on score.
+ * @param withActiveLearningCardTitle The card title to use when showing active
+ * learning options to the user, if active
+ * learning is enabled.
+ * @param withCardNoMatchText The button text to use with active
+ * learning options, allowing a user to
+ * indicate none of the options are
+ * applicable.
+ * @param withTop The maximum number of answers to return
+ * from the knowledge base.
+ * @param withCardNoMatchResponse The activity to send the user if they
+ * select the no match option on an active
+ * learning card.
+ * @param withStrictFilters QnA Maker metadata with which to filter or
+ * boost queries to the knowledge base; or
+ * null to apply none.
+ * @param withHttpClient An HTTP client to use for requests to the
+ * QnA Maker Service; or `null` to use a
+ * default client.
+ */
+ @SuppressWarnings("checkstyle:ParameterNumber")
+ public QnAMakerDialog(String withKnowledgeBaseId, String withEndpointKey, String withHostName,
+ @Nullable Activity withNoAnswer, Float withThreshold, String withActiveLearningCardTitle,
+ String withCardNoMatchText, Integer withTop, @Nullable Activity withCardNoMatchResponse,
+ @Nullable Metadata[] withStrictFilters,
+ @Nullable OkHttpClient withHttpClient) {
+ this(QnAMakerDialog.class.getName(), withKnowledgeBaseId, withEndpointKey, withHostName, withNoAnswer,
+ withThreshold, withActiveLearningCardTitle, withCardNoMatchText, withTop, withCardNoMatchResponse,
+ withStrictFilters, withHttpClient);
+ }
+
+ /**
+ * Called when the dialog is started and pushed onto the dialog stack.
+ *
+ * @param dc The @{link DialogContext} for the current turn of
+ * conversation.
+ * @param options Optional, initial information to pass to the dialog.
+ * @return A Task representing the asynchronous operation. If the task is
+ * successful, the result indicates whether the dialog is still active
+ * after the turn has been processed by the dialog.
+ *
+ * You can use the @{link options} parameter to include the QnA Maker
+ * context data, which represents context from the previous query. To do
+ * so, the value should include a `context` property of type @{link
+ * QnaResponseContext}.
+ */
+ @Override
+ public CompletableFuture beginDialog(DialogContext dc, @Nullable Object options) {
+ if (dc == null) {
+ throw new IllegalArgumentException("dc");
+ }
+
+ if (!dc.getContext().getActivity().isType(ActivityTypes.MESSAGE)) {
+ return CompletableFuture.completedFuture(END_OF_TURN);
+ }
+
+ QnAMakerOptions qnAMakerOptions = this.getQnAMakerOptions(dc).join();
+ QnADialogResponseOptions qnADialogResponseOptions = this.getQnAResponseOptions(dc).join();
+
+ QnAMakerDialogOptions dialogOptions = new QnAMakerDialogOptions() {
+ {
+ setQnAMakerOptions(qnAMakerOptions);
+ setResponseOptions(qnADialogResponseOptions);
+ }
+ };
+
+ if (options != null) {
+ dialogOptions = (QnAMakerDialogOptions) ObjectPath.assign(dialogOptions, options);
+ }
+
+ ObjectPath.setPathValue(dc.getActiveDialog().getState(), OPTIONS, dialogOptions);
+
+ return super.beginDialog(dc, dialogOptions);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture continueDialog(DialogContext dc) {
+ Boolean interrupted = dc.getState().getValue(TurnPath.INTERRUPTED, false, Boolean.class);
+ if (interrupted) {
+ // if qnamaker was interrupted then end the qnamaker dialog
+ return dc.endDialog();
+ }
+
+ return super.continueDialog(dc);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected CompletableFuture onPreBubbleEvent(DialogContext dc, DialogEvent e) {
+ if (dc.getContext().getActivity().isType(ActivityTypes.MESSAGE)) {
+ // decide whether we want to allow interruption or not.
+ // if we don't get a response from QnA which signifies we expected it,
+ // then we allow interruption.
+
+ String reply = dc.getContext().getActivity().getText();
+ QnAMakerDialogOptions dialogOptions = (QnAMakerDialogOptions) ObjectPath
+ .getPathValue(dc.getActiveDialog().getState(), OPTIONS, QnAMakerDialogOptions.class);
+
+ if (reply.equalsIgnoreCase(dialogOptions.getResponseOptions().getCardNoMatchText())) {
+ // it matches nomatch text, we like that.
+ return CompletableFuture.completedFuture(true);
+ }
+
+ List suggestedQuestions = (List) dc.getState().get("this.suggestedQuestions");
+ if (suggestedQuestions != null && suggestedQuestions.stream()
+ .anyMatch(question -> question.compareToIgnoreCase(reply.trim()) == 0)) {
+ // it matches one of the suggested actions, we like that.
+ return CompletableFuture.completedFuture(true);
+ }
+
+ // Calling QnAMaker to get response.
+ return this.getQnAMakerClient(dc).thenCompose(qnaClient -> {
+ QnAMakerDialog.resetOptions(dc, dialogOptions);
+
+ return qnaClient.getAnswersRaw(dc.getContext(), dialogOptions.getQnAMakerOptions(),
+ null, null)
+ .thenApply(response -> {
+ // cache result so step doesn't have to do it again, this is a turn cache and we
+ // use hashcode so we don't conflict with any other qnamakerdialogs out there.
+ dc.getState().setValue(String.format("turn.qnaresult%s", this.hashCode()), response);
+
+ // disable interruption if we have answers.
+ return !(response.getAnswers().length == 0);
+ });
+ });
+ }
+ // call base for default behavior.
+ return this.onPostBubbleEvent(dc, e);
+ }
+
+ /**
+ * Gets an {@link QnAMakerClient} to use to access the QnA Maker knowledge
+ * base.
+ *
+ * @param dc The {@link DialogContext} for the current turn of conversation.
+ * @return A Task representing the asynchronous operation. If the task is
+ * successful, the result contains the QnA Maker client to use.
+ */
+ protected CompletableFuture getQnAMakerClient(DialogContext dc) {
+ QnAMakerClient qnaClient = (QnAMakerClient) dc.getContext().getTurnState();
+ if (qnaClient != null) {
+ // return mock client
+ return CompletableFuture.completedFuture(qnaClient);
+ }
+
+ QnAMakerEndpoint endpoint = new QnAMakerEndpoint() {
+ {
+ setEndpointKey(endpointKey);
+ setHost(hostName);
+ setKnowledgeBaseId(knowledgeBaseId);
+ }
+ };
+
+ return this.getQnAMakerOptions(dc).thenApply(options -> new QnAMaker(endpoint, options,
+ this.getTelemetryClient(), this.logPersonalInformation));
+ }
+
+ /**
+ * Gets the options for the QnA Maker client that the dialog will use to query
+ * the knowledge base.
+ *
+ * @param dc The for the current turn of
+ * conversation.
+ * @return A representing the asynchronous operation. If the
+ * task is successful, the result contains the QnA Maker options to use.
+ */
+ protected CompletableFuture getQnAMakerOptions(DialogContext dc) {
+ return CompletableFuture.completedFuture(new QnAMakerOptions() {
+ {
+ setScoreThreshold(threshold);
+ setStrictFilters(strictFilters);
+ setTop(top);
+ setContext(new QnARequestContext());
+ setQnAId(0);
+ setRankerType(rankerType);
+ setIsTest(isTest);
+ }
+ });
+ }
+
+ /**
+ * Gets the options the dialog will use to display query results to the user.
+ *
+ * @param dc The {@link DialogContext} for the current turn of conversation.
+ * @return A Task representing the asynchronous operation. If the task is
+ * successful, the result contains the response options to use.
+ */
+ protected CompletableFuture getQnAResponseOptions(DialogContext dc) {
+ return CompletableFuture.completedFuture(new QnADialogResponseOptions() {
+ {
+ setNoAnswer(noAnswer.bind(dc, dc.getState()).join());
+ setActiveLearningCardTitle(activeLearningCardTitle != null
+ ? activeLearningCardTitle
+ : DEFAULT_CARD_TITLE);
+ setCardNoMatchText(
+ cardNoMatchText != null ? cardNoMatchText
+ : DEFAULT_CARD_NO_MATCH_TEXT);
+ setCardNoMatchResponse(cardNoMatchResponse.bind(dc, null).join());
+ }
+ });
+ }
+
+ /**
+ * Displays QnA Result from stepContext through Activity - with first answer
+ * from QnA Maker response.
+ *
+ * @param stepContext stepContext.
+ * @return An object of Task of type {@link DialogTurnResult}.
+ */
+ protected CompletableFuture displayQnAResult(WaterfallStepContext stepContext) {
+ QnAMakerDialogOptions dialogOptions = ObjectPath.getPathValue(stepContext.getActiveDialog().getState(),
+ OPTIONS, QnAMakerDialogOptions.class);
+ String reply = stepContext.getContext().getActivity().getText();
+ if (reply.compareToIgnoreCase(dialogOptions.getResponseOptions().getCardNoMatchText()) == 0) {
+ Activity activity = dialogOptions.getResponseOptions().getCardNoMatchResponse();
+ if (activity == null) {
+ stepContext.getContext().sendActivity(DEFAULT_CARD_NO_MATCH_RESPONSE).join();
+ } else {
+ stepContext.getContext().sendActivity(activity).join();
+ }
+
+ return stepContext.endDialog();
+ }
+
+ // If previous QnAId is present, replace the dialog
+ Integer previousQnAId = ObjectPath.getPathValue(stepContext.getActiveDialog().getState(), PREVIOUS_QNA_ID,
+ Integer.class, 0);
+ if (previousQnAId > 0) {
+ // restart the waterfall to step 0
+ return this.runStep(stepContext, 0, DialogReason.BEGIN_CALLED, null);
+ }
+
+ // If response is present then show that response, else default answer.
+ List response = (List) stepContext.getResult();
+ if (response != null && response.size() > 0) {
+ stepContext.getContext().sendActivity(response.get(0).getAnswer()).join();
+ } else {
+ Activity activity = dialogOptions.getResponseOptions().getNoAnswer();
+ if (activity == null) {
+ stepContext.getContext().sendActivity(DEFAULT_NO_ANSWER).join();
+ } else {
+ stepContext.getContext().sendActivity(activity).join();
+ }
+ }
+
+ return stepContext.endDialog();
+ }
+
+ private static void resetOptions(DialogContext dc, QnAMakerDialogOptions dialogOptions) {
+ // Resetting context and QnAId
+ dialogOptions.getQnAMakerOptions().setQnAId(0);
+ dialogOptions.getQnAMakerOptions().setContext(new QnARequestContext());
+
+ // -Check if previous context is present, if yes then put it with the query
+ // -Check for id if query is present in reverse index.
+ Map previousContextData = ObjectPath.getPathValue(dc.getActiveDialog().getState(),
+ QNA_CONTEXT_DATA, Map.class);
+ Integer previousQnAId = ObjectPath.getPathValue(dc.getActiveDialog().getState(), PREVIOUS_QNA_ID,
+ Integer.class, 0);
+
+ if (previousQnAId > 0) {
+ dialogOptions.getQnAMakerOptions().setContext(new QnARequestContext() {
+ {
+ setPreviousQnAId(previousQnAId);
+ }
+ });
+
+ Integer currentQnAId = previousContextData.get(dc.getContext().getActivity().getText());
+ if (currentQnAId != null) {
+ dialogOptions.getQnAMakerOptions().setQnAId(currentQnAId);
+ }
+ }
+ }
+
+ private CompletableFuture callGenerateAnswer(WaterfallStepContext stepContext) {
+ // clear suggestedQuestions between turns.
+ stepContext.getState().removeValue("this.suggestedQuestions");
+
+ QnAMakerDialogOptions dialogOptions = ObjectPath.getPathValue(stepContext.getActiveDialog().getState(),
+ OPTIONS, QnAMakerDialogOptions.class);
+ QnAMakerDialog.resetOptions(stepContext, dialogOptions);
+
+ // Storing the context info
+ stepContext.getValues().put(ValueProperty.CURRENT_QUERY, stepContext.getContext().getActivity().getText());
+
+ // Calling QnAMaker to get response.
+ return this.getQnAMakerClient(stepContext).thenApply(qnaMakerClient -> {
+ QueryResults response = (QueryResults) stepContext.getState()
+ .get(String.format("turn.qnaresult%s", this.hashCode()));
+ if (response == null) {
+ response = qnaMakerClient.getAnswersRaw(stepContext.getContext(), dialogOptions.getQnAMakerOptions(),
+ null, null)
+ .join();
+ }
+
+ // Resetting previous query.
+ Integer previousQnAId = -1;
+ ObjectPath.setPathValue(stepContext.getActiveDialog().getState(), PREVIOUS_QNA_ID, previousQnAId);
+
+ // Take this value from GetAnswerResponse
+ Boolean isActiveLearningEnabled = response.getActiveLearningEnabled();
+
+ stepContext.getValues().put(ValueProperty.QNA_DATA, Arrays.asList(response.getAnswers()));
+
+ // Check if active learning is enabled.
+ // MaximumScoreForLowScoreVariation is the score above which no need to check
+ // for feedback.
+ if (response.getAnswers().length > 0 && response.getAnswers()[0]
+ .getScore() <= (ActiveLearningUtils.getMaximumScoreForLowScoreVariation() / PERCENTAGE_DIVISOR)) {
+ // Get filtered list of the response that support low score variation criteria.
+ response.setAnswers(qnaMakerClient.getLowScoreVariation(response.getAnswers()));
+
+ if (response.getAnswers().length > 1 && isActiveLearningEnabled) {
+ List suggestedQuestions = new ArrayList();
+ for (QueryResult qna : response.getAnswers()) {
+ suggestedQuestions.add(qna.getQuestions()[0]);
+ }
+
+ // Get active learning suggestion card activity.
+ Activity message = QnACardBuilder.getSuggestionsCard(suggestedQuestions,
+ dialogOptions.getResponseOptions().getActiveLearningCardTitle(),
+ dialogOptions.getResponseOptions().getCardNoMatchText());
+ stepContext.getContext().sendActivity(message).join();
+
+ ObjectPath.setPathValue(stepContext.getActiveDialog().getState(), OPTIONS, dialogOptions);
+ stepContext.getState().setValue("this.suggestedQuestions", suggestedQuestions);
+ return new DialogTurnResult(DialogTurnStatus.WAITING);
+ }
+ }
+
+ List result = new ArrayList();
+ if (!(response.getAnswers().length == 0)) {
+ result.add(response.getAnswers()[0]);
+ }
+
+ stepContext.getValues().put(ValueProperty.QNA_DATA, result);
+ ObjectPath.setPathValue(stepContext.getActiveDialog().getState(), OPTIONS, dialogOptions);
+
+ // If card is not shown, move to next step with top QnA response.
+ return stepContext.next(result).join();
+ });
+ }
+
+ private CompletableFuture callTrain(WaterfallStepContext stepContext) {
+ QnAMakerDialogOptions dialogOptions = ObjectPath.getPathValue(stepContext.getActiveDialog().getState(), OPTIONS,
+ QnAMakerDialogOptions.class);
+ List trainResponses = (List) stepContext.getValues().get(ValueProperty.QNA_DATA);
+ String currentQuery = (String) stepContext.getValues().get(ValueProperty.CURRENT_QUERY);
+
+ String reply = stepContext.getContext().getActivity().getText();
+
+ if (trainResponses.size() > 1) {
+ QueryResult qnaResult = trainResponses
+ .stream().filter(kvp -> kvp.getQuestions()[0].equals(reply)).findFirst().orElse(null);
+ if (qnaResult != null) {
+ List queryResultArr = new ArrayList();
+ stepContext.getValues().put(ValueProperty.QNA_DATA, queryResultArr.add(qnaResult));
+ FeedbackRecord record = new FeedbackRecord() {{
+ setUserId(stepContext.getContext().getActivity().getId());
+ setUserQuestion(currentQuery);
+ setQnaId(qnaResult.getId());
+ }};
+ FeedbackRecord[] records = {record};
+ FeedbackRecords feedbackRecords = new FeedbackRecords() {{
+ setRecords(records);
+ }};
+ // Call Active Learning Train API
+ return this.getQnAMakerClient(stepContext).thenCompose(qnaClient -> {
+ try {
+ return qnaClient.callTrain(feedbackRecords).thenCompose(task ->
+ stepContext.next(new ArrayList().add(qnaResult)));
+ } catch (IOException e) {
+ LoggerFactory.getLogger(QnAMakerDialog.class).error("callTrain");
+ }
+ return CompletableFuture.completedFuture(null);
+ });
+ } else if (reply.compareToIgnoreCase(dialogOptions.getResponseOptions().getCardNoMatchText()) == 0) {
+ Activity activity = dialogOptions.getResponseOptions().getCardNoMatchResponse();
+ if (activity == null) {
+ stepContext.getContext().sendActivity(DEFAULT_CARD_NO_MATCH_RESPONSE).join();
+ } else {
+ stepContext.getContext().sendActivity(activity).join();
+ }
+
+ return stepContext.endDialog();
+ } else {
+ // restart the waterfall to step 0
+ return runStep(stepContext, 0, DialogReason.BEGIN_CALLED, null);
+ }
+ }
+
+ return stepContext.next(stepContext.getResult());
+ }
+
+ private CompletableFuture checkForMultiTurnPrompt(WaterfallStepContext stepContext) {
+ QnAMakerDialogOptions dialogOptions = ObjectPath.getPathValue(stepContext.getActiveDialog().getState(),
+ OPTIONS, QnAMakerDialogOptions.class);
+ List response = (List) stepContext.getResult();
+ if (response != null && response.size() > 0) {
+ // -Check if context is present and prompt exists
+ // -If yes: Add reverse index of prompt display name and its corresponding QnA
+ // ID
+ // -Set PreviousQnAId as answer.Id
+ // -Display card for the prompt
+ // -Wait for the reply
+ // -If no: Skip to next step
+
+ QueryResult answer = response.get(0);
+
+ if (answer.getContext() != null && answer.getContext().getPrompts().length > 0) {
+ Map previousContextData = ObjectPath.getPathValue(
+ stepContext.getActiveDialog().getState(), QNA_CONTEXT_DATA, Map.class);
+
+ for (QnAMakerPrompt prompt : answer.getContext().getPrompts()) {
+ previousContextData.put(prompt.getDisplayText(), prompt.getQnaId());
+ }
+
+ ObjectPath.setPathValue(stepContext.getActiveDialog().getState(),
+ QNA_CONTEXT_DATA, previousContextData);
+ ObjectPath.setPathValue(stepContext.getActiveDialog().getState(), PREVIOUS_QNA_ID, answer.getId());
+ ObjectPath.setPathValue(stepContext.getActiveDialog().getState(), OPTIONS, dialogOptions);
+
+ // Get multi-turn prompts card activity.
+ Activity message = QnACardBuilder.getQnAPromptsCard(answer,
+ dialogOptions.getResponseOptions().getCardNoMatchText());
+ stepContext.getContext().sendActivity(message).join();
+
+ return CompletableFuture.completedFuture(new DialogTurnResult(DialogTurnStatus.WAITING));
+ }
+ }
+
+ return stepContext.next(stepContext.getResult());
+ }
+
+ /**
+ * Helper class.
+ */
+ final class ValueProperty {
+ public static final String CURRENT_QUERY = "currentQuery";
+ public static final String QNA_DATA = "qnaData";
+
+ private ValueProperty() { }
+ }
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/dialogs/QnAMakerDialogOptions.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/dialogs/QnAMakerDialogOptions.java
new file mode 100644
index 000000000..75122e83d
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/dialogs/QnAMakerDialogOptions.java
@@ -0,0 +1,52 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna.dialogs;
+
+import com.microsoft.bot.ai.qna.QnADialogResponseOptions;
+import com.microsoft.bot.ai.qna.QnAMakerOptions;
+
+/**
+ * Defines Dialog Options for QnAMakerDialog.
+ */
+public class QnAMakerDialogOptions {
+ private QnAMakerOptions qnaMakerOptions;
+
+ private QnADialogResponseOptions responseOptions;
+
+ /**
+ * Gets the options for the QnAMaker service.
+ *
+ * @return The options for the QnAMaker service.
+ */
+ public QnAMakerOptions getQnAMakerOptions() {
+ return this.qnaMakerOptions;
+ }
+
+ /**
+ * Sets the options for the QnAMaker service.
+ *
+ * @param withQnAMakerOptions The options for the QnAMaker service.
+ */
+ public void setQnAMakerOptions(QnAMakerOptions withQnAMakerOptions) {
+ this.qnaMakerOptions = withQnAMakerOptions;
+ }
+
+ /**
+ * Gets the response options for the QnAMakerDialog.
+ *
+ * @return The response options for the QnAMakerDialog.
+ */
+ public QnADialogResponseOptions getResponseOptions() {
+ return this.responseOptions;
+ }
+
+ /**
+ * Sets the response options for the QnAMakerDialog.
+ *
+ * @param withResponseOptions The response options for the QnAMakerDialog.
+ */
+ public void setResponseOptions(QnADialogResponseOptions withResponseOptions) {
+ this.responseOptions = withResponseOptions;
+ }
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/dialogs/QnAMakerPrompt.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/dialogs/QnAMakerPrompt.java
new file mode 100644
index 000000000..eeb18295b
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/dialogs/QnAMakerPrompt.java
@@ -0,0 +1,99 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna.dialogs;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Prompt Object.
+ */
+public class QnAMakerPrompt {
+ private static final Integer DEFAULT_DISPLAY_ORDER = 0;
+
+ @JsonProperty("displayOrder")
+ private Integer displayOrder = QnAMakerPrompt.DEFAULT_DISPLAY_ORDER;
+
+ @JsonProperty("qnaId")
+ private Integer qnaId;
+
+ @JsonProperty("displayText")
+ private String displayText = new String();
+
+ @JsonProperty("qna")
+ private Object qna;
+
+ /**
+ * Gets displayOrder - index of the prompt - used in ordering of the prompts.
+ *
+ * @return Display order.
+ */
+ public Integer getDisplayOrder() {
+ return this.displayOrder;
+ }
+
+ /**
+ * Sets displayOrder - index of the prompt - used in ordering of the prompts.
+ *
+ * @param withDisplayOrder Display order.
+ */
+ public void setDisplayOrder(Integer withDisplayOrder) {
+ this.displayOrder = withDisplayOrder;
+ }
+
+ /**
+ * Gets qna id corresponding to the prompt - if QnaId is present, QnADTO object
+ * is ignored.
+ *
+ * @return QnA Id.
+ */
+ public Integer getQnaId() {
+ return this.qnaId;
+ }
+
+ /**
+ * Sets qna id corresponding to the prompt - if QnaId is present, QnADTO object
+ * is ignored.
+ *
+ * @param withQnaId QnA Id.
+ */
+ public void setQnaId(Integer withQnaId) {
+ this.qnaId = withQnaId;
+ }
+
+ /**
+ * Gets displayText - Text displayed to represent a follow up question prompt.
+ *
+ * @return Display test.
+ */
+ public String getDisplayText() {
+ return this.displayText;
+ }
+
+ /**
+ * Sets displayText - Text displayed to represent a follow up question prompt.
+ *
+ * @param withDisplayText Display test.
+ */
+ public void setDisplayText(String withDisplayText) {
+ this.displayText = withDisplayText;
+ }
+
+ /**
+ * Gets the QnADTO returned from the API.
+ *
+ * @return The QnA DTO.
+ */
+ public Object getQna() {
+ return this.qna;
+ }
+
+ /**
+ * Sets the QnADTO returned from the API.
+ *
+ * @param withQna The QnA DTO.
+ */
+ public void setQna(Object withQna) {
+ this.qna = withQna;
+ }
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/dialogs/package-info.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/dialogs/package-info.java
new file mode 100644
index 000000000..73805a3b0
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/dialogs/package-info.java
@@ -0,0 +1,8 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for
+// license information.
+
+/**
+ * This package contains the classes for Bot-Builder.
+ */
+package com.microsoft.bot.ai.qna.dialogs;
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/FeedbackRecord.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/FeedbackRecord.java
new file mode 100644
index 000000000..c58ef5f0e
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/FeedbackRecord.java
@@ -0,0 +1,74 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna.models;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Active learning feedback record.
+ */
+public class FeedbackRecord {
+ @JsonProperty("userId")
+ private String userId;
+
+ @JsonProperty("userQuestion")
+ private String userQuestion;
+
+ @JsonProperty("qnaId")
+ private Integer qnaId;
+
+ /**
+ * Gets the feedback recod's user ID.
+ *
+ * @return The user ID.
+ */
+ public String getUserId() {
+ return this.userId;
+ }
+
+ /**
+ * Sets the feedback recod's user ID.
+ *
+ * @param withUserId The user ID.
+ */
+ public void setUserId(String withUserId) {
+ this.userId = withUserId;
+ }
+
+ /**
+ * Gets the question asked by the user.
+ *
+ * @return The user question.
+ */
+ public String getUserQuestion() {
+ return this.userQuestion;
+ }
+
+ /**
+ * Sets question asked by the user.
+ *
+ * @param withUserQuestion The user question.
+ */
+ public void setUserQuestion(String withUserQuestion) {
+ this.userQuestion = withUserQuestion;
+ }
+
+ /**
+ * Gets the QnA ID.
+ *
+ * @return The QnA ID.
+ */
+ public Integer getQnaId() {
+ return this.qnaId;
+ }
+
+ /**
+ * Sets the QnA ID.
+ *
+ * @param withQnaId The QnA ID.
+ */
+ public void setQnaId(Integer withQnaId) {
+ this.qnaId = withQnaId;
+ }
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/FeedbackRecords.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/FeedbackRecords.java
new file mode 100644
index 000000000..94bc084d4
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/FeedbackRecords.java
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna.models;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Active learning feedback records.
+ */
+public class FeedbackRecords {
+ @JsonProperty("feedbackRecords")
+ private FeedbackRecord[] records;
+
+ /**
+ * Gets the list of feedback records.
+ *
+ * @return List of {@link FeedbackRecord}.
+ */
+ public FeedbackRecord[] getRecords() {
+ return this.records;
+ }
+
+ /**
+ * Sets the list of feedback records.
+ *
+ * @param withRecords List of {@link FeedbackRecord}.
+ */
+ public void setRecords(FeedbackRecord[] withRecords) {
+ this.records = withRecords;
+ }
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/Metadata.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/Metadata.java
new file mode 100644
index 000000000..62f02b160
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/Metadata.java
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna.models;
+
+import java.io.Serializable;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Represents the Metadata object sent as part of QnA Maker requests.
+ */
+public class Metadata implements Serializable {
+ @JsonProperty("name")
+ private String name;
+
+ @JsonProperty("value")
+ private String value;
+
+ /**
+ * Gets the name for the Metadata property.
+ *
+ * @return A string.
+ */
+ public String getName() {
+ return this.name;
+ }
+
+ /**
+ * Sets the name for the Metadata property.
+ *
+ * @param withName A string.
+ */
+ public void setName(String withName) {
+ this.name = withName;
+ }
+
+ /**
+ * Gets the value for the Metadata property.
+ *
+ * @return A string.
+ */
+ public String getValue() {
+ return this.value;
+ }
+
+ /**
+ * Sets the value for the Metadata property.
+ *
+ * @param withValue A string.
+ */
+ public void setValue(String withValue) {
+ this.value = withValue;
+ }
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QnAMakerTraceInfo.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QnAMakerTraceInfo.java
new file mode 100644
index 000000000..712cbcd38
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QnAMakerTraceInfo.java
@@ -0,0 +1,256 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna.models;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.microsoft.bot.schema.Activity;
+
+/**
+ * This class represents all the trace info that we collect from the QnAMaker
+ * Middleware.
+ */
+public class QnAMakerTraceInfo {
+ @JsonProperty("message")
+ private Activity message;
+
+ @JsonProperty("queryResults")
+ private QueryResult[] queryResults;
+
+ @JsonProperty("knowledgeBaseId")
+ private String knowledgeBaseId;
+
+ @JsonProperty("scoreThreshold")
+ private Float scoreThreshold;
+
+ @JsonProperty("top")
+ private Integer top;
+
+ @JsonProperty("strictFilters")
+ private Metadata[] strictFilters;
+
+ @JsonProperty("context")
+ private QnARequestContext context;
+
+ @JsonProperty("qnaId")
+ private Integer qnaId;
+
+ @JsonProperty("isTest")
+ private Boolean isTest;
+
+ @JsonProperty("rankerType")
+ private String rankerType;
+
+ @Deprecated
+ @JsonIgnore
+ private Metadata[] metadataBoost;
+
+ /**
+ * Gets message which instigated the query to QnAMaker.
+ *
+ * @return Message which instigated the query to QnAMaker.
+ */
+ public Activity getMessage() {
+ return this.message;
+ }
+
+ /**
+ * Sets message which instigated the query to QnAMaker.
+ *
+ * @param withMessage Message which instigated the query to QnAMaker.
+ */
+ public void setMessage(Activity withMessage) {
+ this.message = withMessage;
+ }
+
+ /**
+ * Gets results that QnAMaker returned.
+ *
+ * @return Results that QnAMaker returned.
+ */
+ public QueryResult[] getQueryResults() {
+ return this.queryResults;
+ }
+
+ /**
+ * Sets results that QnAMaker returned.
+ *
+ * @param withQueryResult Results that QnAMaker returned.
+ */
+ public void setQueryResults(QueryResult[] withQueryResult) {
+ this.queryResults = withQueryResult;
+ }
+
+ /**
+ * Gets iD of the Knowledgebase that is being used.
+ *
+ * @return ID of the Knowledgebase that is being used.
+ */
+ public String getKnowledgeBaseId() {
+ return this.knowledgeBaseId;
+ }
+
+ /**
+ * Sets iD of the Knowledgebase that is being used.
+ *
+ * @param withKnowledgeBaseId ID of the Knowledgebase that is being used.
+ */
+ public void setKnowledgeBaseId(String withKnowledgeBaseId) {
+ this.knowledgeBaseId = withKnowledgeBaseId;
+ }
+
+ /**
+ * Gets the minimum score threshold, used to filter returned results. Scores are
+ * normalized to the range of 0.0 to 1.0 before filtering.
+ *
+ * @return The minimum score threshold, used to filter returned results.
+ */
+ public Float getScoreThreshold() {
+ return this.scoreThreshold;
+ }
+
+ /**
+ * Sets the minimum score threshold, used to filter returned results. Scores are
+ * normalized to the range of 0.0 to 1.0 before filtering
+ *
+ * @param withScoreThreshold The minimum score threshold, used to filter
+ * returned results.
+ */
+ public void setScoreThreshold(Float withScoreThreshold) {
+ this.scoreThreshold = withScoreThreshold;
+ }
+
+ /**
+ * Gets number of ranked results that are asked to be returned.
+ *
+ * @return Number of ranked results that are asked to be returned.
+ */
+ public Integer getTop() {
+ return this.top;
+ }
+
+ /**
+ * Sets number of ranked results that are asked to be returned.
+ *
+ * @param withTop Number of ranked results that are asked to be returned.
+ */
+ public void setTop(Integer withTop) {
+ this.top = withTop;
+ }
+
+ /**
+ * Gets the filters used to return answers that have the specified metadata.
+ *
+ * @return The filters used to return answers that have the specified metadata.
+ */
+ public Metadata[] getStrictFilters() {
+ return this.strictFilters;
+ }
+
+ /**
+ * Sets the filters used to return answers that have the specified metadata.
+ *
+ * @param withStrictFilters The filters used to return answers that have the
+ * specified metadata.
+ */
+ public void setStrictFilters(Metadata[] withStrictFilters) {
+ this.strictFilters = withStrictFilters;
+ }
+
+ /**
+ * Gets context for multi-turn responses.
+ *
+ * @return The context from which the QnA was extracted.
+ */
+ public QnARequestContext getContext() {
+ return this.context;
+ }
+
+ /**
+ * Sets context for multi-turn responses.
+ *
+ * @param withContext The context from which the QnA was extracted.
+ */
+ public void setContext(QnARequestContext withContext) {
+ this.context = withContext;
+ }
+
+ /**
+ * Gets QnA Id of the current question asked.
+ *
+ * @return Id of the current question asked.
+ */
+ public Integer getQnAId() {
+ return this.qnaId;
+ }
+
+ /**
+ * Sets QnA Id of the current question asked.
+ *
+ * @param withQnAId Id of the current question asked.
+ */
+ public void setQnAId(Integer withQnAId) {
+ this.qnaId = withQnAId;
+ }
+
+ /**
+ * Gets a value indicating whether gets or sets environment of knowledgebase to
+ * be called.
+ *
+ * @return A value indicating whether to call test or prod environment of
+ * knowledgebase.
+ */
+ public Boolean getIsTest() {
+ return this.isTest;
+ }
+
+ /**
+ * Sets a value indicating whether gets or sets environment of knowledgebase to
+ * be called.
+ *
+ * @param withIsTest A value indicating whether to call test or prod environment
+ * of knowledgebase.
+ */
+ public void setIsTest(Boolean withIsTest) {
+ this.isTest = withIsTest;
+ }
+
+ /**
+ * Gets ranker Types.
+ *
+ * @return Ranker Types.
+ */
+ public String getRankerType() {
+ return this.rankerType;
+ }
+
+ /**
+ * Sets ranker Types.
+ *
+ * @param withRankerType Ranker Types.
+ */
+ public void setRankerType(String withRankerType) {
+ this.rankerType = withRankerType;
+ }
+
+ /**
+ * Gets the {@link Metadata} collection to be sent when calling QnA Maker to
+ * boost results.
+ *
+ * @return An array of {@link Metadata}.
+ */
+ public Metadata[] getMetadataBoost() {
+ return this.metadataBoost;
+ }
+
+ /**
+ * Sets the {@link Metadata} collection to be sent when calling QnA Maker to
+ * boost results.
+ *
+ * @param withMetadataBoost An array of {@link Metadata}.
+ */
+ public void setMetadataBoost(Metadata[] withMetadataBoost) {
+ this.metadataBoost = withMetadataBoost;
+ }
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QnARequestContext.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QnARequestContext.java
new file mode 100644
index 000000000..f5134d6ca
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QnARequestContext.java
@@ -0,0 +1,54 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna.models;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The context associated with QnA. Used to mark if the current prompt is
+ * relevant with a previous question or not.
+ */
+public class QnARequestContext {
+ @JsonProperty("previousQnAId")
+ private Integer previousQnAId;
+
+ @JsonProperty("previousUserQuery")
+ private String previousUserQuery = new String();
+
+ /**
+ * Gets the previous QnA Id that was returned.
+ *
+ * @return The previous QnA Id.
+ */
+ public Integer getPreviousQnAId() {
+ return this.previousQnAId;
+ }
+
+ /**
+ * Sets the previous QnA Id that was returned.
+ *
+ * @param withPreviousQnAId The previous QnA Id.
+ */
+ public void setPreviousQnAId(Integer withPreviousQnAId) {
+ this.previousQnAId = withPreviousQnAId;
+ }
+
+ /**
+ * Gets the previous user query/question.
+ *
+ * @return The previous user query.
+ */
+ public String getPreviousUserQuery() {
+ return this.previousUserQuery;
+ }
+
+ /**
+ * Sets the previous user query/question.
+ *
+ * @param withPreviousUserQuery The previous user query.
+ */
+ public void setPreviousUserQuery(String withPreviousUserQuery) {
+ this.previousUserQuery = withPreviousUserQuery;
+ }
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QnAResponseContext.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QnAResponseContext.java
new file mode 100644
index 000000000..53971d832
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QnAResponseContext.java
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna.models;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.microsoft.bot.ai.qna.dialogs.QnAMakerPrompt;
+
+/**
+ * The context associated with QnA. Used to mark if the qna response has related
+ * prompts to display.
+ */
+public class QnAResponseContext {
+ @JsonProperty("prompts")
+ private QnAMakerPrompt[] prompts;
+
+ /**
+ * Gets the prompts collection of related prompts.
+ *
+ * @return The QnA prompts array.
+ */
+ public QnAMakerPrompt[] getPrompts() {
+ return this.prompts;
+ }
+
+ /**
+ * Sets the prompts collection of related prompts.
+ *
+ * @param withPrompts The QnA prompts array.
+ */
+ public void setPrompts(QnAMakerPrompt[] withPrompts) {
+ this.prompts = withPrompts;
+ }
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QueryResult.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QueryResult.java
new file mode 100644
index 000000000..4090e4741
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QueryResult.java
@@ -0,0 +1,168 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna.models;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Represents an individual result from a knowledge base query.
+ */
+public class QueryResult {
+ @JsonProperty("questions")
+ private String[] questions;
+
+ @JsonProperty("answer")
+ private String answer;
+
+ @JsonProperty("score")
+ private Float score;
+
+ @JsonProperty("metadata")
+ private Metadata[] metadata;
+
+ @JsonProperty("source")
+ private String source;
+
+ @JsonProperty("id")
+ private Integer id;
+
+ @JsonProperty("context")
+ private QnAResponseContext context;
+
+ /**
+ * Gets the list of questions indexed in the QnA Service for the given answer.
+ *
+ * @return The list of questions indexed in the QnA Service for the given
+ * answer.
+ */
+ public String[] getQuestions() {
+ return this.questions;
+ }
+
+ /**
+ * Sets the list of questions indexed in the QnA Service for the given answer.
+ *
+ * @param withQuestions The list of questions indexed in the QnA Service for the
+ * given answer.
+ */
+ public void setQuestions(String[] withQuestions) {
+ this.questions = withQuestions;
+ }
+
+ /**
+ * Gets the answer text.
+ *
+ * @return The answer text.
+ */
+ public String getAnswer() {
+ return this.answer;
+ }
+
+ /**
+ * Sets the answer text.
+ *
+ * @param withAnswer The answer text.
+ */
+ public void setAnswer(String withAnswer) {
+ this.answer = withAnswer;
+ }
+
+ /**
+ * Gets the answer's score, from 0.0 (least confidence) to 1.0 (greatest
+ * confidence).
+ *
+ * @return The answer's score, from 0.0 (least confidence) to 1.0 (greatest
+ * confidence).
+ */
+ public Float getScore() {
+ return this.score;
+ }
+
+ /**
+ * Sets the answer's score, from 0.0 (least confidence) to 1.0 (greatest
+ * confidence).
+ *
+ * @param withScore The answer's score, from 0.0 (least confidence) to 1.0
+ * (greatest confidence).
+ */
+ public void setScore(Float withScore) {
+ this.score = withScore;
+ }
+
+ /**
+ * Gets metadata that is associated with the answer.
+ *
+ * @return Metadata that is associated with the answer.
+ */
+ public Metadata[] getMetadata() {
+ return this.metadata;
+ }
+
+ /**
+ * Sets metadata that is associated with the answer.
+ *
+ * @param withMetadata Metadata that is associated with the answer.
+ */
+ public void setMetadata(Metadata[] withMetadata) {
+ this.metadata = withMetadata;
+ }
+
+ /**
+ * Gets the source from which the QnA was extracted.
+ *
+ * @return The source from which the QnA was extracted.
+ */
+ public String getSource() {
+ return this.source;
+ }
+
+ /**
+ * Sets the source from which the QnA was extracted.
+ *
+ * @param withSource The source from which the QnA was extracted.
+ */
+ public void setSource(String withSource) {
+ this.source = withSource;
+ }
+
+ /**
+ * Gets the index of the answer in the knowledge base. V3 uses 'qnaId', V4 uses
+ * 'id'.
+ *
+ * @return The index of the answer in the knowledge base. V3 uses 'qnaId', V4
+ * uses 'id'.
+ */
+ public Integer getId() {
+ return this.id;
+ }
+
+ /**
+ * Sets the index of the answer in the knowledge base. V3 uses 'qnaId', V4 uses
+ * 'id'.
+ *
+ * @param withId The index of the answer in the knowledge base. V3 uses 'qnaId',
+ * V4 uses 'id'.
+ */
+ public void setId(Integer withId) {
+ this.id = withId;
+ }
+
+ /**
+ * Gets context for multi-turn responses.
+ *
+ * @return The context from which the QnA was extracted.
+ */
+ public QnAResponseContext getContext() {
+ return this.context;
+ }
+
+ /**
+ * Sets context for multi-turn responses.
+ *
+ * @param withContext The context from which the QnA was extracted.
+ */
+ public void setContext(QnAResponseContext withContext) {
+ this.context = withContext;
+ }
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QueryResults.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QueryResults.java
new file mode 100644
index 000000000..84e26c733
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/QueryResults.java
@@ -0,0 +1,59 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna.models;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Contains answers for a user query.
+ */
+public class QueryResults {
+ @JsonProperty("answers")
+ private QueryResult[] answers;
+
+ @JsonProperty("activeLearningEnabled")
+ private Boolean activeLearningEnabled;
+
+ /**
+ * Gets the answers for a user query, sorted in decreasing order of ranking
+ * score.
+ *
+ * @return The answers for a user query, sorted in decreasing order of ranking
+ * score.
+ */
+ public QueryResult[] getAnswers() {
+ return this.answers;
+ }
+
+ /**
+ * Sets the answers for a user query, sorted in decreasing order of ranking
+ * score.
+ *
+ * @param withAnswers The answers for a user query, sorted in decreasing order
+ * of ranking score.
+ */
+ public void setAnswers(QueryResult[] withAnswers) {
+ this.answers = withAnswers;
+ }
+
+ /**
+ * Gets a value indicating whether gets or set for the active learning enable
+ * flag.
+ *
+ * @return The active learning enable flag.
+ */
+ public Boolean getActiveLearningEnabled() {
+ return this.activeLearningEnabled;
+ }
+
+ /**
+ * Sets a value indicating whether gets or set for the active learning enable
+ * flag.
+ *
+ * @param withActiveLearningEnabled The active learning enable flag.
+ */
+ public void setActiveLearningEnabled(Boolean withActiveLearningEnabled) {
+ this.activeLearningEnabled = withActiveLearningEnabled;
+ }
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/RankerTypes.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/RankerTypes.java
new file mode 100644
index 000000000..0cd10e87e
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/RankerTypes.java
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna.models;
+
+/**
+ * Enumeration of types of ranking.
+ */
+public final class RankerTypes {
+ /**
+ * Default Ranker Behaviour. i.e. Ranking based on Questions and Answer.
+ */
+ public static final String DEFAULT_RANKER_TYPE = "Default";
+
+ /**
+ * Ranker based on question Only.
+ */
+ public static final String QUESTION_ONLY = "QuestionOnly";
+
+ /**
+ * Ranker based on Autosuggest for question field Only.
+ */
+ public static final String AUTO_SUGGEST_QUESTION = "AutoSuggestQuestion";
+
+ private RankerTypes() { }
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/package-info.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/package-info.java
new file mode 100644
index 000000000..cc5be8f6c
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/models/package-info.java
@@ -0,0 +1,8 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for
+// license information.
+
+/**
+ * This package contains the classes for Bot-Builder.
+ */
+package com.microsoft.bot.ai.qna.models;
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/package-info.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/package-info.java
new file mode 100644
index 000000000..6ee0d0731
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/package-info.java
@@ -0,0 +1,8 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for
+// license information.
+
+/**
+ * This package contains the classes for Bot-Builder.
+ */
+package com.microsoft.bot.ai.qna;
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/ActiveLearningUtils.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/ActiveLearningUtils.java
new file mode 100644
index 000000000..726f4cc6d
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/ActiveLearningUtils.java
@@ -0,0 +1,122 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna.utils;
+
+import com.microsoft.bot.ai.qna.models.QueryResult;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Active learning helper class.
+ */
+public final class ActiveLearningUtils {
+ /**
+ * Previous Low Score Variation Multiplier.ActiveLearningUtils.
+ */
+ private static final Float PREVIOUS_LOW_SCORE_VARIATION_MULTIPLIER = 0.7f;
+
+ /**
+ * Max Low Score Variation Multiplier.
+ */
+ private static final Float MAX_LOW_SCORE_VARIATION_MULTIPLIER = 1.0f;
+
+ private static final Integer PERCENTAGE_DIVISOR = 100;
+
+ private static final Float MAXIMUM_SCORE_VARIATION = 95.0F;
+
+ private static final Float MINIMUM_SCORE_VARIATION = 20.0F;
+
+ private static Float maximumScoreForLowScoreVariation = MAXIMUM_SCORE_VARIATION;
+
+ private static Float minimumScoreForLowScoreVariation = MINIMUM_SCORE_VARIATION;
+
+ private ActiveLearningUtils() { }
+
+ /**
+ * Gets maximum Score For Low Score Variation.
+ *
+ * @return Maximum Score For Low Score Variation.
+ */
+ public static Float getMaximumScoreForLowScoreVariation() {
+ return ActiveLearningUtils.maximumScoreForLowScoreVariation;
+ }
+
+ /**
+ * Sets maximum Score For Low Score Variation.
+ *
+ * @param withMaximumScoreForLowScoreVariation Maximum Score For Low Score
+ * Variation.
+ */
+ public static void setMaximumScoreForLowScoreVariation(Float withMaximumScoreForLowScoreVariation) {
+ ActiveLearningUtils.maximumScoreForLowScoreVariation = withMaximumScoreForLowScoreVariation;
+ }
+
+ /**
+ * Gets minimum Score For Low Score Variation.
+ *
+ * @return Minimum Score For Low Score Variation.
+ */
+ public static Float getMinimumScoreForLowScoreVariation() {
+ return ActiveLearningUtils.minimumScoreForLowScoreVariation;
+ }
+
+ /**
+ * Sets minimum Score For Low Score Variation.
+ *
+ * @param withMinimumScoreForLowScoreVariation Minimum Score For Low Score
+ * Variation.
+ */
+ public static void setMinimumScoreForLowScoreVariation(Float withMinimumScoreForLowScoreVariation) {
+ ActiveLearningUtils.minimumScoreForLowScoreVariation = withMinimumScoreForLowScoreVariation;
+ }
+
+ /**
+ * Returns list of qnaSearch results which have low score variation.
+ *
+ * @param qnaSearchResults List of QnaSearch results.
+ * @return List of filtered qnaSearch results.
+ */
+ public static List getLowScoreVariation(List qnaSearchResults) {
+ List filteredQnaSearchResult = new ArrayList();
+
+ if (qnaSearchResults == null || qnaSearchResults.isEmpty()) {
+ return filteredQnaSearchResult;
+ }
+
+ if (qnaSearchResults.size() == 1) {
+ return qnaSearchResults;
+ }
+
+ Float topAnswerScore = qnaSearchResults.get(0).getScore() * PERCENTAGE_DIVISOR;
+ if (topAnswerScore > ActiveLearningUtils.maximumScoreForLowScoreVariation) {
+ filteredQnaSearchResult.add(qnaSearchResults.get(0));
+ return filteredQnaSearchResult;
+ }
+
+ Float prevScore = topAnswerScore;
+
+ if (topAnswerScore > ActiveLearningUtils.minimumScoreForLowScoreVariation) {
+ filteredQnaSearchResult.add(qnaSearchResults.get(0));
+
+ for (int i = 1; i < qnaSearchResults.size(); i++) {
+ if (ActiveLearningUtils
+ .includeForClustering(prevScore, qnaSearchResults.get(i).getScore() * PERCENTAGE_DIVISOR,
+ ActiveLearningUtils.PREVIOUS_LOW_SCORE_VARIATION_MULTIPLIER)
+ && ActiveLearningUtils.includeForClustering(topAnswerScore,
+ qnaSearchResults.get(i).getScore() * PERCENTAGE_DIVISOR,
+ ActiveLearningUtils.MAX_LOW_SCORE_VARIATION_MULTIPLIER)) {
+ prevScore = qnaSearchResults.get(i).getScore() * PERCENTAGE_DIVISOR;
+ filteredQnaSearchResult.add(qnaSearchResults.get(i));
+ }
+ }
+ }
+
+ return filteredQnaSearchResult;
+ }
+
+ private static Boolean includeForClustering(Float prevScore, Float currentScore, Float multiplier) {
+ return (prevScore - currentScore) < (multiplier * Math.sqrt(prevScore));
+ }
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/BindToActivity.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/BindToActivity.java
new file mode 100644
index 000000000..9126f0064
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/BindToActivity.java
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna.utils;
+
+import com.microsoft.bot.dialogs.DialogContext;
+import com.microsoft.bot.schema.Activity;
+
+import java.util.concurrent.CompletableFuture;
+
+import javax.annotation.Nullable;
+
+/**
+ * Class to bind activities.
+ */
+public class BindToActivity {
+ private Activity activity;
+
+ /**
+ * Construct to bind an Activity.
+ * @param withActivity activity to bind.
+ */
+ public BindToActivity(Activity withActivity) {
+ this.activity = withActivity;
+ }
+
+ /**
+ *
+ * @param context The context.
+ * @param data The data.
+ * @return The activity.
+ */
+ public CompletableFuture bind(DialogContext context, @Nullable Object data) {
+ return CompletableFuture.completedFuture(this.activity);
+ }
+
+ /**
+ * Get the activity text.
+ * @return The activity text.
+ */
+ public String toString() {
+ return String.format("%s", this.activity.getText());
+ }
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/GenerateAnswerUtils.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/GenerateAnswerUtils.java
new file mode 100644
index 000000000..05178903b
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/GenerateAnswerUtils.java
@@ -0,0 +1,274 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna.utils;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.microsoft.bot.ai.qna.QnAMaker;
+import com.microsoft.bot.ai.qna.QnAMakerEndpoint;
+import com.microsoft.bot.ai.qna.QnAMakerOptions;
+import com.microsoft.bot.ai.qna.models.Metadata;
+import com.microsoft.bot.ai.qna.models.QnAMakerTraceInfo;
+import com.microsoft.bot.ai.qna.models.QueryResult;
+import com.microsoft.bot.ai.qna.models.QueryResults;
+import com.microsoft.bot.ai.qna.models.RankerTypes;
+import com.microsoft.bot.builder.TurnContext;
+import com.microsoft.bot.restclient.serializer.JacksonAdapter;
+import com.microsoft.bot.schema.Activity;
+
+import net.minidev.json.JSONObject;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Helper class for Generate Answer API.
+ */
+public class GenerateAnswerUtils {
+ private QnAMakerEndpoint endpoint;
+ private QnAMakerOptions options;
+
+ private static final Integer PERCENTAGE_DIVISOR = 100;
+ private static final Float SCORE_THRESHOLD = 0.3f;
+ private static final Double TIMEOUT = 100000d;
+
+ /**
+ * Initializes a new instance of the {@link GenerateAnswerUtils} class.
+ *
+ * @param withEndpoint QnA Maker endpoint details.
+ * @param withOptions QnA Maker options.
+ */
+ public GenerateAnswerUtils(QnAMakerEndpoint withEndpoint,
+ QnAMakerOptions withOptions) {
+ this.endpoint = withEndpoint;
+
+ this.options = withOptions != null ? withOptions : new QnAMakerOptions();
+ GenerateAnswerUtils.validateOptions(this.options);
+ }
+
+ /**
+ * Gets qnA Maker options.
+ *
+ * @return The options for QnAMaker.
+ */
+ public QnAMakerOptions getOptions() {
+ return this.options;
+ }
+
+ /**
+ * Sets qnA Maker options.
+ *
+ * @param withOptions The options for QnAMaker.
+ */
+ public void setOptions(QnAMakerOptions withOptions) {
+ this.options = withOptions;
+ }
+
+ /**
+ * Generates an answer from the knowledge base.
+ *
+ * @param turnContext The Turn Context that contains the user question to be
+ * queried against your knowledge base.
+ * @param messageActivity Message activity of the turn context.
+ * @param withOptions The options for the QnA Maker knowledge base. If null,
+ * constructor option is used for this instance.
+ * @return A list of answers for the user query, sorted in decreasing order of
+ * ranking score.
+ * @throws IOException IOException
+ */
+ @Deprecated
+ public CompletableFuture getAnswers(TurnContext turnContext, Activity messageActivity,
+ QnAMakerOptions withOptions) throws IOException {
+ return this.getAnswersRaw(turnContext, messageActivity, withOptions).thenApply(result -> result.getAnswers());
+ }
+
+ /**
+ * Generates an answer from the knowledge base.
+ *
+ * @param turnContext The Turn Context that contains the user question to be
+ * queried against your knowledge base.
+ * @param messageActivity Message activity of the turn context.
+ * @param withOptions The options for the QnA Maker knowledge base. If null,
+ * constructor option is used for this instance.
+ * @return A list of answers for the user query, sorted in decreasing order of
+ * ranking score.
+ */
+ public CompletableFuture getAnswersRaw(TurnContext turnContext, Activity messageActivity,
+ QnAMakerOptions withOptions) {
+ if (turnContext == null) {
+ throw new IllegalArgumentException("turnContext");
+ }
+
+ if (turnContext.getActivity() == null) {
+ throw new IllegalArgumentException(String.format("The %1$s property for %2$s can't be null: turnContext",
+ turnContext.getActivity(), "turnContext"));
+ }
+
+ if (messageActivity == null) {
+ throw new IllegalArgumentException("Activity type is not a message");
+ }
+
+ QnAMakerOptions hydratedOptions = this.hydrateOptions(withOptions);
+ GenerateAnswerUtils.validateOptions(hydratedOptions);
+
+ try {
+ return this.queryQnaService(messageActivity, hydratedOptions).thenCompose(result -> {
+ this.emitTraceInfo(turnContext, messageActivity, result.getAnswers(), hydratedOptions);
+ return CompletableFuture.completedFuture(result);
+ });
+ } catch (IOException e) {
+ LoggerFactory.getLogger(GenerateAnswerUtils.class).error("getAnswersRaw");
+ return CompletableFuture.completedFuture(null);
+ }
+ }
+
+ private static CompletableFuture formatQnAResult(JsonNode response, QnAMakerOptions options)
+ throws IOException {
+ String jsonResponse = null;
+ JacksonAdapter jacksonAdapter = new JacksonAdapter();
+ QueryResults results = null;
+
+ jsonResponse = response.toString();
+ results = jacksonAdapter.deserialize(jsonResponse, QueryResults.class);
+ for (QueryResult answer : results.getAnswers()) {
+ answer.setScore(answer.getScore() / PERCENTAGE_DIVISOR);
+ }
+ List answerList = Arrays.asList(results.getAnswers()).
+ stream().filter(answer -> answer.getScore() > options.getScoreThreshold()).collect(Collectors.toList());
+ results.setAnswers(answerList.toArray(new QueryResult[answerList.size()]));
+
+ return CompletableFuture.completedFuture(results);
+ }
+
+ private static void validateOptions(QnAMakerOptions options) {
+ if (options.getScoreThreshold() == 0) {
+ options.setScoreThreshold(SCORE_THRESHOLD);
+ }
+
+ if (options.getTop() == 0) {
+ options.setTop(1);
+ }
+
+ if (options.getScoreThreshold() < 0 || options.getScoreThreshold() > 1) {
+ throw new IllegalArgumentException(String
+ .format("options: The %s property should be a value between 0 and 1", options.getScoreThreshold()));
+ }
+
+ if (options.getTimeout() == 0.0d) {
+ options.setTimeout(TIMEOUT);
+ }
+
+ if (options.getTop() < 1) {
+ throw new IllegalArgumentException("options: The top property should be an integer greater than 0");
+ }
+
+ if (options.getStrictFilters() == null) {
+ options.setStrictFilters(new Metadata[0]);
+ }
+
+ if (options.getRankerType() == null) {
+ options.setRankerType(RankerTypes.DEFAULT_RANKER_TYPE);
+ }
+ }
+
+ /**
+ * Combines QnAMakerOptions passed into the QnAMaker constructor with the
+ * options passed as arguments into GetAnswersAsync().
+ *
+ * @param queryOptions The options for the QnA Maker knowledge base.
+ * @return Return modified options for the QnA Maker knowledge base.
+ */
+ private QnAMakerOptions hydrateOptions(QnAMakerOptions queryOptions) {
+ JacksonAdapter jacksonAdapter = new JacksonAdapter();
+ QnAMakerOptions hydratedOptions = null;
+
+ try {
+ hydratedOptions = jacksonAdapter.deserialize(jacksonAdapter.serialize(options),
+ QnAMakerOptions.class);
+ } catch (IOException e) {
+ LoggerFactory.getLogger(GenerateAnswerUtils.class).error("hydrateOptions");
+ }
+
+ if (queryOptions != null) {
+ if (queryOptions.getScoreThreshold() != hydratedOptions.getScoreThreshold()
+ && queryOptions.getScoreThreshold() != 0) {
+ hydratedOptions.setScoreThreshold(queryOptions.getScoreThreshold());
+ }
+
+ if (queryOptions.getTop() != hydratedOptions.getTop() && queryOptions.getTop() != 0) {
+ hydratedOptions.setTop(queryOptions.getTop());
+ }
+
+ if (queryOptions.getStrictFilters() != null && queryOptions.getStrictFilters().length > 0) {
+ hydratedOptions.setStrictFilters(queryOptions.getStrictFilters());
+ }
+
+ hydratedOptions.setContext(queryOptions.getContext());
+ hydratedOptions.setQnAId(queryOptions.getQnAId());
+ hydratedOptions.setIsTest(queryOptions.getIsTest());
+ hydratedOptions.setRankerType(queryOptions.getRankerType() != null ? queryOptions.getRankerType()
+ : RankerTypes.DEFAULT_RANKER_TYPE);
+ hydratedOptions.setStrictFiltersJoinOperator(queryOptions.getStrictFiltersJoinOperator());
+ }
+
+ return hydratedOptions;
+ }
+
+ private CompletableFuture queryQnaService(Activity messageActivity, QnAMakerOptions withOptions)
+ throws IOException {
+ String requestUrl = String.format("%1$s/knowledgebases/%2$s/generateanswer", this.endpoint.getHost(),
+ this.endpoint.getKnowledgeBaseId());
+ JacksonAdapter jacksonAdapter = new JacksonAdapter();
+ String jsonRequest = null;
+
+ jsonRequest = jacksonAdapter.serialize(new JSONObject() {
+ {
+ put("question", messageActivity.getText());
+ put("top", withOptions.getTop());
+ put("strictFilters", withOptions.getStrictFilters());
+ put("scoreThreshold", withOptions.getScoreThreshold());
+ put("context", withOptions.getContext());
+ put("qnaId", withOptions.getQnAId());
+ put("isTest", withOptions.getIsTest());
+ put("rankerType", withOptions.getRankerType());
+ put("StrictFiltersCompoundOperationType", withOptions.getStrictFiltersJoinOperator());
+ }
+ });
+
+ HttpRequestUtils httpRequestHelper = new HttpRequestUtils();
+ return httpRequestHelper.executeHttpRequest(requestUrl, jsonRequest, this.endpoint).thenCompose(response -> {
+ try {
+ return GenerateAnswerUtils.formatQnAResult(response, withOptions);
+ } catch (IOException e) {
+ LoggerFactory.getLogger(GenerateAnswerUtils.class).error("QueryQnAService", e);
+ return CompletableFuture.completedFuture(null);
+ }
+ });
+ }
+
+ private CompletableFuture emitTraceInfo(TurnContext turnContext, Activity messageActivity,
+ QueryResult[] result, QnAMakerOptions withOptions) {
+ String knowledgeBaseId = this.endpoint.getKnowledgeBaseId();
+ QnAMakerTraceInfo traceInfo = new QnAMakerTraceInfo() {
+ {
+ setMessage(messageActivity);
+ setQueryResults(result);
+ setKnowledgeBaseId(knowledgeBaseId);
+ setScoreThreshold(withOptions.getScoreThreshold());
+ setTop(withOptions.getTop());
+ setStrictFilters(withOptions.getStrictFilters());
+ setContext(withOptions.getContext());
+ setQnAId(withOptions.getQnAId());
+ setIsTest(withOptions.getIsTest());
+ setRankerType(withOptions.getRankerType());
+ }
+ };
+ Activity traceActivity = Activity.createTraceActivity(QnAMaker.QNA_MAKER_NAME, QnAMaker.QNA_MAKER_TRACE_TYPE,
+ traceInfo, QnAMaker.QNA_MAKER_TRACE_LABEL);
+ return turnContext.sendActivity(traceActivity).thenApply(response -> null);
+ }
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/HttpRequestUtils.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/HttpRequestUtils.java
new file mode 100644
index 000000000..dcfff3e59
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/HttpRequestUtils.java
@@ -0,0 +1,83 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna.utils;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.microsoft.bot.ai.qna.QnAMakerEndpoint;
+import com.microsoft.bot.connector.UserAgent;
+
+import okhttp3.HttpUrl;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Helper for HTTP requests.
+ */
+public class HttpRequestUtils {
+ private OkHttpClient httpClient = new OkHttpClient();
+ /**
+ * Execute Http request.
+ *
+ * @param requestUrl Http request url.
+ * @param payloadBody Http request body.
+ * @param endpoint QnA Maker endpoint details.
+ * @return Returns http response object.
+ */
+ public CompletableFuture executeHttpRequest(String requestUrl, String payloadBody,
+ QnAMakerEndpoint endpoint) {
+ if (requestUrl == null) {
+ throw new IllegalArgumentException("requestUrl: Request url can not be null.");
+ }
+
+ if (payloadBody == null) {
+ throw new IllegalArgumentException("payloadBody: Payload body can not be null.");
+ }
+
+ if (endpoint == null) {
+ throw new IllegalArgumentException("endpoint");
+ }
+
+ ObjectMapper mapper = new ObjectMapper();
+ String endpointKey = endpoint.getEndpointKey();
+ Response response;
+ JsonNode qnaResponse = null;
+ try {
+ Request request = buildRequest(requestUrl, endpointKey, buildRequestBody(payloadBody));
+ response = this.httpClient.newCall(request).execute();
+ qnaResponse = mapper.readTree(response.body().string());
+ if (!response.isSuccessful()) {
+ String message = "Unexpected code " + response.code();
+ throw new Exception(message);
+ }
+ } catch (Exception e) {
+ LoggerFactory.getLogger(HttpRequestUtils.class).error("findPackages", e);
+ throw new CompletionException(e);
+ }
+
+ return CompletableFuture.completedFuture(qnaResponse);
+ }
+
+ private Request buildRequest(String requestUrl, String endpointKey, RequestBody body) {
+ HttpUrl.Builder httpBuilder = HttpUrl.parse(requestUrl).newBuilder();
+ Request.Builder requestBuilder = new Request.Builder()
+ .url(httpBuilder.build())
+ .addHeader("Authorization", String.format("EndpointKey %s", endpointKey))
+ .addHeader("Ocp-Apim-Subscription-Key", endpointKey).addHeader("User-Agent", UserAgent.value())
+ .post(body);
+ return requestBuilder.build();
+ }
+
+ private RequestBody buildRequestBody(String payloadBody) throws JsonProcessingException {
+ return RequestBody.create(payloadBody, MediaType.parse("application/json; charset=utf-8"));
+ }
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/QnACardBuilder.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/QnACardBuilder.java
new file mode 100644
index 000000000..c2b7bb110
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/QnACardBuilder.java
@@ -0,0 +1,127 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna.utils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.microsoft.bot.ai.qna.dialogs.QnAMakerPrompt;
+import com.microsoft.bot.ai.qna.models.QueryResult;
+import com.microsoft.bot.schema.ActionTypes;
+import com.microsoft.bot.schema.Activity;
+import com.microsoft.bot.schema.Attachment;
+import com.microsoft.bot.schema.CardAction;
+import com.microsoft.bot.schema.HeroCard;
+
+/**
+ * Message activity card builder for QnAMaker dialogs.
+ */
+public final class QnACardBuilder {
+
+ private QnACardBuilder() { }
+
+ /**
+ * Get active learning suggestions card.
+ *
+ * @param suggestionsList List of suggested questions.
+ * @param cardTitle Title of the cards
+ * @param cardNoMatchText No match text.
+ * @return Activity.
+ */
+ public static Activity getSuggestionsCard(List suggestionsList, String cardTitle, String cardNoMatchText) {
+ if (suggestionsList == null) {
+ throw new IllegalArgumentException("suggestionsList");
+ }
+
+ if (cardTitle == null) {
+ throw new IllegalArgumentException("cardTitle");
+ }
+
+ if (cardNoMatchText == null) {
+ throw new IllegalArgumentException("cardNoMatchText");
+ }
+
+ Activity chatActivity = Activity.createMessageActivity();
+ chatActivity.setText(cardTitle);
+ List buttonList = new ArrayList();
+
+ // Add all suggestions
+ for (String suggestion : suggestionsList) {
+ buttonList.add(new CardAction() {
+ {
+ setValue(suggestion);
+ setType(ActionTypes.IM_BACK);
+ setTitle(suggestion);
+ }
+ });
+ }
+
+ // Add No match text
+ buttonList.add(new CardAction() {
+ {
+ setValue(cardNoMatchText);
+ setType(ActionTypes.IM_BACK);
+ setTitle(cardNoMatchText);
+ }
+ });
+
+ HeroCard plCard = new HeroCard() {
+ {
+ setButtons(buttonList);
+ }
+ };
+
+ // Create the attachment.
+ Attachment attachment = plCard.toAttachment();
+
+ chatActivity.setAttachment(attachment);
+
+ return chatActivity;
+ }
+
+ /**
+ * Get active learning suggestions card.
+ *
+ * @param result Result to be dispalyed as prompts.
+ * @param cardNoMatchText No match text.
+ * @return Activity.
+ */
+ public static Activity getQnAPromptsCard(QueryResult result, String cardNoMatchText) {
+ if (result == null) {
+ throw new IllegalArgumentException("result");
+ }
+
+ if (cardNoMatchText == null) {
+ throw new IllegalArgumentException("cardNoMatchText");
+ }
+
+ Activity chatActivity = Activity.createMessageActivity();
+ chatActivity.setText(result.getAnswer());
+ List buttonList = new ArrayList();
+
+ // Add all prompt
+ for (QnAMakerPrompt prompt : result.getContext().getPrompts()) {
+ buttonList.add(new CardAction() {
+ {
+ setValue(prompt.getDisplayText());
+ setType(ActionTypes.IM_BACK);
+ setTitle(prompt.getDisplayText());
+ }
+ });
+ }
+
+ HeroCard plCard = new HeroCard() {
+ {
+ setButtons(buttonList);
+ }
+ };
+
+ // Create the attachment.
+ Attachment attachment = plCard.toAttachment();
+
+ chatActivity.setAttachment(attachment);
+
+ return chatActivity;
+ }
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/QnATelemetryConstants.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/QnATelemetryConstants.java
new file mode 100644
index 000000000..0fad35865
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/QnATelemetryConstants.java
@@ -0,0 +1,68 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna.utils;
+
+/**
+ * Default QnA event and property names logged using IBotTelemetryClient.
+ */
+public final class QnATelemetryConstants {
+
+ private QnATelemetryConstants() { }
+
+ /**
+ * The Key used for the custom event type within telemetry.
+ */
+ public static final String QNA_MSG_EVENT = "QnaMessage"; // Event name
+
+ /**
+ * The Key used when storing a QnA Knowledge Base ID in a custom event within
+ * telemetry.
+ */
+ public static final String KNOWLEDGE_BASE_ID_PROPERTY = "knowledgeBaseId";
+
+ /**
+ * The Key used when storing a QnA Answer in a custom event within telemetry.
+ */
+ public static final String ANSWER_PROPERTY = "answer";
+
+ /**
+ * The Key used when storing a flag indicating if a QnA article was found in a
+ * custom event within telemetry.
+ */
+ public static final String ARTICLE_FOUND_PROPERTY = "articleFound";
+
+ /**
+ * The Key used when storing the Channel ID in a custom event within telemetry.
+ */
+ public static final String CHANNEL_ID_PROPERTY = "channelId";
+
+ /**
+ * The Key used when storing a matched question ID in a custom event within
+ * telemetry.
+ */
+ public static final String MATCHED_QUESTION_PROPERTY = "matchedQuestion";
+
+ /**
+ * The Key used when storing the identified question text in a custom event
+ * within telemetry.
+ */
+ public static final String QUESTION_PROPERTY = "question";
+
+ /**
+ * The Key used when storing the identified question ID in a custom event within
+ * telemetry.
+ */
+ public static final String QUESTION_ID_PROPERTY = "questionId";
+
+ /**
+ * The Key used when storing a QnA Maker result score in a custom event within
+ * telemetry.
+ */
+ public static final String SCORE_PROPERTY = "score";
+
+ /**
+ * The Key used when storing a username in a custom event within telemetry.
+ */
+ public static final String USERNAME_PROPERTY = "username";
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/TrainUtils.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/TrainUtils.java
new file mode 100644
index 000000000..d7fa020cc
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/TrainUtils.java
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna.utils;
+
+import com.microsoft.bot.ai.qna.QnAMakerEndpoint;
+import com.microsoft.bot.ai.qna.models.FeedbackRecords;
+import com.microsoft.bot.restclient.serializer.JacksonAdapter;
+
+import java.io.IOException;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Helper class for train API.
+ */
+public class TrainUtils {
+ private QnAMakerEndpoint endpoint;
+
+ /**
+ * Initializes a new instance of the {@link TrainUtils} class.
+ *
+ * @param withEndpoint QnA Maker endpoint details.
+ */
+ public TrainUtils(QnAMakerEndpoint withEndpoint) {
+ this.endpoint = withEndpoint;
+ }
+
+ /**
+ * Train API to provide feedback.
+ *
+ * @param feedbackRecords Feedback record list.
+ * @return A Task representing the asynchronous operation.
+ * @throws IOException IOException
+ */
+ public CompletableFuture callTrain(FeedbackRecords feedbackRecords) throws IOException {
+ if (feedbackRecords == null) {
+ throw new IllegalArgumentException("feedbackRecords: Feedback records cannot be null.");
+ }
+
+ if (feedbackRecords.getRecords() == null || feedbackRecords.getRecords().length == 0) {
+ return CompletableFuture.completedFuture(null);
+ }
+
+ // Call train
+ return this.queryTrain(feedbackRecords);
+ }
+
+ private CompletableFuture queryTrain(FeedbackRecords feedbackRecords) throws IOException {
+ String requestUrl = String.format("%1$s/knowledgebases/%2$s/train", this.endpoint.getHost(),
+ this.endpoint.getKnowledgeBaseId());
+
+ JacksonAdapter jacksonAdapter = new JacksonAdapter();
+ String jsonRequest = jacksonAdapter.serialize(feedbackRecords);
+
+ HttpRequestUtils httpRequestHelper = new HttpRequestUtils();
+ return httpRequestHelper.executeHttpRequest(requestUrl, jsonRequest, this.endpoint).thenApply(result -> null);
+ }
+}
diff --git a/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/package-info.java b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/package-info.java
new file mode 100644
index 000000000..145e9ebb8
--- /dev/null
+++ b/libraries/bot-ai-qna/src/main/java/com/microsoft/bot/ai/qna/utils/package-info.java
@@ -0,0 +1,8 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for
+// license information.
+
+/**
+ * This package contains the classes for Bot-Builder.
+ */
+package com.microsoft.bot.ai.qna.utils;
diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/MyTurnContext.java b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/MyTurnContext.java
new file mode 100644
index 000000000..872955189
--- /dev/null
+++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/MyTurnContext.java
@@ -0,0 +1,100 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna;
+
+import com.microsoft.bot.builder.BotAdapter;
+import com.microsoft.bot.builder.DeleteActivityHandler;
+import com.microsoft.bot.builder.SendActivitiesHandler;
+import com.microsoft.bot.builder.TurnContext;
+import com.microsoft.bot.builder.TurnContextStateCollection;
+import com.microsoft.bot.builder.UpdateActivityHandler;
+import com.microsoft.bot.schema.Activity;
+import com.microsoft.bot.schema.ConversationReference;
+import com.microsoft.bot.schema.InputHints;
+import com.microsoft.bot.schema.ResourceResponse;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+public class MyTurnContext implements TurnContext {
+
+ private BotAdapter adapter;
+ private Activity activity;
+
+ public MyTurnContext(BotAdapter withAdapter, Activity withActivity) {
+ this.adapter = withAdapter;
+ this.activity = withActivity;
+ }
+
+ public String getLocale() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void setLocale(String withLocale) {
+ throw new UnsupportedOperationException();
+ }
+
+ public BotAdapter getAdapter() {
+ return adapter;
+ }
+
+ public Activity getActivity() {
+ return activity;
+ }
+
+ public TurnContextStateCollection getTurnState() {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean getResponded() {
+ throw new UnsupportedOperationException();
+ }
+
+ public CompletableFuture deleteActivity(String activityId) {
+ throw new UnsupportedOperationException();
+ }
+
+ public CompletableFuture deleteActivity(ConversationReference conversationReference) {
+ throw new UnsupportedOperationException();
+ }
+
+ public TurnContext onDeleteActivity(DeleteActivityHandler handler) {
+ throw new UnsupportedOperationException();
+ }
+
+ public TurnContext onSendActivities(SendActivitiesHandler handler) {
+ throw new UnsupportedOperationException();
+ }
+
+ public TurnContext onUpdateActivity(UpdateActivityHandler handler) {
+ throw new UnsupportedOperationException();
+ }
+
+ public CompletableFuture sendActivities(List activities) {
+ throw new UnsupportedOperationException();
+ }
+
+ public CompletableFuture sendActivity(String textReplyToSend, String speak,
+ InputHints inputHint) {
+ inputHint = inputHint != null ? inputHint : InputHints.ACCEPTING_INPUT;
+ throw new UnsupportedOperationException();
+ }
+
+ public CompletableFuture sendActivity(Activity activity) {
+ throw new UnsupportedOperationException();
+ }
+
+ public CompletableFuture sendActivity(String textToReply) {
+ throw new UnsupportedOperationException();
+ }
+
+ public CompletableFuture sendActivity(String textReplyToSend, String speak) {
+ throw new UnsupportedOperationException();
+ }
+
+ public CompletableFuture updateActivity(Activity activity) {
+ throw new UnsupportedOperationException();
+ }
+
+}
diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/QnAMakerCardEqualityComparer.java b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/QnAMakerCardEqualityComparer.java
new file mode 100644
index 000000000..81d40abd1
--- /dev/null
+++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/QnAMakerCardEqualityComparer.java
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna;
+
+import com.microsoft.bot.schema.Activity;
+import com.microsoft.bot.schema.ActivityTypes;
+
+public class QnAMakerCardEqualityComparer {
+
+ public Boolean Equals(Activity x, Activity y) {
+ if (x == null && y == null) {
+ return true;
+ }
+
+ if(x == null || y == null) {
+ return false;
+ }
+
+ if(x.isType(ActivityTypes.MESSAGE) && y.isType(ActivityTypes.MESSAGE)) {
+ Activity activity1 = x;
+ Activity activity2 = y;
+
+ if (activity1 == null || activity2 == null) {
+ return false;
+ }
+
+ // Check for attachments
+ if (activity1.getAttachments() != null && activity2.getAttachments() != null) {
+ if(activity1.getAttachments().size() != activity2.getAttachments().size()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ public Integer getHashCode(Activity obj) {
+ return obj.getId().hashCode();
+ }
+
+}
diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/QnAMakerRecognizerTests.java b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/QnAMakerRecognizerTests.java
new file mode 100644
index 000000000..cdb2f31d4
--- /dev/null
+++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/QnAMakerRecognizerTests.java
@@ -0,0 +1,255 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.microsoft.bot.builder.RecognizerResult;
+import com.microsoft.bot.builder.TurnContext;
+import com.microsoft.bot.builder.TurnContextImpl;
+import com.microsoft.bot.builder.adapters.TestAdapter;
+import com.microsoft.bot.dialogs.DialogContext;
+import com.microsoft.bot.dialogs.DialogSet;
+import com.microsoft.bot.dialogs.DialogState;
+import com.microsoft.bot.schema.Activity;
+
+import okhttp3.HttpUrl;
+import org.apache.commons.io.FileUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+
+import static org.junit.Assert.fail;
+
+public class QnAMakerRecognizerTests {
+ private final String knowledgeBaseId = "dummy-id";
+ private final String endpointKey = "dummy-key";
+ private final String hostname = "http://localhost";
+ private final Boolean mockQnAResponse = true;
+
+ @Test
+ public void logPiiIsFalseByDefault() {
+ QnAMakerRecognizer recognizer = new QnAMakerRecognizer() {
+ {
+ setHostName(hostname);
+ setEndpointKey(endpointKey);
+ setKnowledgeBaseId(knowledgeBaseId);
+ }
+ };
+ Boolean logPersonalInfo = recognizer.getLogPersonalInformation();
+ // Should be false by default, when not specified by user.
+ Assert.assertFalse(logPersonalInfo);
+ }
+
+ @Test
+ public void noTextNoAnswer() {
+ Activity activity = Activity.createMessageActivity();
+ TurnContext context = new TurnContextImpl(new TestAdapter(), activity);
+ DialogContext dc = new DialogContext(new DialogSet(), context, new DialogState());
+ QnAMakerRecognizer recognizer = new QnAMakerRecognizer() {
+ {
+ setHostName(hostname);
+ setKnowledgeBaseId(knowledgeBaseId);
+ setEndpointKey(endpointKey);
+ }
+ };
+ RecognizerResult result = recognizer.recognize(dc, activity).join();
+ Assert.assertEquals(result.getEntities(), null);
+ Assert.assertEquals(result.getProperties().get("answers"), null);
+ Assert.assertEquals(result.getIntents().get("QnAMatch"), null);
+ Assert.assertNotEquals(result.getIntents().get("None"), null);
+ }
+
+ @Test
+ public void noAnswer() {
+ MockWebServer mockWebServer = new MockWebServer();
+ try {
+ String content = readFileContent("QnaMaker_ReturnsNoAnswer.json");
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode response = mapper.readTree(content);
+ // Set mock response in MockWebServer
+ String url = "/qnamaker/knowledgebases/";
+ String endpoint = "";
+ if (this.mockQnAResponse) {
+ endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer,response, url).port());
+ }
+ String finalEndpoint = endpoint;
+ QnAMakerRecognizer recognizer = new QnAMakerRecognizer() {
+ {
+ setHostName(finalEndpoint);
+ setKnowledgeBaseId(knowledgeBaseId);
+ setEndpointKey(endpointKey);
+ }
+ };
+ Activity activity = Activity.createMessageActivity();
+ activity.setText("test");
+ TurnContext context = new TurnContextImpl(new TestAdapter(), activity);
+ DialogContext dc = new DialogContext(new DialogSet(), context, new DialogState());
+ RecognizerResult result = recognizer.recognize(dc, activity).join();
+ Assert.assertEquals(result.getEntities(), null);
+ Assert.assertEquals(result.getProperties().get("answers"), null);
+ Assert.assertEquals(result.getIntents().get("QnAMatch"), null);
+ Assert.assertNotEquals(result.getIntents().get("None"), null);
+ } catch (Exception e) {
+ fail();
+ } finally {
+ try {
+ mockWebServer.shutdown();
+ } catch (IOException e) {
+ // Empty error
+ }
+ }
+ }
+
+ @Test
+ public void returnAnswers() {
+ MockWebServer mockWebServer = new MockWebServer();
+ try {
+ String content = readFileContent("QnaMaker_ReturnsAnswer.json");
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode response = mapper.readTree(content);
+ // Set mock response in MockWebServer
+ String url = "/qnamaker/knowledgebases/";
+ String endpoint = "";
+ if (this.mockQnAResponse) {
+ endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer,response, url).port());
+ }
+ String finalEndpoint = endpoint;
+ QnAMakerRecognizer recognizer = new QnAMakerRecognizer() {
+ {
+ setHostName(finalEndpoint);
+ setKnowledgeBaseId(knowledgeBaseId);
+ setEndpointKey(endpointKey);
+ }
+ };
+ Activity activity = Activity.createMessageActivity();
+ activity.setText("test");
+ TurnContext context = new TurnContextImpl(new TestAdapter(), activity);
+ DialogContext dc = new DialogContext(new DialogSet(), context, new DialogState());
+ RecognizerResult result = recognizer.recognize(dc, activity).join();
+ validateAnswers(result);
+ Assert.assertEquals(result.getIntents().get("None"), null);
+ Assert.assertNotEquals(result.getIntents().get("QnAMatch"), null);
+ } catch (Exception e) {
+ fail();
+ } finally {
+ try {
+ mockWebServer.shutdown();
+ } catch (IOException e) {
+ // Empty error
+ }
+ }
+ }
+
+ @Test
+ public void topNAnswers() {
+ MockWebServer mockWebServer = new MockWebServer();
+ try {
+ String content = readFileContent("QnaMaker_TopNAnswer.json");
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode response = mapper.readTree(content);
+ // Set mock response in MockWebServer
+ String url = "/qnamaker/knowledgebases/";
+ String endpoint = "";
+ if (this.mockQnAResponse) {
+ endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer,response, url).port());
+ }
+ String finalEndpoint = endpoint;
+ QnAMakerRecognizer recognizer = new QnAMakerRecognizer() {
+ {
+ setHostName(finalEndpoint);
+ setKnowledgeBaseId(knowledgeBaseId);
+ setEndpointKey(endpointKey);
+ }
+ };
+ Activity activity = Activity.createMessageActivity();
+ activity.setText("test");
+ TurnContext context = new TurnContextImpl(new TestAdapter(), activity);
+ DialogContext dc = new DialogContext(new DialogSet(), context, new DialogState());
+ RecognizerResult result = recognizer.recognize(dc, activity).join();
+ validateAnswers(result);
+ Assert.assertEquals(result.getIntents().get("None"), null);
+ Assert.assertNotEquals(result.getIntents().get("QnAMatch"), null);
+ } catch (Exception e) {
+ fail();
+ } finally {
+ try {
+ mockWebServer.shutdown();
+ } catch (IOException e) {
+ // Empty error
+ }
+ }
+ }
+
+ @Test
+ public void returnAnswersWithIntents() {
+ MockWebServer mockWebServer = new MockWebServer();
+ try {
+ String content = readFileContent("QnaMaker_ReturnsAnswerWithIntent.json");
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode response = mapper.readTree(content);
+ // Set mock response in MockWebServer
+ String url = "/qnamaker/knowledgebases/";
+ String endpoint = "";
+ if (this.mockQnAResponse) {
+ endpoint = String.format("%s:%s", hostname, initializeMockServer(mockWebServer,response, url).port());
+ }
+ String finalEndpoint = endpoint;
+ QnAMakerRecognizer recognizer = new QnAMakerRecognizer() {
+ {
+ setHostName(finalEndpoint);
+ setKnowledgeBaseId(knowledgeBaseId);
+ setEndpointKey(endpointKey);
+ }
+ };
+ Activity activity = Activity.createMessageActivity();
+ activity.setText("test");
+ TurnContext context = new TurnContextImpl(new TestAdapter(), activity);
+ DialogContext dc = new DialogContext(new DialogSet(), context, new DialogState());
+ RecognizerResult result = recognizer.recognize(dc, activity).join();
+ validateAnswers(result);
+ Assert.assertEquals(result.getIntents().get("None"), null);
+ Assert.assertNotEquals(result.getIntents().get("DeferToRecognizer_xxx"), null);
+ } catch (Exception e) {
+ fail();
+ } finally {
+ try {
+ mockWebServer.shutdown();
+ } catch (IOException e) {
+ // Empty error
+ }
+ }
+ }
+
+ private String readFileContent (String fileName) throws IOException {
+ String path = Paths.get("", "src", "test", "java", "com", "microsoft", "bot", "ai", "qna",
+ "testData", fileName).toAbsolutePath().toString();
+ File file = new File(path);
+ return FileUtils.readFileToString(file, "utf-8");
+ }
+
+ private HttpUrl initializeMockServer(MockWebServer mockWebServer, JsonNode response, String url) throws IOException {
+ ObjectMapper mapper = new ObjectMapper();
+ String mockResponse = mapper.writeValueAsString(response);
+ mockWebServer.enqueue(new MockResponse()
+ .addHeader("Content-Type", "application/json; charset=utf-8")
+ .setBody(mockResponse));
+
+ mockWebServer.start();
+ return mockWebServer.url(url);
+ }
+
+ private void validateAnswers(RecognizerResult result) {
+ Assert.assertNotEquals(result.getProperties().get("answers"), null);
+ Assert.assertEquals(result.getEntities().get("answer").size(), 1);
+ Assert.assertEquals(result.getEntities().get("$instance").get("answer").get(0).get("startIndex").asInt(), 0);
+ Assert.assertTrue(result.getEntities().get("$instance").get("answer").get(0).get("endIndex") != null);
+ }
+}
diff --git a/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/QnAMakerTests.java b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/QnAMakerTests.java
new file mode 100644
index 000000000..160b8ce78
--- /dev/null
+++ b/libraries/bot-ai-qna/src/test/java/com/microsoft/bot/ai/qna/QnAMakerTests.java
@@ -0,0 +1,2178 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.bot.ai.qna;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.microsoft.bot.ai.qna.dialogs.QnAMakerDialog;
+import com.microsoft.bot.ai.qna.models.FeedbackRecord;
+import com.microsoft.bot.ai.qna.models.FeedbackRecords;
+import com.microsoft.bot.ai.qna.models.Metadata;
+import com.microsoft.bot.ai.qna.models.QnAMakerTraceInfo;
+import com.microsoft.bot.ai.qna.models.QnARequestContext;
+import com.microsoft.bot.ai.qna.models.QueryResult;
+import com.microsoft.bot.ai.qna.models.QueryResults;
+import com.microsoft.bot.ai.qna.utils.QnATelemetryConstants;
+import com.microsoft.bot.builder.BotTelemetryClient;
+import com.microsoft.bot.builder.ConversationState;
+import com.microsoft.bot.builder.MemoryStorage;
+import com.microsoft.bot.builder.MemoryTranscriptStore;
+import com.microsoft.bot.builder.PagedResult;
+import com.microsoft.bot.builder.Storage;
+import com.microsoft.bot.builder.TraceTranscriptLogger;
+import com.microsoft.bot.builder.TranscriptLoggerMiddleware;
+import com.microsoft.bot.builder.TurnContext;
+import com.microsoft.bot.builder.TurnContextImpl;
+import com.microsoft.bot.builder.UserState;
+import com.microsoft.bot.builder.adapters.TestAdapter;
+import com.microsoft.bot.builder.adapters.TestFlow;
+
+import com.microsoft.bot.dialogs.ComponentDialog;
+import com.microsoft.bot.dialogs.Dialog;
+import com.microsoft.bot.dialogs.DialogContext;
+import com.microsoft.bot.dialogs.DialogDependencies;
+import com.microsoft.bot.dialogs.DialogManager;
+import com.microsoft.bot.dialogs.DialogReason;
+import com.microsoft.bot.dialogs.DialogTurnResult;
+import com.microsoft.bot.schema.Activity;
+import com.microsoft.bot.schema.ActivityTypes;
+import com.microsoft.bot.schema.ChannelAccount;
+import com.microsoft.bot.schema.ConversationAccount;
+
+import okhttp3.HttpUrl;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.slf4j.LoggerFactory;
+
+import okhttp3.OkHttpClient;
+
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+@RunWith(MockitoJUnitRunner.class)
+public class QnAMakerTests {
+ private final String knowledgeBaseId = "dummy-id";
+ private final String endpointKey = "dummy-key";
+ private final String hostname = "http://localhost";
+ private final Boolean mockQnAResponse = true;
+
+ @Captor
+ ArgumentCaptor eventNameCaptor;
+
+ @Captor
+ ArgumentCaptor