From 1e24fc1d67db691fd0d4637fe8d20b2603d56b9e Mon Sep 17 00:00:00 2001 From: Ashley Finafrock Date: Tue, 10 Nov 2020 16:42:16 -0800 Subject: [PATCH 1/6] Added JoinOperator and documentation on QnA options --- .../botbuilder/ai/qna/models/join_operator.py | 20 +++++++++++ .../botbuilder/ai/qna/qnamaker_options.py | 36 +++++++++++++++++++ libraries/botbuilder-ai/tests/qna/test_qna.py | 7 +++- 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 libraries/botbuilder-ai/botbuilder/ai/qna/models/join_operator.py diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/models/join_operator.py b/libraries/botbuilder-ai/botbuilder/ai/qna/models/join_operator.py new file mode 100644 index 000000000..6b6cb4166 --- /dev/null +++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/join_operator.py @@ -0,0 +1,20 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from enum import Enum + + +class JoinOperator(str, Enum): + """ + Join Operator for Strict Filters. + + remarks: + -------- + For example, using multiple filters in a query, if you want results that have + metadata that matches all filters, then use `"AND"` operator. + + If instead you only wish that the results from querying a QnA knowledge base + match at least one of the filters, then use `"OR"` operator. + """ + AND = "AND" + OR = "OR" diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker_options.py b/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker_options.py index 95ae70b81..7250acf55 100644 --- a/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker_options.py +++ b/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker_options.py @@ -3,9 +3,17 @@ from .models import Metadata, QnARequestContext from .models.ranker_types import RankerTypes +from .models.join_operator import JoinOperator class QnAMakerOptions: + """ + Defines options used to configure a `QnAMaker` instance. + + remarks: + -------- + All parameters are optional. + """ def __init__( self, score_threshold: float = 0.0, @@ -16,7 +24,34 @@ def __init__( qna_id: int = None, is_test: bool = False, ranker_type: str = RankerTypes.DEFAULT, + strict_filters_join_operator: str = JoinOperator.AND ): + """ + Parameters: + ----------- + score_threshold (float): + The minimum score threshold, used to filter returned results. + Values range from score of 0.0 to 1.0. + timeout (int): + The time in milliseconds to wait before the request times out. + top (int): + The number of ranked results to return. + strict_filters ([Metadata]): + Filters to use on queries to a QnA knowledge base, based on a + QnA pair's metadata. + context ([QnARequestContext]): + The context of the previous turn. + qna_id (int): + Id of the current question asked (if available). + is_test (bool): + A value indicating whether to call test or prod environment of a knowledge base. + ranker_type (str): + The QnA ranker type to use. + strict_filters_join_operator (str): + A value indicating how strictly you want to apply strict_filters on QnA pairs' metadata. + For example, when combining several metadata filters, you can determine if you are + concerned with all filters matching or just at least one filter matching. + """ self.score_threshold = score_threshold self.timeout = timeout self.top = top @@ -25,3 +60,4 @@ def __init__( self.qna_id = qna_id self.is_test = is_test self.ranker_type = ranker_type + self.strict_filters_join_operator = strict_filters_join_operator diff --git a/libraries/botbuilder-ai/tests/qna/test_qna.py b/libraries/botbuilder-ai/tests/qna/test_qna.py index e733b6564..26bd99ddc 100644 --- a/libraries/botbuilder-ai/tests/qna/test_qna.py +++ b/libraries/botbuilder-ai/tests/qna/test_qna.py @@ -166,7 +166,12 @@ async def test_active_learning_enabled_status(self): self.assertIsNotNone(result) self.assertEqual(1, len(result.answers)) self.assertFalse(result.active_learning_enabled) - + + async def test_returns_answer_with_strict_filters_with_or_operator(self): + # Arrange + question: str = "is this about" + # TODO finish writing test + async def test_returns_answer_using_requests_module(self): question: str = "how do I clean the stove?" response_path: str = "ReturnsAnswer.json" From 39308814121de867fd234ddef7e358909797fa48 Mon Sep 17 00:00:00 2001 From: Ashley Finafrock Date: Thu, 12 Nov 2020 11:12:51 -0800 Subject: [PATCH 2/6] Finished writing unit tests --- .../botbuilder/ai/qna/models/__init__.py | 2 + .../models/generate_answer_request_body.py | 7 ++ .../botbuilder/ai/qna/models/join_operator.py | 9 +- .../botbuilder/ai/qna/qnamaker_options.py | 51 +++++----- .../ai/qna/utils/generate_answer_utils.py | 4 + ...sAnswer_WithStrictFilter_And_Operator.json | 29 ++++++ ...nsAnswer_WithStrictFilter_Or_Operator.json | 76 +++++++++++++++ libraries/botbuilder-ai/tests/qna/test_qna.py | 96 ++++++++++++++++++- 8 files changed, 240 insertions(+), 34 deletions(-) create mode 100644 libraries/botbuilder-ai/tests/qna/test_data/RetrunsAnswer_WithStrictFilter_And_Operator.json create mode 100644 libraries/botbuilder-ai/tests/qna/test_data/RetrunsAnswer_WithStrictFilter_Or_Operator.json diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/models/__init__.py b/libraries/botbuilder-ai/botbuilder/ai/qna/models/__init__.py index 018d40c95..608ffeef1 100644 --- a/libraries/botbuilder-ai/botbuilder/ai/qna/models/__init__.py +++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/__init__.py @@ -8,6 +8,7 @@ from .feedback_record import FeedbackRecord from .feedback_records import FeedbackRecords from .generate_answer_request_body import GenerateAnswerRequestBody +from .join_operator import JoinOperator from .metadata import Metadata from .prompt import Prompt from .qnamaker_trace_info import QnAMakerTraceInfo @@ -21,6 +22,7 @@ "FeedbackRecord", "FeedbackRecords", "GenerateAnswerRequestBody", + "JoinOperator", "Metadata", "Prompt", "QnAMakerTraceInfo", diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/models/generate_answer_request_body.py b/libraries/botbuilder-ai/botbuilder/ai/qna/models/generate_answer_request_body.py index 4a4e9fdd7..dd4104185 100644 --- a/libraries/botbuilder-ai/botbuilder/ai/qna/models/generate_answer_request_body.py +++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/generate_answer_request_body.py @@ -16,6 +16,10 @@ class GenerateAnswerRequestBody(Model): "qna_id": {"key": "qnaId", "type": "int"}, "is_test": {"key": "isTest", "type": "bool"}, "ranker_type": {"key": "rankerType", "type": "RankerTypes"}, + "strict_filters_join_operator": { + "key": "strictFiltersCompoundOperationType", + "type": "str", + }, } def __init__(self, **kwargs): @@ -28,3 +32,6 @@ def __init__(self, **kwargs): self.qna_id = kwargs.get("qna_id", None) self.is_test = kwargs.get("is_test", None) self.ranker_type = kwargs.get("ranker_type", None) + self.strict_filters_join_operator = kwargs.get( + "strict_filters_join_operator", None + ) diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/models/join_operator.py b/libraries/botbuilder-ai/botbuilder/ai/qna/models/join_operator.py index 6b6cb4166..a454afa81 100644 --- a/libraries/botbuilder-ai/botbuilder/ai/qna/models/join_operator.py +++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/join_operator.py @@ -10,11 +10,12 @@ class JoinOperator(str, Enum): remarks: -------- - For example, using multiple filters in a query, if you want results that have - metadata that matches all filters, then use `"AND"` operator. + For example, when using multiple filters in a query, if you want results that + have metadata that matches all filters, then use `AND` operator. - If instead you only wish that the results from querying a QnA knowledge base - match at least one of the filters, then use `"OR"` operator. + If instead you only wish that the results from knowledge base match + at least one of the filters, then use `OR` operator. """ + AND = "AND" OR = "OR" diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker_options.py b/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker_options.py index 7250acf55..af4a4ad1c 100644 --- a/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker_options.py +++ b/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker_options.py @@ -14,6 +14,7 @@ class QnAMakerOptions: -------- All parameters are optional. """ + def __init__( self, score_threshold: float = 0.0, @@ -24,33 +25,33 @@ def __init__( qna_id: int = None, is_test: bool = False, ranker_type: str = RankerTypes.DEFAULT, - strict_filters_join_operator: str = JoinOperator.AND + strict_filters_join_operator: str = JoinOperator.AND, ): """ - Parameters: - ----------- - score_threshold (float): - The minimum score threshold, used to filter returned results. - Values range from score of 0.0 to 1.0. - timeout (int): - The time in milliseconds to wait before the request times out. - top (int): - The number of ranked results to return. - strict_filters ([Metadata]): - Filters to use on queries to a QnA knowledge base, based on a - QnA pair's metadata. - context ([QnARequestContext]): - The context of the previous turn. - qna_id (int): - Id of the current question asked (if available). - is_test (bool): - A value indicating whether to call test or prod environment of a knowledge base. - ranker_type (str): - The QnA ranker type to use. - strict_filters_join_operator (str): - A value indicating how strictly you want to apply strict_filters on QnA pairs' metadata. - For example, when combining several metadata filters, you can determine if you are - concerned with all filters matching or just at least one filter matching. + Parameters: + ----------- + score_threshold (float): + The minimum score threshold, used to filter returned results. + Values range from score of 0.0 to 1.0. + timeout (int): + The time in milliseconds to wait before the request times out. + top (int): + The number of ranked results to return. + strict_filters ([Metadata]): + Filters to use on queries to a QnA knowledge base, based on a + QnA pair's metadata. + context ([QnARequestContext]): + The context of the previous turn. + qna_id (int): + Id of the current question asked (if available). + is_test (bool): + A value indicating whether to call test or prod environment of a knowledge base. + ranker_type (str): + The QnA ranker type to use. + strict_filters_join_operator (str): + A value indicating how strictly you want to apply strict_filters on QnA pairs' metadata. + For example, when combining several metadata filters, you can determine if you are + concerned with all filters matching or just at least one filter matching. """ self.score_threshold = score_threshold self.timeout = timeout 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 b12c492c7..1f335f9e6 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 @@ -144,6 +144,9 @@ def _hydrate_options(self, query_options: QnAMakerOptions) -> QnAMakerOptions: hydrated_options.qna_id = query_options.qna_id hydrated_options.is_test = query_options.is_test hydrated_options.ranker_type = query_options.ranker_type + hydrated_options.strict_filters_join_operator = ( + query_options.strict_filters_join_operator + ) return hydrated_options @@ -161,6 +164,7 @@ async def _query_qna_service( qna_id=options.qna_id, is_test=options.is_test, ranker_type=options.ranker_type, + strict_filters_join_operator=options.strict_filters_join_operator, ) http_request_helper = HttpRequestUtils(self._http_client) diff --git a/libraries/botbuilder-ai/tests/qna/test_data/RetrunsAnswer_WithStrictFilter_And_Operator.json b/libraries/botbuilder-ai/tests/qna/test_data/RetrunsAnswer_WithStrictFilter_And_Operator.json new file mode 100644 index 000000000..1bb54754a --- /dev/null +++ b/libraries/botbuilder-ai/tests/qna/test_data/RetrunsAnswer_WithStrictFilter_And_Operator.json @@ -0,0 +1,29 @@ +{ + "answers": [ + { + "questions": [ + "Where can you find Misty", + "Misty" + ], + "answer": "Wherever people are having a swimming good time", + "score": 74.51, + "id": 27, + "source": "Editorial", + "metadata": [ + { + "name": "species", + "value": "human" + }, + { + "name": "type", + "value": "water" + } + ], + "context": { + "isContextOnly": false, + "prompts": [] + } + } + ], + "activeLearningEnabled": true +} diff --git a/libraries/botbuilder-ai/tests/qna/test_data/RetrunsAnswer_WithStrictFilter_Or_Operator.json b/libraries/botbuilder-ai/tests/qna/test_data/RetrunsAnswer_WithStrictFilter_Or_Operator.json new file mode 100644 index 000000000..c83c1b7ae --- /dev/null +++ b/libraries/botbuilder-ai/tests/qna/test_data/RetrunsAnswer_WithStrictFilter_Or_Operator.json @@ -0,0 +1,76 @@ +{ + "answers": [ + { + "questions": [ + "Where can you find Squirtle" + ], + "answer": "Did you not see him in the first three balls?", + "score": 80.22, + "id": 28, + "source": "Editorial", + "metadata": [ + { + "name": "species", + "value": "turtle" + }, + { + "name": "type", + "value": "water" + } + ], + "context": { + "isContextOnly": false, + "prompts": [] + } + }, + { + "questions": [ + "Where can you find Ash", + "Ash" + ], + "answer": "I don't know. Maybe ask your little electric mouse friend?", + "score": 63.74, + "id": 26, + "source": "Editorial", + "metadata": [ + { + "name": "species", + "value": "human" + }, + { + "name": "type", + "value": "miscellaneous" + } + ], + "context": { + "isContextOnly": false, + "prompts": [] + } + }, + { + "questions": [ + "Where can you find Misty", + "Misty" + ], + "answer": "Wherever people are having a swimming good time", + "score": 31.13, + "id": 27, + "source": "Editorial", + "metadata": [ + { + "name": "species", + "value": "human" + }, + { + "name": "type", + "value": "water" + } + ], + "context": { + "isContextOnly": false, + "prompts": [] + } + } + ], + "activeLearningEnabled": true +} \ 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 index 26bd99ddc..7025ce801 100644 --- a/libraries/botbuilder-ai/tests/qna/test_qna.py +++ b/libraries/botbuilder-ai/tests/qna/test_qna.py @@ -17,6 +17,7 @@ from botbuilder.ai.qna import QnAMakerEndpoint, QnAMaker, QnAMakerOptions from botbuilder.ai.qna.models import ( FeedbackRecord, + JoinOperator, Metadata, QueryResult, QnARequestContext, @@ -166,12 +167,97 @@ async def test_active_learning_enabled_status(self): self.assertIsNotNone(result) self.assertEqual(1, len(result.answers)) self.assertFalse(result.active_learning_enabled) - - async def test_returns_answer_with_strict_filters_with_or_operator(self): + + async def test_returns_answer_with_strict_filters_with_OR_operator(self): + # Arrange + question: str = "Where can you find" + response_path: str = "RetrunsAnswer_WithStrictFilter_OR_Operator.json" + response_json = QnaApplicationTest._get_json_for_file(response_path) + + strict_filters = [ + Metadata(name="species", value="human"), + Metadata(name="type", value="water"), + ] + options = QnAMakerOptions( + top=5, + strict_filters=strict_filters, + strict_filters_join_operator=JoinOperator.OR, + ) + qna = QnAMaker(endpoint=QnaApplicationTest.tests_endpoint) + context = QnaApplicationTest._get_context(question, TestAdapter()) + + # Act + with patch( + "aiohttp.ClientSession.post", + return_value=aiounittest.futurized(response_json), + ) as mock_http_client: + result = await qna.get_answers_raw(context, options) + + serialized_http_req_args = mock_http_client.call_args.kwargs["data"] + req_args = json.loads(serialized_http_req_args) + + # Assert + self.assertIsNotNone(result) + self.assertEqual(3, len(result.answers)) + self.assertEqual( + JoinOperator.OR, req_args["strictFiltersCompoundOperationType"] + ) + + req_args_strict_filters = req_args["strictFilters"] + + first_filter = strict_filters[0] + self.assertEqual(first_filter.name, req_args_strict_filters[0]["name"]) + self.assertEqual(first_filter.value, req_args_strict_filters[0]["value"]) + + second_filter = strict_filters[1] + self.assertEqual(second_filter.name, req_args_strict_filters[1]["name"]) + self.assertEqual(second_filter.value, req_args_strict_filters[1]["value"]) + + async def test_returns_answer_with_strict_filters_with_AND_operator(self): # Arrange - question: str = "is this about" - # TODO finish writing test - + question: str = "Where can you find" + response_path: str = "RetrunsAnswer_WithStrictFilter_AND_Operator.json" + response_json = QnaApplicationTest._get_json_for_file(response_path) + + strict_filters = [ + Metadata(name="species", value="human"), + Metadata(name="type", value="water"), + ] + options = QnAMakerOptions( + top=5, + strict_filters=strict_filters, + strict_filters_join_operator=JoinOperator.AND, + ) + qna = QnAMaker(endpoint=QnaApplicationTest.tests_endpoint) + context = QnaApplicationTest._get_context(question, TestAdapter()) + + # Act + with patch( + "aiohttp.ClientSession.post", + return_value=aiounittest.futurized(response_json), + ) as mock_http_client: + result = await qna.get_answers_raw(context, options) + + serialized_http_req_args = mock_http_client.call_args.kwargs["data"] + req_args = json.loads(serialized_http_req_args) + + # Assert + self.assertIsNotNone(result) + self.assertEqual(1, len(result.answers)) + self.assertEqual( + JoinOperator.AND, req_args["strictFiltersCompoundOperationType"] + ) + + req_args_strict_filters = req_args["strictFilters"] + + first_filter = strict_filters[0] + self.assertEqual(first_filter.name, req_args_strict_filters[0]["name"]) + self.assertEqual(first_filter.value, req_args_strict_filters[0]["value"]) + + second_filter = strict_filters[1] + self.assertEqual(second_filter.name, req_args_strict_filters[1]["name"]) + self.assertEqual(second_filter.value, req_args_strict_filters[1]["value"]) + async def test_returns_answer_using_requests_module(self): question: str = "how do I clean the stove?" response_path: str = "ReturnsAnswer.json" From d30f751fdcf9648243a8ea69b3c3441e8f542ff3 Mon Sep 17 00:00:00 2001 From: Ashley Finafrock Date: Thu, 12 Nov 2020 11:27:57 -0800 Subject: [PATCH 3/6] Added newline at end of file --- .../test_data/RetrunsAnswer_WithStrictFilter_Or_Operator.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/botbuilder-ai/tests/qna/test_data/RetrunsAnswer_WithStrictFilter_Or_Operator.json b/libraries/botbuilder-ai/tests/qna/test_data/RetrunsAnswer_WithStrictFilter_Or_Operator.json index c83c1b7ae..3346464fc 100644 --- a/libraries/botbuilder-ai/tests/qna/test_data/RetrunsAnswer_WithStrictFilter_Or_Operator.json +++ b/libraries/botbuilder-ai/tests/qna/test_data/RetrunsAnswer_WithStrictFilter_Or_Operator.json @@ -73,4 +73,4 @@ } ], "activeLearningEnabled": true -} \ No newline at end of file +} From 86f823d84a1807c5fa5e13ae81eef062347cf0be Mon Sep 17 00:00:00 2001 From: Ashley Finafrock Date: Thu, 12 Nov 2020 11:35:56 -0800 Subject: [PATCH 4/6] Updated file name --- libraries/botbuilder-ai/tests/qna/test_qna.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/botbuilder-ai/tests/qna/test_qna.py b/libraries/botbuilder-ai/tests/qna/test_qna.py index 7025ce801..fc903ff45 100644 --- a/libraries/botbuilder-ai/tests/qna/test_qna.py +++ b/libraries/botbuilder-ai/tests/qna/test_qna.py @@ -171,7 +171,7 @@ async def test_active_learning_enabled_status(self): async def test_returns_answer_with_strict_filters_with_OR_operator(self): # Arrange question: str = "Where can you find" - response_path: str = "RetrunsAnswer_WithStrictFilter_OR_Operator.json" + response_path: str = "RetrunsAnswer_WithStrictFilter_Or_Operator.json" response_json = QnaApplicationTest._get_json_for_file(response_path) strict_filters = [ @@ -216,7 +216,7 @@ async def test_returns_answer_with_strict_filters_with_OR_operator(self): async def test_returns_answer_with_strict_filters_with_AND_operator(self): # Arrange question: str = "Where can you find" - response_path: str = "RetrunsAnswer_WithStrictFilter_AND_Operator.json" + response_path: str = "RetrunsAnswer_WithStrictFilter_And_Operator.json" response_json = QnaApplicationTest._get_json_for_file(response_path) strict_filters = [ From d141a202c1bc8a26199b6b9cbd01d3fabe8a5920 Mon Sep 17 00:00:00 2001 From: Ashley Finafrock Date: Thu, 12 Nov 2020 12:00:44 -0800 Subject: [PATCH 5/6] Snake case method name in tests --- libraries/botbuilder-ai/tests/qna/test_qna.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/botbuilder-ai/tests/qna/test_qna.py b/libraries/botbuilder-ai/tests/qna/test_qna.py index fc903ff45..30bba8563 100644 --- a/libraries/botbuilder-ai/tests/qna/test_qna.py +++ b/libraries/botbuilder-ai/tests/qna/test_qna.py @@ -168,7 +168,7 @@ async def test_active_learning_enabled_status(self): self.assertEqual(1, len(result.answers)) self.assertFalse(result.active_learning_enabled) - async def test_returns_answer_with_strict_filters_with_OR_operator(self): + async def test_returns_answer_with_strict_filters_with_or_operator(self): # Arrange question: str = "Where can you find" response_path: str = "RetrunsAnswer_WithStrictFilter_Or_Operator.json" @@ -213,7 +213,7 @@ async def test_returns_answer_with_strict_filters_with_OR_operator(self): self.assertEqual(second_filter.name, req_args_strict_filters[1]["name"]) self.assertEqual(second_filter.value, req_args_strict_filters[1]["value"]) - async def test_returns_answer_with_strict_filters_with_AND_operator(self): + async def test_returns_answer_with_strict_filters_with_and_operator(self): # Arrange question: str = "Where can you find" response_path: str = "RetrunsAnswer_WithStrictFilter_And_Operator.json" From 8198eadaa660f5859e6163c51c9cc12a564843b7 Mon Sep 17 00:00:00 2001 From: Ashley Finafrock Date: Thu, 12 Nov 2020 12:19:08 -0800 Subject: [PATCH 6/6] Access call_arg values as tuple in unit tests --- libraries/botbuilder-ai/tests/qna/test_qna.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/botbuilder-ai/tests/qna/test_qna.py b/libraries/botbuilder-ai/tests/qna/test_qna.py index 30bba8563..236594ac0 100644 --- a/libraries/botbuilder-ai/tests/qna/test_qna.py +++ b/libraries/botbuilder-ai/tests/qna/test_qna.py @@ -193,7 +193,7 @@ async def test_returns_answer_with_strict_filters_with_or_operator(self): ) as mock_http_client: result = await qna.get_answers_raw(context, options) - serialized_http_req_args = mock_http_client.call_args.kwargs["data"] + serialized_http_req_args = mock_http_client.call_args[1]["data"] req_args = json.loads(serialized_http_req_args) # Assert @@ -238,7 +238,7 @@ async def test_returns_answer_with_strict_filters_with_and_operator(self): ) as mock_http_client: result = await qna.get_answers_raw(context, options) - serialized_http_req_args = mock_http_client.call_args.kwargs["data"] + serialized_http_req_args = mock_http_client.call_args[1]["data"] req_args = json.loads(serialized_http_req_args) # Assert