8000 Allow requests module as QnAMaker's HTTP client (#1369) · itsmokha/botbuilder-python@c1e98de · GitHub
[go: up one dir, main page]

Skip to content

Commit c1e98de

Browse files
authored
Allow requests module as QnAMaker's HTTP client (microsoft#1369)
* Allow QnA to make reqs using requests library * Added unit test: test requests http client w/o timeout * Reordered methods to the default, more-frequently used method first * Linted
1 parent 1ae1836 commit c1e98de

File tree

7 files changed

+130
-22
lines changed

7 files changed

+130
-22
lines changed

libraries/botbuilder-ai/botbuilder/ai/qna/models/prompt.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class Prompt(Model):
1515
}
1616

1717
def __init__(self, **kwargs):
18-
super(Prompt, self).__init__(**kwargs)
18+
super().__init__(**kwargs)
1919
self.display_order = kwargs.get("display_order", None)
2020
self.qna_id = kwargs.get("qna_id", None)
2121
self.display_text = kwargs.get("display_text", None)

libraries/botbuilder-ai/botbuilder/ai/qna/models/qna_response_context.py

Lines changed: 1 addition & 1 deletion
10000
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,6 @@ def __init__(self, **kwargs):
2626
2727
"""
2828

29-
super(QnAResponseContext, self).__init__(**kwargs)
29+
super().__init__(**kwargs)
3030
self.is_context_only = kwargs.get("is_context_only", None)
3131
self.prompts = kwargs.get("prompts", None)

libraries/botbuilder-ai/botbuilder/ai/qna/models/query_result.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class QueryResult(Model):
1818
}
1919

2020
def __init__(self, **kwargs):
21-
super(QueryResult, self).__init__(**kwargs)
21+
super().__init__(**kwargs)
2222
self.questions = kwargs.get("questions", None)
2323
self.answer = kwargs.get("answer", None)
2424
self.score = kwargs.get("score", None)

libraries/botbuilder-ai/botbuilder/ai/qna/models/query_results.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@ def __init__(
2525
2626
active_learning_enabled: The active learning enable flag.
2727
"""
28-
super(QueryResults, self).__init__(**kwargs)
28+
super().__init__(**kwargs)
2929
self.answers = answers
3030
self.active_learning_enabled = active_learning_enabled

libraries/botbuilder-ai/botbuilder/ai/qna/utils/generate_answer_utils.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
# Licensed under the MIT License.
33

44
from copy import copy
5-
from typing import List, Union
5+
from typing import Any, List, Union
6+
import json
7+
import requests
68

79
from aiohttp import ClientResponse, ClientSession
810

