diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/models/prompt.py b/libraries/botbuilder-ai/botbuilder/ai/qna/models/prompt.py index 0865c2d22..b0a2fe7fe 100644 --- a/libraries/botbuilder-ai/botbuilder/ai/qna/models/prompt.py +++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/prompt.py @@ -15,7 +15,7 @@ class Prompt(Model): } def __init__(self, **kwargs): - super(Prompt, self).__init__(**kwargs) + super().__init__(**kwargs) self.display_order = kwargs.get("display_order", None) self.qna_id = kwargs.get("qna_id", None) self.display_text = kwargs.get("display_text", None) diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/models/qna_response_context.py b/libraries/botbuilder-ai/botbuilder/ai/qna/models/qna_response_context.py index e3814cca9..bf68bb213 100644 --- a/libraries/botbuilder-ai/botbuilder/ai/qna/models/qna_response_context.py +++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/qna_response_context.py @@ -26,6 +26,6 @@ def __init__(self, **kwargs): """ - super(QnAResponseContext, self).__init__(**kwargs) + super().__init__(**kwargs) self.is_context_only = kwargs.get("is_context_only", None) self.prompts = kwargs.get("prompts", None) diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/models/query_result.py b/libraries/botbuilder-ai/botbuilder/ai/qna/models/query_result.py index f91febf5f..a0b1c2c0a 100644 --- a/libraries/botbuilder-ai/botbuilder/ai/qna/models/query_result.py +++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/query_result.py @@ -18,7 +18,7 @@ class QueryResult(Model): } def __init__(self, **kwargs): - super(QueryResult, self).__init__(**kwargs) + super().__init__(**kwargs) self.questions = kwargs.get("questions", None) self.answer = kwargs.get("answer", None) self.score = kwargs.get("score", None) 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 17fd2a2c8..f3c413618 100644 --- a/libraries/botbuilder-ai/botbuilder/ai/qna/models/query_results.py +++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/query_results.py @@ -25,6 +25,6 @@ def __init__( active_learning_enabled: The active learning enable flag. """ - super(QueryResults, self).__init__(**kwargs) + super().__init__(**kwargs) self.answers = answers self.active_learning_enabled = active_learning_enabled 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 aaed7fbca..b12c492c7 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 @@ -2,7 +2,9 @@ # Licensed under the MIT License. from copy import copy -from typing import List, Union +from typing import Any, List, Union +import json +import requests from aiohttp import ClientResponse, ClientSession @@ -109,7 +111,8 @@ def _hydrate_options(self, query_options: QnAMakerOptions) -> QnAMakerOptions: with the options passed as arguments into get_answers(). Return: ------- - QnAMakerOptions with options passed into constructor overwritten by new options passed into get_answers() + QnAMakerOptions with options passed into constructor overwritten + by new options passed into get_answers() rtype: ------ @@ -162,7 +165,7 @@ async def _query_qna_service( http_request_helper = HttpRequestUtils(self._http_client) - response: ClientResponse = await http_request_helper.execute_http_request( + response: Any = await http_request_helper.execute_http_request( url, question, self._endpoint, options.timeout ) @@ -200,14 +203,19 @@ async def _format_qna_result( self, result, options: QnAMakerOptions ) -> QueryResults: json_res = result + if isinstance(result, ClientResponse): json_res = await result.json() + if isinstance(result, requests.Response): + json_res = json.loads(result.text) + answers_within_threshold = [ {**answer, "score": answer["score"] / 100} for answer in json_res["answers"] if answer["score"] / 100 > options.score_threshold ] + sorted_answers = sorted( answers_within_threshold, key=lambda ans: ans["score"], reverse=True ) diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/utils/http_request_utils.py b/libraries/botbuilder-ai/botbuilder/ai/qna/utils/http_request_utils.py index c1d0035e5..977f839de 100644 --- a/libraries/botbuilder-ai/botbuilder/ai/qna/utils/http_request_utils.py +++ b/libraries/botbuilder-ai/botbuilder/ai/qna/utils/http_request_utils.py @@ -3,6 +3,8 @@ import json import platform +from typing import Any +import requests from aiohttp import ClientResponse, ClientSession, ClientTimeout @@ -12,9 +14,15 @@ class HttpRequestUtils: - """ HTTP request utils class. """ + """ HTTP request utils class. - def __init__(self, http_client: ClientSession): + Parameters: + ----------- + + http_client: Client to make HTTP requests with. Default client used in the SDK is `aiohttp.ClientSession`. + """ + + def __init__(self, http_client: Any): self._http_client = http_client async def execute_http_request( @@ -23,7 +31,7 @@ async def execute_http_request( payload_body: object, endpoint: QnAMakerEndpoint, timeout: float = None, - ) -> ClientResponse: + ) -> Any: """ Execute HTTP request. @@ -57,19 +65,16 @@ async def execute_http_request( headers = self._get_headers(endpoint) - if timeout: - # Convert miliseconds to seconds (as other BotBuilder SDKs accept timeout value in miliseconds) - # aiohttp.ClientSession units are in seconds - request_timeout = ClientTimeout(total=timeout / 1000) - - response: ClientResponse = await self._http_client.post( - request_url, - data=serialized_payload_body, - headers=headers, - timeout=request_timeout, + if isinstance(self._http_client, ClientSession): + response: ClientResponse = await self._make_request_with_aiohttp( + request_url, serialized_payload_body, headers, timeout + ) + elif self._is_using_requests_module(): + response: requests.Response = self._make_request_with_requests( + request_url, serialized_payload_body, headers, timeout ) else: - response: ClientResponse = await self._http_client.post( + response = await self._http_client.post( request_url, data=serialized_payload_body, headers=headers ) @@ -94,3 +99,42 @@ def _get_user_agent(self): user_agent = f"{package_user_agent} {platform_user_agent}" return user_agent + + def _is_using_requests_module(self) -> bool: + return (type(self._http_client).__name__ == "module") and ( + self._http_client.__name__ == "requests" + ) + + async def _make_request_with_aiohttp( + self, request_url: str, payload_body: str, headers: dict, timeout: float + ) -> ClientResponse: + if timeout: + # aiohttp.ClientSession's timeouts are in seconds + timeout_in_seconds = ClientTimeout(total=timeout / 1000) + + return await self._http_client.post( + request_url, + data=payload_body, + headers=headers, + timeout=timeout_in_seconds, + ) + + return await self._http_client.post( + request_url, data=payload_body, headers=headers + ) + + def _make_request_with_requests( + self, request_url: str, payload_body: str, headers: dict, timeout: float + ) -> requests.Response: + if timeout: + # requests' timeouts are in seconds + timeout_in_seconds = timeout / 1000 + + return self._http_client.post( + request_url, + data=payload_body, + headers=headers, + timeout=timeout_in_seconds, + ) + + return self._http_client.post(request_url, data=payload_body, headers=headers) diff --git a/libraries/botbuilder-ai/tests/qna/test_qna.py b/libraries/botbuilder-ai/tests/qna/test_qna.py index 309967839..03e176d6e 100644 --- a/libraries/botbuilder-ai/tests/qna/test_qna.py +++ b/libraries/botbuilder-ai/tests/qna/test_qna.py @@ -5,6 +5,7 @@ # pylint: disable=too-many-lines import json +import requests from os import path from typing import List, Dict import unittest @@ -19,7 +20,8 @@ QueryResult, QnARequestContext, ) -from botbuilder.ai.qna.utils import QnATelemetryConstants +from botbuilder.ai.qna.utils import HttpRequestUtils, QnATelemetryConstants +from botbuilder.ai.qna.models import GenerateAnswerRequestBody from botbuilder.core import BotAdapter, BotTelemetryClient, TurnContext from botbuilder.core.adapters import TestAdapter from botbuilder.schema import ( @@ -164,6 +166,27 @@ 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_using_requests_module(self): + question: str = "how do I clean the stove?" + response_path: str = "ReturnsAnswer.json" + response_json = QnaApplicationTest._get_json_for_file(response_path) + + qna = QnAMaker( + endpoint=QnaApplicationTest.tests_endpoint, http_client=requests + ) + context = QnaApplicationTest._get_context(question, TestAdapter()) + + with patch("requests.post", return_value=response_json): + result = await qna.get_answers_raw(context) + answers = result.answers + + self.assertIsNotNone(result) + self.assertEqual(1, len(answers)) + self.assertEqual( + "BaseCamp: You can use a damp rag to clean around the Power Pack", + answers[0].answer, + ) + async def test_returns_answer_using_options(self): # Arrange question: str = "up" @@ -254,6 +277,39 @@ async def test_returns_answer_with_timeout(self): options.timeout, qna._generate_answer_helper.options.timeout ) + async def test_returns_answer_using_requests_module_with_no_timeout(self): + url = f"{QnaApplicationTest._host}/knowledgebases/{QnaApplicationTest._knowledge_base_id}/generateAnswer" + question = GenerateAnswerRequestBody( + question="how do I clean the stove?", + top=1, + score_threshold=0.3, + strict_filters=[], + context=None, + qna_id=None, + is_test=False, + ranker_type="Default" + ) + response_path = "ReturnsAnswer.json" + response_json = QnaApplicationTest._get_json_for_file(response_path) + + http_request_helper = HttpRequestUtils(requests) + + with patch("requests.post", return_value=response_json): + result = await http_request_helper.execute_http_request( + url, + question, + QnaApplicationTest.tests_endpoint, + timeout=None + ) + answers = result["answers"] + + self.assertIsNotNone(result) + self.assertEqual(1, len(answers)) + self.assertEqual( + "BaseCamp: You can use a damp rag to clean around the Power Pack", + answers[0]["answer"], + ) + async def test_telemetry_returns_answer(self): # Arrange question: str = "how do I clean the stove?"