From ad5bbef8e6bafb053ebd541a7d998cf936627927 Mon Sep 17 00:00:00 2001 From: Gurvinder Singh Date: Tue, 17 Sep 2019 14:39:46 +0530 Subject: [PATCH 1/3] [QnAMaker] GetAnswerRaw added --- .../botbuilder/ai/qna/models/query_results.py | 13 ++++++- .../botbuilder/ai/qna/qnamaker.py | 28 +++++++++++++-- .../ai/qna/utils/generate_answer_utils.py | 33 +++++++++++------ libraries/botbuilder-ai/setup.py | 8 ++++- .../tests/qna/test_data/ReturnsAnswer.json | 1 + libraries/botbuilder-ai/tests/qna/test_qna.py | 36 +++++++++++++++++++ 6 files changed, 105 insertions(+), 14 deletions(-) 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 3983ca351..9f754f7dd 100644 --- a/libraries/botbuilder-ai/botbuilder/ai/qna/models/query_results.py +++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/query_results.py @@ -9,11 +9,22 @@ class QueryResults: """ 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 c19c57154..d49104292 100644 --- a/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py +++ b/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py @@ -8,7 +8,7 @@ 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 @@ async def get_answers( ------ 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.") @@ -97,7 +121,7 @@ async def get_answers( result = await self._generate_answer_helper.get_answers(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 5831875cc..d7ecffa67 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 ..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" @@ -57,7 +62,7 @@ def __init__( async def get_answers( self, context: TurnContext, options: QnAMakerOptions = None - ) -> List[QueryResult]: + ) -> QueryResults: if not isinstance(context, TurnContext): raise TypeError( "GenerateAnswerUtils.get_answers(): context must be an instance of TurnContext" @@ -66,11 +71,9 @@ async def get_answers( 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 +137,7 @@ def _hydrate_options(self, query_options: QnAMakerOptions) -> QnAMakerOptions: 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( @@ -151,7 +154,7 @@ async def _query_qna_service( 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 @@ -180,7 +183,7 @@ async def _emit_trace_info( async def _format_qna_result( self, result, options: QnAMakerOptions - ) -> List[QueryResult]: + ) -> QueryResults: json_res = result if isinstance(result, ClientResponse): json_res = await result.json() @@ -198,4 +201,14 @@ async def _format_qna_result( 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 9904df585..ba8c272df 100644 --- a/libraries/botbuilder-ai/setup.py +++ b/libraries/botbuilder-ai/setup.py @@ -33,7 +33,13 @@ 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, diff --git a/libraries/botbuilder-ai/tests/qna/test_data/ReturnsAnswer.json b/libraries/botbuilder-ai/tests/qna/test_data/ReturnsAnswer.json index 98ad181ee..fa682d3f2 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 24134fc9e..81f095cac 100644 --- a/libraries/botbuilder-ai/tests/qna/test_qna.py +++ b/libraries/botbuilder-ai/tests/qna/test_qna.py @@ -146,6 +146,21 @@ async def test_returns_answer(self): 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" @@ -765,6 +780,27 @@ async def _get_service_result( 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__)) From 8c320dac8f2c7d85dc4c6b81bcd6642f5b9b53ad Mon Sep 17 00:00:00 2001 From: Gurvinder Singh Date: Wed, 18 Sep 2019 15:31:01 +0530 Subject: [PATCH 2/3] Making a new function with QueryResults and putting old function in obsolete mode. --- libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py | 2 +- .../botbuilder/ai/qna/utils/generate_answer_utils.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py b/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py index d49104292..f01aeb453 100644 --- a/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py +++ b/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py @@ -118,7 +118,7 @@ async def get_answers_raw( "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.answers, context, telemetry_properties, telemetry_metrics 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 d7ecffa67..6913b22b4 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 @@ -62,6 +62,13 @@ def __init__( 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( From 312932c10f47feb913880b83b149e482fd84fe19 Mon Sep 17 00:00:00 2001 From: Gurvinder Singh Date: Mon, 23 Sep 2019 17:28:15 +0530 Subject: [PATCH 3/3] Comment fix --- .../botbuilder-ai/botbuilder/ai/qna/models/query_results.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 9f754f7dd..17fd2a2c8 100644 --- a/libraries/botbuilder-ai/botbuilder/ai/qna/models/query_results.py +++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/query_results.py @@ -2,11 +2,11 @@ # 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. """ _attribute_map = {