Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion libraries/botbuilder-ai/botbuilder/ai/qna/models/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
------
Expand Down Expand Up @@ -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
)

Expand Down Expand Up @@ -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
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import json
import platform
from typing import Any
import requests

from aiohttp import ClientResponse, ClientSession, ClientTimeout

Expand All @@ -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(
Expand All @@ -23,7 +31,7 @@ async def execute_http_request(
payload_body: object,
endpoint: QnAMakerEndpoint,
timeout: float = None,
) -> ClientResponse:
) -> Any:
"""
Execute HTTP request.

Expand Down Expand Up @@ -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
)

Expand All @@ -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)
58 changes: 57 additions & 1 deletion libraries/botbuilder-ai/tests/qna/test_qna.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# pylint: disable=too-many-lines

import json
import requests
from os import path
from typing import List, Dict
import unittest
Expand All @@ -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 (
Expand Down Expand Up @@ -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"
Expand Down Expand 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?"
Expand Down