@@ -109,7 +111,8 @@ def _hydrate_options(self, query_options: QnAMakerOptions) -> QnAMakerOptions:
109111
with the options passed as arguments into get_answers().
110112
Return:
111113
-------
112-
QnAMakerOptions with options passed into constructor overwritten by new options passed into get_answers()
114+
QnAMakerOptions with options passed into constructor overwritten
115+
by new options passed into get_answers()
113116
114117
rtype:
115118
------
@@ -162,7 +165,7 @@ async def _query_qna_service(
162165

163166
http_request_helper = HttpRequestUtils(self._http_client)
164167

165-
response: ClientResponse = await http_request_helper.execute_http_request(
168+
response: Any = await http_request_helper.execute_http_request(
166169
url, question, self._endpoint, options.timeout
167170
)
168171

@@ -200,14 +203,19 @@ async def _format_qna_result(
200203
self, result, options: QnAMakerOptions
201204
) -> QueryResults:
202205
json_res = result
206+
203207
if isinstance(result, ClientResponse):
204208
json_res = await result.json()
205209

210+
if isinstance(result, requests.Response):
211+
json_res = json.loads(result.text)
212+
206213
answers_within_threshold = [
207214
{**answer, "score": answer["score"] / 100}
208215
for answer in json_res["answers"]
209216
if answer["score"] / 100 > options.score_threshold
210217
]
218+
211219
sorted_answers = sorted(
212220
answers_within_threshold, key=lambda ans: ans["score"], reverse=True
213221
)

libraries/botbuilder-ai/botbuilder/ai/qna/utils/http_request_utils.py

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
import json
55
import platform
6+
from typing import Any
7+
import requests
68

79
from aiohttp import ClientResponse, ClientSession, ClientTimeout
810

@@ -12,9 +14,15 @@
1214

1315

1416
class HttpRequestUtils:
15-
""" HTTP request utils class. """
17+
""" HTTP request utils class.
1618
17-
def __init__(self, http_client: ClientSession):
19+
Parameters:
20+
-----------
21+
22+
http_client: Client to make HTTP requests with. Default client used in the SDK is `aiohttp.ClientSession`.
23+
"""
24+
25+
def __init__(self, http_client: Any):
1826
self._http_client = http_client
1927

2028
async def execute_http_request(
@@ -23,7 +31,7 @@ async def execute_http_request(
2331
payload_body: object,
2432
endpoint: QnAMakerEndpoint,
2533
timeout: float = None,
26-
) -> ClientResponse:
34+
) -> Any:
2735
"""
2836
Execute HTTP request.
2937
@@ -57,19 +65,16 @@ async def execute_http_request(
5765

5866
headers = self._get_headers(endpoint)
5967

60-
if timeout:
61-
# Convert miliseconds to seconds (as other BotBuilder SDKs accept timeout value in miliseconds)
62-
# aiohttp.ClientSession units are in seconds
63-
request_timeout = ClientTimeout(total=timeout / 1000)
64-
65-
response: ClientResponse = await self._http_client.post(
66-
request_url,
67-
data=serialized_payload_body,
68-
headers=headers,
69-
timeout=request_timeout,
68+
if isinstance(self._http_client, ClientSession):
69+
response: ClientResponse = await self._make_request_with_aiohttp(
70+
request_url, serialized_payload_body, headers, timeout
71+
)
72+
elif self._is_using_requests_module():
73+
response: requests.Response = self._make_request_with_requests(
74+
request_url, serialized_payload_body, headers, timeout
7075
)
7176
else:
72-
response: ClientResponse = await self._http_client.post(
77+
response = await self._http_client.post(
7378
request_url, data=serialized_payload_body, headers=headers
7479
)
7580

@@ -94,3 +99,42 @@ def _get_user_agent(self):
9499
user_agent = f"{package_user_agent} {platform_user_agent}"
95100

96101
return user_agent
102+
103+
def _is_using_requests_module(self) -> bool:
104+
return (type(self._http_client).__name__ == "module") and (
105+
self._http_client.__name__ == "requests"
106+
)
107+
108+
async def _make_request_with_aiohttp(
109+
self, request_url: str, payload_body: str, headers: dict, timeout: float
110+
) -> ClientResponse:
111+
if timeout:
112+
# aiohttp.ClientSession's timeouts are in seconds
113+
timeout_in_seconds = ClientTimeout(total=timeout / 1000)
114+
115+
return await self._http_client.post(
116+
request_url,
117+
data=payload_body,
118+
headers=headers,
119+
timeout=timeout_in_seconds,
120+
)
121+
122+
return await self._http_client.post(
123+
request_url, data=payload_body, headers=headers
124+
)
125+
126+
def _make_request_with_requests(
127+
self, request_url: str, payload_body: str, headers: dict, timeout: float
128+
) -> requests.Response:
129+
if timeout:
130+
# requests' timeouts are in seconds
131+
timeout_in_seconds = timeout / 1000
132+
133+
return self._http_client.post(
134+
request_url,
135+
data=payload_body,
136+
headers=headers,
137+
timeout=timeout_in_seconds,
138+
)
139+
140+
return self._http_client.post(request_url, data=payload_body, headers=headers)

libraries/botbuilder-ai/tests/qna/test_qna.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# pylint: disable=too-many-lines
66

77
import json
8+
import requests
89
from os import path
910
from typing import List, Dict
1011
import unittest
@@ -19,7 +20,8 @@
1920
QueryResult,
2021
QnARequestContext,
2122
)
22-
from botbuilder.ai.qna.utils import QnATelemetryConstants
23+
from botbuilder.ai.qna.utils import HttpRequestUtils, QnATelemetryConstants
24+
from botbuilder.ai.qna.models import GenerateAnswerRequestBody
2325
from botbuilder.core import BotAdapter, BotTelemetryClient, TurnContext
2426
from botbuilder.core.adapters import TestAdapter
2527
from botbuilder.schema import (
@@ -164,6 +166,27 @@ async def test_active_learning_enabled_status(self):
164166
self.assertEqual(1, len(result.answers))
165167
self.assertFalse(result.active_learning_enabled)
166168

169+
async def test_returns_answer_using_requests_module(self):
170+
question: str = "how do I clean the stove?"
171+
response_path: str = "ReturnsAnswer.json"
172+
response_json = QnaApplicationTest._get_json_for_file(response_path)
173+
174+
qna = QnAMaker(
175+
endpoint=QnaApplicationTest.tests_endpoint, http_client=requests
176+
)
177+
context = QnaApplicationTest._get_context(question, TestAdapter())
178+
179+
with patch("requests.post", return_value=response_json):
180+
result = await qna.get_answers_raw(context)
181+
answers = result.answers
182+
183+
self.assertIsNotNone(result)
184+
self.assertEqual(1, len(answers))
185+
self.assertEqual(
186+
"BaseCamp: You can use a damp rag to clean around the Power Pack",
187+
answers[0].answer,
188+
)
189+
167190
async def test_returns_answer_using_options(self):
168191
# Arrange
169192
question: str = "up"
@@ -254,6 +277,39 @@ async def test_returns_answer_with_timeout(self):
254277
options.timeout, qna._generate_answer_helper.options.timeout
255278
)
256279

280+
async def test_returns_answer_using_requests_module_with_no_timeout(self):
281+
url = f"{QnaApplicationTest._host}/knowledgebases/{QnaApplicationTest._knowledge_base_id}/generateAnswer"
282+
question = GenerateAnswerRequestBody(
283+
question="how do I clean the stove?",
284+
top=1,
285+
score_threshold=0.3,
286+
strict_filters=[],
287+
context=None,
288+
qna_id=None,
289+
is_test=False,
290+
ranker_type="Default"
291+
)
292+
response_path = "ReturnsAnswer.json"
293+
response_json = QnaApplicationTest._get_json_for_file(response_path)
294+
295+
http_request_helper = HttpRequestUtils(requests)
296+
297+
with patch("requests.post", return_value=response_json):
298+
result = await http_request_helper.execute_http_request(
299+
url,
300+
question,
301+
QnaApplicationTest.tests_endpoint,
302+
timeout=None
303+
)
304+
answers = result["answers"]
305+
306+
self.assertIsNotNone(result)
307+
self.assertEqual(1, len(answers))
308+
self.assertEqual(
309+
"BaseCamp: You can use a damp rag to clean around the Power Pack",
310+
answers[0]["answer"],
311+
)
312+
257313
async def test_telemetry_returns_answer(self):
258314
# Arrange
259315
question: str = "how do I clean the stove?"

0 commit comments

Comments
 (0)
0