Repository: microsoft/botbuilder-python

Issue:
[PORT] [QnAMaker] Volitile LowScoreVariation and Active Learning Issue fix
> Port this change from botbuilder-dotnet/master branch:
https://github.com/microsoft/botbuilder-dotnet/pull/3786

For issue #3714 


# Changed projects
* Microsoft.Bot.Builder.AI.QnA
* Microsoft.Bot.Builder.AI.QnA.Tests

[changes required]





Discussion:

Resolving patch:
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/dialogs/qnamaker_dialog.py b/libraries/botbuilder-ai/botbuilder/ai/qna/dialogs/qnamaker_dialog.py
--- a/libraries/botbuilder-ai/botbuilder/ai/qna/dialogs/qnamaker_dialog.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/dialogs/qnamaker_dialog.py
@@ -292,13 +292,12 @@ async def __call_generate_answer(self, step_context: WaterfallStepContext):
         # Check if active learning is enabled and send card
         # maximum_score_for_low_score_variation is the score above which no need to check for feedback.
         if (
-            is_active_learning_enabled
-            and response.answers
+            response.answers
             and response.answers[0].score <= self.maximum_score_for_low_score_variation
         ):
             # Get filtered list of the response that support low score variation criteria.
             response.answers = qna_client.get_low_score_variation(response.answers)
-            if len(response.answers) > 1:
+            if len(response.answers) > 1 and is_active_learning_enabled:
                 suggested_questions = [qna.questions[0] for qna in response.answers]
                 message = QnACardBuilder.get_suggestions_card(
                     suggested_questions,


Test patch:
diff --git a/libraries/botbuilder-ai/tests/qna/test_data/QnaMaker_TopNAnswer.json b/libraries/botbuilder-ai/tests/qna/test_data/QnaMaker_TopNAnswer.json
new file mode 100644
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/qna/test_data/QnaMaker_TopNAnswer.json
@@ -0,0 +1,65 @@
+{
+  "activeLearningEnabled": true,
+  "answers": [
+    {
+      "questions": [
+        "Q1"
+      ],
+      "answer": "A1",
+      "score": 80,
+      "id": 15,
+      "source": "Editorial",
+      "metadata": [
+        {
+          "name": "topic",
+          "value": "value"
+        }
+      ]
+    },
+    {
+      "questions": [
+        "Q2"
+      ],
+      "answer": "A2",
+      "score": 78,
+      "id": 16,
+      "source": "Editorial",
+      "metadata": [
+        {
+          "name": "topic",
+          "value": "value"
+        }
+      ]
+    },
+    {
+      "questions": [
+        "Q3"
+      ],
+      "answer": "A3",
+      "score": 75,
+      "id": 17,
+      "source": "Editorial",
+      "metadata": [
+        {
+          "name": "topic",
+          "value": "value"
+        }
+      ]
+    },
+    {
+      "questions": [
+        "Q4"
+      ],
+      "answer": "A4",
+      "score": 50,
+      "id": 18,
+      "source": "Editorial",
+      "metadata": [
+        {
+          "name": "topic",
+          "value": "value"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/libraries/botbuilder-ai/tests/qna/test_data/QnaMaker_TopNAnswer_DisableActiveLearning.json b/libraries/botbuilder-ai/tests/qna/test_data/QnaMaker_TopNAnswer_DisableActiveLearning.json
new file mode 100644
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/qna/test_data/QnaMaker_TopNAnswer_DisableActiveLearning.json
@@ -0,0 +1,65 @@
+{
+  "activeLearningEnabled": false,
+  "answers": [
+    {
+      "questions": [
+        "Q1"
+      ],
+      "answer": "A1",
+      "score": 80,
+      "id": 15,
+      "source": "Editorial",
+      "metadata": [
+        {
+          "name": "topic",
+          "value": "value"
+        }
+      ]
+    },
+    {
+      "questions": [
+        "Q2"
+      ],
+      "answer": "A2",
+      "score": 78,
+      "id": 16,
+      "source": "Editorial",
+      "metadata": [
+        {
+          "name": "topic",
+          "value": "value"
+        }
+      ]
+    },
+    {
+      "questions": [
+        "Q3"
+      ],
+      "answer": "A3",
+      "score": 75,
+      "id": 17,
+      "source": "Editorial",
+      "metadata": [
+        {
+          "name": "topic",
+          "value": "value"
+        }
+      ]
+    },
+    {
+      "questions": [
+        "Q4"
+      ],
+      "answer": "A4",
+      "score": 50,
+      "id": 18,
+      "source": "Editorial",
+      "metadata": [
+        {
+          "name": "topic",
+          "value": "value"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/libraries/botbuilder-ai/tests/qna/test_qna.py b/libraries/botbuilder-ai/tests/qna/test_qna.py
--- a/libraries/botbuilder-ai/tests/qna/test_qna.py
+++ b/libraries/botbuilder-ai/tests/qna/test_qna.py
@@ -2,6 +2,7 @@
 # Licensed under the MIT License.
 
 # pylint: disable=protected-access
+# pylint: disable=too-many-lines
 
 import json
 from os import path
@@ -811,6 +812,46 @@ async def test_should_answer_with_low_score_without_provided_context(self):
             )
             self.assertEqual(True, results[0].score < 1, "Score should be low.")
 
+    async def test_low_score_variation(self):
+        qna = QnAMaker(QnaApplicationTest.tests_endpoint)
+        options = QnAMakerOptions(top=5, context=None)
+
+        turn_context = QnaApplicationTest._get_context("Q11", TestAdapter())
+        response_json = QnaApplicationTest._get_json_for_file(
+            "QnaMaker_TopNAnswer.json"
+        )
+
+        # active learning enabled
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ):
+            results = await qna.get_answers(turn_context, options)
+            self.assertIsNotNone(results)
+            self.assertEqual(4, len(results), "should get four results")
+
+            filtered_results = qna.get_low_score_variation(results)
+            self.assertIsNotNone(filtered_results)
+            self.assertEqual(3, len(filtered_results), "should get three results")
+
+        # active learning disabled
+        turn_context = QnaApplicationTest._get_context("Q11", TestAdapter())
+        response_json = QnaApplicationTest._get_json_for_file(
+            "QnaMaker_TopNAnswer_DisableActiveLearning.json"
+        )
+
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ):
+            results = await qna.get_answers(turn_context, options)
+            self.assertIsNotNone(results)
+            self.assertEqual(4, len(results), "should get four results")
+
+            filtered_results = qna.get_low_score_variation(results)
+            self.assertIsNotNone(filtered_results)
+            self.assertEqual(3, len(filtered_results), "should get three results")
+
     @classmethod
     async def _get_service_result(
         cls,