Repository: microsoft/botbuilder-python
Pull request #316 (2019-09-17 09:10:36)

[QnAMaker] GetAnswerRaw added
Added Active learning enabled flag support
Tests added for that.
[dotnet PR]: https://github.com/microsoft/botbuilder-dotnet/pull/2512
[node PR]: https://github.com/microsoft/botbuilder-js/pull/1172


Patch:
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/models/query_results.py b/libraries/botbuilder-ai/botbuilder/ai/qna/models/query_results.py
index 3983ca35..17fd2a2c 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/qna/models/query_results.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/query_results.py
@@ -2,18 +2,29 @@
 # Licensed under the MIT License.
 
 from typing import List
-
+from msrest.serialization import Model
 from .query_result import QueryResult
 
 
-class QueryResults:
+class QueryResults(Model):
     """ Contains answers for a user query. """
 
-    def __init__(self, answers: List[QueryResult]):
+    _attribute_map = {
+        "answers": {"key": "answers", "type": "[QueryResult]"},
+        "active_learning_enabled": {"key": "activeLearningEnabled", "type": "bool"},
+    }
+
+    def __init__(
+        self, answers: List[QueryResult], active_learning_enabled: bool = None, **kwargs
+    ):
         """
         Parameters:
         -----------
 
         answers: The answers for a user query.
+
+        active_learning_enabled: The active learning enable flag.
         """
+        super(QueryResults, self).__init__(**kwargs)
         self.answers = answers
+        self.active_learning_enabled = active_learning_enabled
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py b/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py
index c19c5715..f01aeb45 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py
@@ -8,7 +8,7 @@ from aiohttp import ClientSession, ClientTimeout
 from botbuilder.schema import Activity
 from botbuilder.core import BotTelemetryClient, NullTelemetryClient, TurnContext
 
