1
1
# Copyright (c) Microsoft Corporation. All rights reserved.
2
2
# Licensed under the MIT License.
3
3
4
+ from aiohttp import ClientSession , ClientTimeout
5
+
4
6
from botbuilder .schema import Activity
5
7
from botbuilder .core import BotTelemetryClient , NullTelemetryClient , TurnContext
6
8
from copy import copy
7
- import json , requests
8
- from typing import Dict , List , NamedTuple
9
+ import json , platform , requests
10
+ from typing import Dict , List , NamedTuple , Union
9
11
10
12
from .metadata import Metadata
11
13
from .query_result import QueryResult
15
17
from .qna_telemetry_constants import QnATelemetryConstants
16
18
from .qnamaker_trace_info import QnAMakerTraceInfo
17
19
20
+ from .. import __title__ , __version__
21
+
18
22
QNAMAKER_TRACE_NAME = 'QnAMaker'
19
23
QNAMAKER_TRACE_LABEL = 'QnAMaker Trace'
20
24
QNAMAKER_TRACE_TYPE = 'https://www.qnamaker.ai/schemas/trace'
@@ -31,7 +35,8 @@ class QnAMaker(QnAMakerTelemetryClient):
31
35
def __init__ (
32
36
8000
self ,
33
37
endpoint : QnAMakerEndpoint ,
34
- options : QnAMakerOptions = None ,
38
+ options : QnAMakerOptions = None ,
39
+ http_client : ClientSession = None ,
35
40
telemetry_client : BotTelemetryClient = None ,
36
41
log_personal_information : bool = None
37
42
):
@@ -41,13 +46,16 @@ def __init__(
41
46
if endpoint .host .endswith ('v2.0' ):
42
47
raise ValueError ('v2.0 of QnA Maker service is no longer supported in the Bot Framework. Please upgrade your QnA Maker service at www.qnamaker.ai.' )
43
48
44
- self ._endpoint = endpoint
49
+ self ._endpoint : str = endpoint
45
50
self ._is_legacy_protocol : bool = self ._endpoint .host .endswith ('v3.0' )
46
51
47
- self ._options : QnAMakerOptions = options or QnAMakerOptions ()
52
+ self ._options = options or QnAMakerOptions ()
48
53
self ._validate_options (self ._options )
49
54
50
- self ._telemetry_client = telemetry_client or NullTelemetryClient ()
55
+ instance_timeout = ClientTimeout (total = self ._options .timeout / 1000 )
56
+ self ._req_client = http_client or ClientSession (timeout = instance_timeout )
57
+
58
+ self ._telemetry_client : Union [BotTelemetryClient , NullTelemetryClient ] = telemetry_client or NullTelemetryClient ()
51
59
self ._log_personal_information = log_personal_information or False
52
60
53
61
@property
@@ -186,11 +194,10 @@ async def get_answers(
186
194
:rtype: [QueryResult]
187
195
"""
188
196
189
-
190
197
hydrated_options = self ._hydrate_options (options )
191
198
self ._validate_options (hydrated_options )
192
199
193
- result = self ._query_qna_service (context . activity , hydrated_options )
200
+ result = await self ._query_qna_service (context , hydrated_options )
194
201
195
202
await self ._emit_trace_info (context , result , hydrated_options )
196
203
@@ -211,6 +218,9 @@ def _validate_options(self, options: QnAMakerOptions):
211
218
212
219
if not options .strict_filters :
213
220
options .strict_filters = []
221
+
222
+ if not options .timeout :
223
+ options .timeout = 100000
214
224
215
225
def _hydrate_options (self , query_options : QnAMakerOptions ) -> QnAMakerOptions :
216
226
"""
@@ -235,29 +245,38 @@ def _hydrate_options(self, query_options: QnAMakerOptions) -> QnAMakerOptions:
235
245
236
246
if (len (query_options .strict_filters ) > 0 ):
237
247
hydrated_options .strict_filters = query_options .strict_filters
248
+
249
+ if (query_options .timeout != hydrated_options .timeout and query_options .timeout ):
250
+ hydrated_options .timeout = query_options .timeout
238
251
239
252
return hydrated_options
240
253
241
- def _query_qna_service (self , message_activity : Activity , options : QnAMakerOptions ) -> [QueryResult ]:
254
+ async def _query_qna_service (self , turn_context : TurnContext , options : QnAMakerOptions ) -> [QueryResult ]:
242
255
url = f'{ self ._endpoint .host } /knowledgebases/{ self ._endpoint .knowledge_base_id } /generateAnswer'
243
-
244
256
question = {
245
- 'question' : message_activity .text ,
257
+ 'question' : turn_context . activity .text ,
246
258
'top' : options .top ,
247
259
'scoreThreshold' : options .score_threshold ,
248
260
'strictFilters' : options .strict_filters
249
261
}
250
-
251
262
serialized_content = json .dumps (question )
252
-
253
263
headers = self ._get_headers ()
254
264
255
- response = requests .post (url , data = serialized_content , headers = headers )
256
-
257
- result = self ._format_qna_result (response , options )
258
-
265
+ # Convert miliseconds to seconds (as other BotBuilder SDKs accept timeout value in miliseconds)
266
+ # aiohttp.ClientSession units are in seconds
267
+ timeout = ClientTimeout (total = options .timeout / 1000 )
268
+
269
+ response = await self ._req_client .post (
270
+ url ,
271
+ data = serialized_content ,
272
+ headers = headers ,
273
+ timeout = timeout
274
+ )
275
+
276
+ result = await self ._format_qna_result (response , options )
277
+
259
278
return result
260
-
279
+
261
280
async def _emit_trace_info (self , turn_context : TurnContext , result : [QueryResult ], options : QnAMakerOptions ):
262
281
trace_info = QnAMakerTraceInfo (
263
282
message = turn_context .activity ,
@@ -278,12 +297,13 @@ async def _emit_trace_info(self, turn_context: TurnContext, result: [QueryResult
278
297
279
298
await turn_context .send_activity (trace_activity )
280
299
281
- def _format_qna_result (self , qna_result : requests . Response , options : QnAMakerOptions ) -> [QueryResult ]:
282
- result = qna_result .json ()
300
+ async def _format_qna_result (self , result , options : QnAMakerOptions ) -> [QueryResult ]:
301
+ json_res = await result .json ()
283
302
284
303
answers_within_threshold = [
285
- { ** answer ,'score' : answer ['score' ]/ 100 } for answer in result ['answers' ]
286
- if answer ['score' ]/ 100 > options .score_threshold
304
+ { ** answer ,'score' : answer ['score' ]/ 100 }
305
+ if answer ['score' ]/ 100 > options .score_threshold
306
+ else {** answer } for answer in json_res ['answers' ]
287
307
]
288
308
sorted_answers = sorted (answers_within_threshold , key = lambda ans : ans ['score' ], reverse = True )
289
309
@@ -298,13 +318,24 @@ def _format_qna_result(self, qna_result: requests.Response, options: QnAMakerOpt
298
318
return answers_as_query_results
299
319
300
320
def _get_headers (self ):
301
- headers = { 'Content-Type' : 'application/json' }
321
+ headers = {
322
+ 'Content-Type' : 'application/json' ,
323
+ 'User-Agent' : self .get_user_agent ()
324
+ }
302
325
303
326
if self ._is_legacy_protocol :
304
327
headers ['Ocp-Apim-Subscription-Key' ] = self ._endpoint .endpoint_key
305
328
else :
306
329
headers ['Authorization' ] = f'EndpointKey { self ._endpoint .endpoint_key } '
307
330
308
- # need user-agent header
309
-
310
331
return headers
332
+
333
+ def get_user_agent (self ):
334
+ package_user_agent = f'{ __title__ } /{ __version__ } '
335
+ uname = platform .uname ()
336
+ os_version = f'{ uname .machine } -{ uname .system } -{ uname .version } '
337
+ py_version = f'Python,Version={ platform .python_version ()} '
338
+ platform_user_agent = f'({ os_version } ; { py_version } )'
339
+ user_agent = f'{ package_user_agent } { platform_user_agent } '
340
+
341
+ return user_agent
0 commit comments