2
2
# Licensed under the MIT License.
3
3
4
4
import json
5
- import aiounittest
5
+ import aiounittest , unittest , requests
6
+ from msrest import Deserializer
7
+ from os import path
8
+ from requests .models import Response
6
9
from typing import List , Tuple
7
10
from uuid import uuid4
8
- from botbuilder .schema import Activity , ChannelAccount , ResourceResponse , ConversationAccount
11
+ from unittest .mock import Mock , patch , MagicMock
12
+ from asyncio import Future
13
+
14
+
15
+
16
+ from botbuilder .ai .qna import Metadata , QnAMakerEndpoint , QnAMaker , QnAMakerOptions , QnATelemetryConstants , QueryResult
9
17
from botbuilder .core import BotAdapter , BotTelemetryClient , NullTelemetryClient , TurnContext
10
- from botbuilder .ai .qna import QnAMakerEndpoint , QnAMaker , QnAMakerOptions , QnATelemetryConstants
11
-
12
-
13
- # DELETE YO
14
- ACTIVITY = Activity (id = '1234' ,
15
- type = 'message' ,
16
- text = 'up' ,
17
- from_property = ChannelAccount (id = 'user' , name = 'User Name' ),
18
- recipient = ChannelAccount (id = 'bot' , name = 'Bot Name' ),
19
- conversation = ConversationAccount (id = 'convo' , name = 'Convo Name' ),
20
- channel_id = 'UnitTest' ,
21
- service_url = 'https://example.org'
22
- )
23
-
24
- class SimpleAdapter (BotAdapter ):
25
- async def send_activities (self , context , activities ):
26
- responses = []
27
- for (idx , activity ) in enumerate (activities ):
28
- responses .append (ResourceResponse (id = '5678' ))
29
- return responses
30
-
31
- async def update_activity (self , context , activity ):
32
- assert context is not None
33
- assert activity is not None
34
-
35
- async def delete_activity (self , context , reference ):
36
- assert context is not None
37
- assert reference is not None
38
- assert reference .activity_id == '1234'
18
+ from botbuilder .core .adapters import TestAdapter
19
+ from botbuilder .schema import Activity , ActivityTypes , ChannelAccount , ResourceResponse , ConversationAccount
20
+
21
+ # from botbuilder.ai.qna import (
22
+ # Metadata,
23
+ # QnAMakerEndpoint,
24
+ # QnAMaker,
25
+ # QnAMakerOptions,
26
+ # QnATelemetryConstants,
27
+ # QueryResult
28
+ # )
29
+ # from botbuilder.core import (BotAdapter,
30
+ # BotTelemetryClient,
31
+ # NullTelemetryClient,
32
+ # TurnContext
33
+ # )
34
+ # from botbuilder.core.adapters import TestAdapter
35
+ # from botbuilder.schema import (Activity,
36
+ # ActivityTypes,
37
+ # ChannelAccount,
38
+ # ResourceResponse,
39
+ # ConversationAccount
40
+ # )
41
+
39
42
40
43
class QnaApplicationTest (aiounittest .AsyncTestCase ):
44
+ _knowledge_base_id : str = 'a090f9f3-2f8e-41d1-a581-4f7a49269a0c'
45
+ _endpoint_key : str = '4a439d5b-163b-47c3-b1d1-168cc0db5608'
46
+ _host : str = 'https://ashleyNlpBot1-qnahost.azurewebsites.net/qnamaker'
47
+
48
+ tests_endpoint = QnAMakerEndpoint (_knowledge_base_id , _endpoint_key , _host )
49
+
50
+ def __init__ (self , * args , ** kwargs ):
51
+ super ().__init__ (* args , ** kwargs )
52
+ # self._mocked_results: QueryResult(
53
+
54
+ # )
55
+
56
+ def test_qnamaker_construction (self ):
57
+ # Arrange
58
+ endpoint = self .tests_endpoint
59
+
60
+ # Act
61
+ qna = QnAMaker (endpoint )
62
+ endpoint = qna ._endpoint
63
+
64
+ # Assert
65
+ self .assertEqual ('a090f9f3-2f8e-41d1-a581-4f7a49269a0c' , endpoint .knowledge_base_id )
66
+ self .assertEqual ('4a439d5b-163b-47c3-b1d1-168cc0db5608' , endpoint .endpoint_key )
67
+ self .assertEqual ('https://ashleyNlpBot1-qnahost.azurewebsites.net/qnamaker' , endpoint .host )
68
+
69
+
70
+ def test_endpoint_with_empty_kbid (self ):
71
+ empty_kbid = ''
72
+
73
+ with self .assertRaises (TypeError ):
74
+ QnAMakerEndpoint (
75
+ empty_kbid ,
76
+ self ._endpoint_key ,
77
+ self ._host
78
+ )
79
+
80
+ def test_endpoint_with_empty_endpoint_key (self ):
81
+ empty_endpoint_key = ''
82
+
83
+ with self .assertRaises (TypeError ):
84
+ QnAMakerEndpoint (
85
+ self ._knowledge_base_id ,
86
+ empty_endpoint_key ,
87
+ self ._host
88
+ )
89
+
90
+ def test_endpoint_with_emptyhost (self ):
91
+ with self .assertRaises (TypeError ):
92
+ QnAMakerEndpoint (
93
+ self ._knowledge_base_id ,
94
+ self ._endpoint_key ,
95
+ ''
96
+ )
97
+
98
+ def test_qnamaker_with_none_endpoint (self ):
99
+ with self .assertRaises (TypeError ):
100
+ QnAMaker (None )
101
+
102
+ def test_v2_legacy_endpoint (self ):
103
+ v2_hostname = 'https://westus.api.cognitive.microsoft.com/qnamaker/v2.0'
104
+
105
+ v2_legacy_endpoint = QnAMakerEndpoint (
106
+ self ._knowledge_base_id ,
107
+ self ._endpoint_key ,
108
+ v2_hostname
109
+ )
110
+
111
+ with self .assertRaises (ValueError ):
112
+ QnAMaker (v2_legacy_endpoint )
113
+
114
+ def test_legacy_protocol (self ):
115
+ v3_hostname = 'https://westus.api.cognitive.microsoft.com/qnamaker/v3.0'
116
+ v3_legacy_endpoint = QnAMakerEndpoint (
117
+ self ._knowledge_base_id ,
118
+ self ._endpoint_key ,
119
+ v3_hostname
120
+ )
121
+ legacy_qna = QnAMaker (v3_legacy_endpoint )
122
+ is_legacy = True
123
+
124
+ v4_hostname = 'https://UpdatedNonLegacyQnaHostName.azurewebsites.net/qnamaker'
125
+ nonlegacy_endpoint = QnAMakerEndpoint (
126
+ self ._knowledge_base_id ,
127
+ self ._endpoint_key ,
128
+ v4_hostname
129
+ )
130
+ v4_qna = QnAMaker (nonlegacy_endpoint )
131
+
132
+ self .assertEqual (is_legacy , legacy_qna ._is_legacy_protocol )
133
+ self .assertNotEqual (is_legacy , v4_qna ._is_legacy_protocol )
134
+
135
+ def test_set_default_options_with_no_options_arg (self ):
136
+ qna_without_options = QnAMaker (self .tests_endpoint )
137
+
138
+ options = qna_without_options ._options
139
+
140
+ default_threshold = 0.3
141
+ default_top = 1
142
+ default_strict_filters = []
143
+
144
+ self .assertEqual (default_threshold , options .score_threshold )
145
+ self .assertEqual (default_top , options .top )
146
+ self .assertEqual (default_strict_filters , options .strict_filters )
147
+
148
+ def test_options_passed_to_ctor (self ):
149
+ options = QnAMakerOptions (
150
+ score_threshold = 0.8 ,
151
+ timeout = 9000 ,
152
+ top = 5 ,
153
+ strict_filters = [Metadata ('movie' , 'disney' )]
154
+ )
155
+
156
+ qna_with_options = QnAMaker (self .tests_endpoint , options )
157
+ actual_options = qna_with_options ._options
158
+
159
+ expected_threshold = 0.8
160
+ expected_timeout = 9000
161
+ expected_top = 5
162
+ expected_strict_filters = [Metadata ('movie' , 'disney' )]
163
+
164
+ self .assertEqual (expected_threshold , actual_options .score_threshold )
165
+ self .assertEqual (expected_timeout , actual_options .timeout )
166
+ self .assertEqual (expected_top , actual_options .top )
167
+ self .assertEqual (expected_strict_filters [0 ].name , actual_options .strict_filters [0 ].name )
168
+ self .assertEqual (expected_strict_filters [0 ].value , actual_options .strict_filters [0 ].value )
169
+
170
+
171
+ async def test_returns_answer_using_options (self ):
172
+ # Arrange
173
+ question : str = 'up'
174
+ response_path : str = 'AnswerWithOptions.json'
175
+ options = QnAMakerOptions (
176
+ score_threshold = 0.8 ,
177
+ top = 5 ,
178
+ strict_filters = [{
179
+ 'name' : 'movie' ,
180
+ 'value' : 'disney'
181
+ }]
182
+
10000
)
183
+
184
+ # Act
185
+ result = await QnaApplicationTest ._get_service_result (
186
+ question ,
187
+ response_path ,
188
+ options = options
189
+ )
190
+
191
+ first_answer = result ['answers' ][0 ]
192
+ has_at_least_1_ans = True
193
+ first_metadata = first_answer ['metadata' ][0 ]
194
+
195
+ # Assert
196
+ self .assertIsNotNone (result )
197
+ self .assertEqual (has_at_least_1_ans , len (result ['answers' ]) >= 1 and len (result ['answers' ]) <= options .top )
198
+ self .assertTrue (question in first_answer ['questions' ])
199
+ self .assertTrue (first_answer ['answer' ])
200
+ self .assertEqual ('is a movie' , first_answer ['answer' ])
201
+ self .assertTrue (first_answer ['score' ] >= options .score_threshold )
202
+ self .assertEqual ('movie' , first_metadata ['name' ])
203
+ self .assertEqual ('disney' , first_metadata ['value' ])
204
+
205
+ @classmethod
206
+ async def _get_service_result (
207
+ cls ,
208
+ utterance : str ,
209
+ response_file : str ,
210
+ bot_adapter : BotAdapter = TestAdapter (),
211
+ options : QnAMakerOptions = None
212
+ ) -> [QueryResult ]:
213
+ response_json = QnaApplicationTest ._get_json_for_file (response_file )
214
+
215
+ qna = QnAMaker (QnaApplicationTest .tests_endpoint )
216
+ context = QnaApplicationTest ._get_context (utterance , bot_adapter )
217
+ response = Mock (spec = Response )
218
+ response .status_code = 200
219
+ response .headers = {}
220
+ response .reason = ''
221
+
222
+ with patch ('requests.post' , return_value = response ):
223
+ with patch ('botbuilder.ai.qna.QnAMaker._format_qna_result' , return_value = response_json ):
224
+ result = await qna .get_answers (context , options )
225
+
226
+ return result
227
+
228
+ @classmethod
229
+ def _get_json_for_file (cls , response_file : str ) -> object :
230
+ curr_dir = path .dirname (path .abspath (__file__ ))
231
+ response_path = path .join (curr_dir , "test_data" , response_file )
232
+
233
+ with open (response_path , "r" , encoding = "utf-8-sig" ) as f :
234
+ response_str = f .read ()
235
+ response_json = json .loads (response_str )
236
+
237
+ return response_json
238
+
239
+ @staticmethod
240
+ def _get_context (utterance : str , bot_adapter : BotAdapter ) -> TurnContext :
241
+ test_adapter = TestAdapter ()
242
+ activity = Activity (
243
+ type = ActivityTypes .message ,
244
+ text = utterance ,
245
+ conversation = ConversationAccount (),
246
+ recipient = ChannelAccount (),
247
+ from_property = ChannelAccount (),
248
+ )
249
+
250
+ return TurnContext (test_adapter , activity )
251
+
252
+
253
+
254
+
255
+
256
+
257
+
258
+
259
+
260
+
41
261
42
- async def test_initial_test (self ):
43
- pass
0 commit comments