-from .models import FeedbackRecord, QueryResult
+from .models import FeedbackRecord, QueryResult, QueryResults
 from .utils import (
     ActiveLearningUtils,
     GenerateAnswerUtils,
@@ -86,6 +86,30 @@ class QnAMaker(QnAMakerTelemetryClient):
         ------
         List[QueryResult]
         """
+        result = await self.get_answers_raw(
+            context, options, telemetry_properties, telemetry_metrics
+        )
+
+        return result.answers
+
+    async def get_answers_raw(
+        self,
+        context: TurnContext,
+        options: QnAMakerOptions = None,
+        telemetry_properties: Dict[str, str] = None,
+        telemetry_metrics: Dict[str, int] = None,
+    ) -> QueryResults:
+        """
+        Generates raw answers from the knowledge base.
+
+        return:
+        -------
+        A list of answers for the user's query, sorted in decreasing order of ranking score.
+
+        rtype:
+        ------
+        QueryResults
+        """
         if not context:
             raise TypeError("QnAMaker.get_answers(): context cannot be None.")
 
@@ -94,10 +118,10 @@ class QnAMaker(QnAMakerTelemetryClient):
                 "QnAMaker.get_answers(): TurnContext's activity must be an Activity instance."
             )
 
-        result = await self._generate_answer_helper.get_answers(context, options)
+        result = await self._generate_answer_helper.get_answers_raw(context, options)
 
         await self.on_qna_result(
-            result, context, telemetry_properties, telemetry_metrics
+            result.answers, context, telemetry_properties, telemetry_metrics
         )
 
         return result
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/utils/generate_answer_utils.py b/libraries/botbuilder-ai/botbuilder/ai/qna/utils/generate_answer_utils.py
index 3a6e28fc..b683c50d 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/qna/utils/generate_answer_utils.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/utils/generate_answer_utils.py
@@ -13,7 +13,12 @@ from .http_request_utils import HttpRequestUtils
 
 from ..qnamaker_endpoint import QnAMakerEndpoint
 from ..qnamaker_options import QnAMakerOptions
-from ..models import GenerateAnswerRequestBody, QnAMakerTraceInfo, QueryResult
+from ..models import (
+    GenerateAnswerRequestBody,
+    QnAMakerTraceInfo,
+    QueryResult,
+    QueryResults,
+)
 
 QNAMAKER_TRACE_NAME = "QnAMaker"
 QNAMAKER_TRACE_LABEL = "QnAMaker Trace"
@@ -58,6 +63,13 @@ class GenerateAnswerUtils:
     async def get_answers(
         self, context: TurnContext, options: QnAMakerOptions = None
     ) -> List[QueryResult]:
+        result: QueryResults = await self.get_answers_raw(context, options)
+
+        return result
+
+    async def get_answers_raw(
+        self, context: TurnContext, options: QnAMakerOptions = None
+    ) -> QueryResults:
         if not isinstance(context, TurnContext):
             raise TypeError(
                 "GenerateAnswerUtils.get_answers(): context must be an instance of TurnContext"
@@ -66,11 +78,9 @@ class GenerateAnswerUtils:
         hydrated_options = self._hydrate_options(options)
         self._validate_options(hydrated_options)
 
-        result: List[QueryResult] = await self._query_qna_service(
-            context, hydrated_options
-        )
+        result: QueryResults = await self._query_qna_service(context, hydrated_options)
 
-        await self._emit_trace_info(context, result, hydrated_options)
+        await self._emit_trace_info(context, result.answers, hydrated_options)
 
         return result
 
@@ -134,7 +144,7 @@ class GenerateAnswerUtils:
 
     async def _query_qna_service(
         self, turn_context: TurnContext, options: QnAMakerOptions
-    ) -> List[QueryResult]:
+    ) -> QueryResults:
         url = f"{ self._endpoint.host }/knowledgebases/{ self._endpoint.knowledge_base_id }/generateAnswer"
 
         question = GenerateAnswerRequestBody(
@@ -152,7 +162,7 @@ class GenerateAnswerUtils:
             url, question, self._endpoint, options.timeout
         )
 
-        result: List[QueryResult] = await self._format_qna_result(response, options)
+        result: QueryResults = await self._format_qna_result(response, options)
 
         return result
 
@@ -182,7 +192,7 @@ class GenerateAnswerUtils:
 
     async def _format_qna_result(
         self, result, options: QnAMakerOptions
-    ) -> List[QueryResult]:
+    ) -> QueryResults:
         json_res = result
         if isinstance(result, ClientResponse):
             json_res = await result.json()
@@ -200,4 +210,14 @@ class GenerateAnswerUtils:
             map(lambda answer: QueryResult(**answer), sorted_answers)
         )
 
-        return answers_as_query_results
+        active_learning_enabled = (
+            json_res["activeLearningEnabled"]
+            if "activeLearningEnabled" in json_res
+            else True
+        )
+
+        query_answer_response = QueryResults(
+            answers_as_query_results, active_learning_enabled
+        )
+
+        return query_answer_response
diff --git a/libraries/botbuilder-ai/setup.py b/libraries/botbuilder-ai/setup.py
index 9904df58..ba8c272d 100644
--- a/libraries/botbuilder-ai/setup.py
+++ b/libraries/botbuilder-ai/setup.py
@@ -33,7 +33,13 @@ setup(
     long_description=long_description,
     long_description_content_type="text/x-rst",
     license=package_info["__license__"],
-    packages=["botbuilder.ai", "botbuilder.ai.qna", "botbuilder.ai.luis"],
+    packages=[
+        "botbuilder.ai",
+        "botbuilder.ai.qna",
+        "botbuilder.ai.luis",
+        "botbuilder.ai.qna.models",
+        "botbuilder.ai.qna.utils",
+    ],
     install_requires=REQUIRES + TESTS_REQUIRES,
     tests_require=TESTS_REQUIRES,
     include_package_data=True,


Test patch:
diff --git a/libraries/botbuilder-ai/tests/qna/test_data/ReturnsAnswer.json b/libraries/botbuilder-ai/tests/qna/test_data/ReturnsAnswer.json
index 98ad181e..fa682d3f 100644
--- a/libraries/botbuilder-ai/tests/qna/test_data/ReturnsAnswer.json
+++ b/libraries/botbuilder-ai/tests/qna/test_data/ReturnsAnswer.json
@@ -1,4 +1,5 @@
 {
+    "activeLearningEnabled": false,
     "answers": [
         {
             "questions": [
diff --git a/libraries/botbuilder-ai/tests/qna/test_qna.py b/libraries/botbuilder-ai/tests/qna/test_qna.py
index eb7f5a9e..fa164361 100644
--- a/libraries/botbuilder-ai/tests/qna/test_qna.py
+++ b/libraries/botbuilder-ai/tests/qna/test_qna.py
@@ -146,6 +146,21 @@ class QnaApplicationTest(aiounittest.AsyncTestCase):
             first_answer.answer,
         )
 
+    async def test_active_learning_enabled_status(self):
+        # Arrange
+        question: str = "how do I clean the stove?"
+        response_path: str = "ReturnsAnswer.json"
+
+        # Act
+        result = await QnaApplicationTest._get_service_result_raw(
+            question, response_path
+        )
+
+        # Assert
+        self.assertIsNotNone(result)
+        self.assertEqual(1, len(result.answers))
+        self.assertFalse(result.active_learning_enabled)
+
     async def test_returns_answer_using_options(self):
         # Arrange
         question: str = "up"
@@ -783,6 +798,27 @@ class QnaApplicationTest(aiounittest.AsyncTestCase):
 
             return result
 
+    @classmethod
+    async def _get_service_result_raw(
+        cls,
+        utterance: str,
+        response_file: str,
+        bot_adapter: BotAdapter = TestAdapter(),
+        options: QnAMakerOptions = None,
+    ) -> [dict]:
+        response_json = QnaApplicationTest._get_json_for_file(response_file)
+
+        qna = QnAMaker(QnaApplicationTest.tests_endpoint)
+        context = QnaApplicationTest._get_context(utterance, bot_adapter)
+
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ):
+            result = await qna.get_answers_raw(context, options)
+
+            return result
+
     @classmethod
     def _get_json_for_file(cls, response_file: str) -> object:
         curr_dir = path.dirname(path.abspath(__file__))