microsoft-agents-hosting-dialogs 0.10.0.dev2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. microsoft_agents/hosting/dialogs/__init__.py +76 -0
  2. microsoft_agents/hosting/dialogs/_component_registration.py +30 -0
  3. microsoft_agents/hosting/dialogs/_telemetry_client.py +78 -0
  4. microsoft_agents/hosting/dialogs/choices/__init__.py +38 -0
  5. microsoft_agents/hosting/dialogs/choices/channel.py +121 -0
  6. microsoft_agents/hosting/dialogs/choices/choice_factory.py +262 -0
  7. microsoft_agents/hosting/dialogs/choices/choice_recognizer.py +148 -0
  8. microsoft_agents/hosting/dialogs/choices/find.py +242 -0
  9. microsoft_agents/hosting/dialogs/choices/models/__init__.py +23 -0
  10. microsoft_agents/hosting/dialogs/choices/models/choice.py +14 -0
  11. microsoft_agents/hosting/dialogs/choices/models/choice_factory_options.py +13 -0
  12. microsoft_agents/hosting/dialogs/choices/models/find_choices_options.py +28 -0
  13. microsoft_agents/hosting/dialogs/choices/models/find_values_options.py +31 -0
  14. microsoft_agents/hosting/dialogs/choices/models/found_choice.py +22 -0
  15. microsoft_agents/hosting/dialogs/choices/models/found_value.py +20 -0
  16. microsoft_agents/hosting/dialogs/choices/models/list_style.py +15 -0
  17. microsoft_agents/hosting/dialogs/choices/models/model_result.py +16 -0
  18. microsoft_agents/hosting/dialogs/choices/models/sorted_value.py +16 -0
  19. microsoft_agents/hosting/dialogs/choices/models/token.py +20 -0
  20. microsoft_agents/hosting/dialogs/choices/tokenizer.py +92 -0
  21. microsoft_agents/hosting/dialogs/component_dialog.py +284 -0
  22. microsoft_agents/hosting/dialogs/dialog.py +198 -0
  23. microsoft_agents/hosting/dialogs/dialog_component_registration.py +52 -0
  24. microsoft_agents/hosting/dialogs/dialog_container.py +31 -0
  25. microsoft_agents/hosting/dialogs/dialog_context.py +426 -0
  26. microsoft_agents/hosting/dialogs/dialog_extensions.py +201 -0
  27. microsoft_agents/hosting/dialogs/dialog_manager.py +189 -0
  28. microsoft_agents/hosting/dialogs/dialog_manager_result.py +17 -0
  29. microsoft_agents/hosting/dialogs/dialog_set.py +174 -0
  30. microsoft_agents/hosting/dialogs/dialog_state.py +20 -0
  31. microsoft_agents/hosting/dialogs/memory/__init__.py +24 -0
  32. microsoft_agents/hosting/dialogs/memory/component_memory_scopes_base.py +14 -0
  33. microsoft_agents/hosting/dialogs/memory/component_path_resolvers_base.py +15 -0
  34. microsoft_agents/hosting/dialogs/memory/dialog_path.py +33 -0
  35. microsoft_agents/hosting/dialogs/memory/dialog_state_manager.py +563 -0
  36. microsoft_agents/hosting/dialogs/memory/dialog_state_manager_configuration.py +11 -0
  37. microsoft_agents/hosting/dialogs/memory/path_resolver_base.py +8 -0
  38. microsoft_agents/hosting/dialogs/memory/path_resolvers/__init__.py +19 -0
  39. microsoft_agents/hosting/dialogs/memory/path_resolvers/alias_path_resolver.py +53 -0
  40. microsoft_agents/hosting/dialogs/memory/path_resolvers/at_at_path_resolver.py +9 -0
  41. microsoft_agents/hosting/dialogs/memory/path_resolvers/at_path_resolver.py +44 -0
  42. microsoft_agents/hosting/dialogs/memory/path_resolvers/dollar_path_resolver.py +9 -0
  43. microsoft_agents/hosting/dialogs/memory/path_resolvers/hash_path_resolver.py +9 -0
  44. microsoft_agents/hosting/dialogs/memory/path_resolvers/percent_path_resolver.py +9 -0
  45. microsoft_agents/hosting/dialogs/memory/scope_path.py +38 -0
  46. microsoft_agents/hosting/dialogs/memory/scopes/__init__.py +31 -0
  47. microsoft_agents/hosting/dialogs/memory/scopes/bot_state_memory_scope.py +66 -0
  48. microsoft_agents/hosting/dialogs/memory/scopes/class_memory_scope.py +64 -0
  49. microsoft_agents/hosting/dialogs/memory/scopes/conversation_memory_scope.py +12 -0
  50. microsoft_agents/hosting/dialogs/memory/scopes/dialog_class_memory_scope.py +52 -0
  51. microsoft_agents/hosting/dialogs/memory/scopes/dialog_context_memory_scope.py +68 -0
  52. microsoft_agents/hosting/dialogs/memory/scopes/dialog_memory_scope.py +75 -0
  53. microsoft_agents/hosting/dialogs/memory/scopes/memory_scope.py +91 -0
  54. microsoft_agents/hosting/dialogs/memory/scopes/settings_memory_scope.py +38 -0
  55. microsoft_agents/hosting/dialogs/memory/scopes/this_memory_scope.py +36 -0
  56. microsoft_agents/hosting/dialogs/memory/scopes/turn_memory_scope.py +86 -0
  57. microsoft_agents/hosting/dialogs/memory/scopes/user_memory_scope.py +12 -0
  58. microsoft_agents/hosting/dialogs/models/__init__.py +15 -0
  59. microsoft_agents/hosting/dialogs/models/dialog_event.py +13 -0
  60. microsoft_agents/hosting/dialogs/models/dialog_events.py +12 -0
  61. microsoft_agents/hosting/dialogs/models/dialog_instance.py +28 -0
  62. microsoft_agents/hosting/dialogs/models/dialog_reason.py +34 -0
  63. microsoft_agents/hosting/dialogs/models/dialog_turn_result.py +17 -0
  64. microsoft_agents/hosting/dialogs/models/dialog_turn_status.py +26 -0
  65. microsoft_agents/hosting/dialogs/object_path.py +315 -0
  66. microsoft_agents/hosting/dialogs/persisted_state.py +22 -0
  67. microsoft_agents/hosting/dialogs/persisted_state_keys.py +8 -0
  68. microsoft_agents/hosting/dialogs/prompts/__init__.py +41 -0
  69. microsoft_agents/hosting/dialogs/prompts/activity_prompt.py +203 -0
  70. microsoft_agents/hosting/dialogs/prompts/attachment_prompt.py +87 -0
  71. microsoft_agents/hosting/dialogs/prompts/choice_prompt.py +156 -0
  72. microsoft_agents/hosting/dialogs/prompts/confirm_prompt.py +161 -0
  73. microsoft_agents/hosting/dialogs/prompts/datetime_prompt.py +90 -0
  74. microsoft_agents/hosting/dialogs/prompts/datetime_resolution.py +16 -0
  75. microsoft_agents/hosting/dialogs/prompts/number_prompt.py +81 -0
  76. microsoft_agents/hosting/dialogs/prompts/oauth_prompt.py +569 -0
  77. microsoft_agents/hosting/dialogs/prompts/oauth_prompt_settings.py +43 -0
  78. microsoft_agents/hosting/dialogs/prompts/prompt.py +224 -0
  79. microsoft_agents/hosting/dialogs/prompts/prompt_culture_models.py +222 -0
  80. microsoft_agents/hosting/dialogs/prompts/prompt_options.py +42 -0
  81. microsoft_agents/hosting/dialogs/prompts/prompt_recognizer_result.py +11 -0
  82. microsoft_agents/hosting/dialogs/prompts/prompt_validator.py +0 -0
  83. microsoft_agents/hosting/dialogs/prompts/prompt_validator_context.py +44 -0
  84. microsoft_agents/hosting/dialogs/prompts/text_prompt.py +82 -0
  85. microsoft_agents/hosting/dialogs/waterfall_dialog.py +266 -0
  86. microsoft_agents/hosting/dialogs/waterfall_step_context.py +109 -0
  87. microsoft_agents_hosting_dialogs-0.10.0.dev2.dist-info/METADATA +87 -0
  88. microsoft_agents_hosting_dialogs-0.10.0.dev2.dist-info/RECORD +91 -0
  89. microsoft_agents_hosting_dialogs-0.10.0.dev2.dist-info/WHEEL +5 -0
  90. microsoft_agents_hosting_dialogs-0.10.0.dev2.dist-info/licenses/LICENSE +21 -0
  91. microsoft_agents_hosting_dialogs-0.10.0.dev2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,16 @@
1
+ # Copyright (c) Microsoft Corporation. All rights reserved.
2
+ # Licensed under the MIT License.
3
+
4
+
5
+ class DateTimeResolution:
6
+ def __init__(
7
+ self,
8
+ value: str | None = None,
9
+ start: str | None = None,
10
+ end: str | None = None,
11
+ timex: str | None = None,
12
+ ):
13
+ self.value = value
14
+ self.start = start
15
+ self.end = end
16
+ self.timex = timex
@@ -0,0 +1,81 @@
1
+ # Copyright (c) Microsoft Corporation. All rights reserved.
2
+ # Licensed under the MIT License.
3
+
4
+ from typing import Callable, cast
5
+
6
+ from recognizers_number import recognize_number
7
+ from recognizers_text import Culture, ModelResult
8
+ from babel.numbers import parse_decimal
9
+
10
+ from microsoft_agents.hosting.core import TurnContext
11
+ from microsoft_agents.activity import ActivityTypes
12
+
13
+ from .prompt import Prompt, PromptValidatorContext
14
+ from .prompt_options import PromptOptions
15
+ from .prompt_recognizer_result import PromptRecognizerResult
16
+
17
+
18
+ class NumberPrompt(Prompt):
19
+ def __init__(
20
+ self,
21
+ dialog_id: str,
22
+ validator: Callable[[PromptValidatorContext], bool] | None = None,
23
+ default_locale: str | None = None,
24
+ ):
25
+ super(NumberPrompt, self).__init__(dialog_id, validator)
26
+ self.default_locale = default_locale
27
+
28
+ async def on_prompt(
29
+ self,
30
+ turn_context: TurnContext,
31
+ state: dict[str, object],
32
+ options: PromptOptions,
33
+ is_retry: bool,
34
+ ):
35
+ if not turn_context:
36
+ raise TypeError("NumberPrompt.on_prompt(): turn_context cannot be None.")
37
+ if not options:
38
+ raise TypeError("NumberPrompt.on_prompt(): options cannot be None.")
39
+
40
+ if is_retry and options.retry_prompt is not None:
41
+ await turn_context.send_activity(options.retry_prompt)
42
+ elif options.prompt is not None:
43
+ await turn_context.send_activity(options.prompt)
44
+
45
+ async def on_recognize(
46
+ self,
47
+ turn_context: TurnContext,
48
+ state: dict[str, object],
49
+ options: PromptOptions,
50
+ ) -> PromptRecognizerResult:
51
+ if not turn_context:
52
+ raise TypeError("NumberPrompt.on_recognize(): turn_context cannot be None.")
53
+
54
+ result = PromptRecognizerResult()
55
+ if turn_context.activity.type == ActivityTypes.message:
56
+ utterance = turn_context.activity.text
57
+ if not utterance:
58
+ return result
59
+ culture = self._get_culture(turn_context)
60
+ results: list[ModelResult] = recognize_number(utterance, culture)
61
+
62
+ if results:
63
+ result.succeeded = True
64
+ result.value = parse_decimal(
65
+ cast(str, results[0].resolution["value"]),
66
+ locale=culture.replace("-", "_"),
67
+ )
68
+
69
+ return result
70
+
71
+ def _get_culture(self, turn_context: TurnContext):
72
+ culture = (
73
+ turn_context.activity.locale
74
+ if turn_context.activity.locale
75
+ else self.default_locale
76
+ )
77
+
78
+ if not culture:
79
+ culture = Culture.English
80
+
81
+ return culture
@@ -0,0 +1,569 @@
1
+ # Copyright (c) Microsoft Corporation. All rights reserved.
2
+ # Licensed under the MIT License.
3
+
4
+ import logging
5
+ from datetime import datetime, timedelta
6
+ from http import HTTPStatus
7
+ from typing import cast
8
+
9
+ from microsoft_agents.activity import (
10
+ Channels,
11
+ Activity,
12
+ ActivityTypes,
13
+ ActionTypes,
14
+ CardAction,
15
+ InputHints,
16
+ SigninCard,
17
+ SignInConstants,
18
+ OAuthCard,
19
+ TokenResponse,
20
+ TokenExchangeInvokeRequest,
21
+ TokenExchangeInvokeResponse,
22
+ InvokeResponse,
23
+ )
24
+ from microsoft_agents.hosting.core import (
25
+ CardFactory,
26
+ MessageFactory,
27
+ TurnContext,
28
+ ChannelAdapter,
29
+ ClaimsIdentity,
30
+ UserTokenClient,
31
+ MemoryStorage,
32
+ )
33
+ from microsoft_agents.hosting.core._oauth import (
34
+ _OAuthFlow,
35
+ _FlowStorageClient,
36
+ _FlowState,
37
+ _FlowStateTag,
38
+ _FlowResponse,
39
+ )
40
+ from opentelemetry import context
41
+
42
+ from ..dialog import Dialog
43
+ from ..dialog_context import DialogContext
44
+ from ..models.dialog_turn_result import DialogTurnResult
45
+ from .prompt_options import PromptOptions
46
+ from .oauth_prompt_settings import OAuthPromptSettings
47
+
48
+ logger = logging.getLogger(__name__)
49
+
50
+
51
+ class CallerInfo:
52
+ def __init__(self, caller_service_url: str | None = None, scope: str | None = None):
53
+ self.caller_service_url = caller_service_url
54
+ self.scope = scope
55
+
56
+
57
+ class OAuthPrompt(Dialog):
58
+ PERSISTED_OPTIONS = "options"
59
+ PERSISTED_STATE = "state"
60
+ PERSISTED_EXPIRES = "expires"
61
+ PERSISTED_CALLER = "caller"
62
+
63
+ """
64
+ Creates a new prompt that asks the user to sign in.
65
+ """
66
+
67
+ def __init__(
68
+ self,
69
+ dialog_id: str,
70
+ settings: OAuthPromptSettings,
71
+ ):
72
+ super().__init__(dialog_id)
73
+ self._storage = MemoryStorage() # to keep track of the OAuth flow state
74
+
75
+ if not settings:
76
+ raise TypeError(
77
+ "OAuthPrompt.__init__(): OAuthPrompt requires OAuthPromptSettings."
78
+ )
79
+
80
+ self._settings = settings
81
+
82
+ @staticmethod
83
+ def _get_user_token_client(context: TurnContext) -> UserTokenClient:
84
+ return context.turn_state.get(context.adapter.USER_TOKEN_CLIENT_KEY)
85
+
86
+ def _get_app_id(self, context: TurnContext) -> str:
87
+ if (
88
+ hasattr(self._settings, "oauth_app_credentials")
89
+ and self._settings.oauth_app_credentials
90
+ and hasattr(self._settings.oauth_app_credentials, "app_id")
91
+ ):
92
+ return self._settings.oauth_app_credentials.app_id
93
+ return context._identity.claims.get("aud", "")
94
+
95
+ async def _load_flow(
96
+ self, context: TurnContext
97
+ ) -> tuple[_OAuthFlow, _FlowStorageClient]:
98
+ """Loads the OAuth flow.
99
+
100
+ A new flow is created in Storage if none exists for the channel, user, and handler
101
+ combination.
102
+
103
+ :param context: The context object for the current turn.
104
+ :type context: TurnContext
105
+ :return: A tuple containing the OAuthFlow and FlowStorageClient created from the
106
+ context and the specified auth handler.
107
+ :rtype: tuple[OAuthFlow, FlowStorageClient]
108
+ """
109
+ user_token_client = OAuthPrompt._get_user_token_client(context)
110
+
111
+ if (
112
+ not context.activity.channel_id
113
+ or not context.activity.from_property
114
+ or not context.activity.from_property.id
115
+ ):
116
+ raise ValueError("Channel ID and User ID are required")
117
+
118
+ channel_id = context.activity.channel_id
119
+ user_id = context.activity.from_property.id
120
+
121
+ ms_app_id = self._get_app_id(context)
122
+
123
+ # try to load existing state
124
+ flow_storage_client = _FlowStorageClient(channel_id, user_id, self._storage)
125
+ logger.info("Loading OAuth flow state from storage")
126
+ flow_state: _FlowState | None = await flow_storage_client.read(self._id)
127
+ if not flow_state:
128
+ logger.info("No existing flow state found, creating new flow state")
129
+ flow_state = _FlowState(
130
+ channel_id=channel_id,
131
+ user_id=user_id,
132
+ auth_handler_id=self._id,
133
+ connection=self._settings.connection_name,
134
+ ms_app_id=ms_app_id,
135
+ )
136
+
137
+ timeout = (
138
+ self._settings.timeout
139
+ if isinstance(self._settings.timeout, int)
140
+ else 900000
141
+ )
142
+
143
+ flow = _OAuthFlow(
144
+ flow_state,
145
+ user_token_client,
146
+ default_flow_duration=timeout,
147
+ )
148
+ return flow, flow_storage_client
149
+
150
+ async def begin_dialog(
151
+ self, dialog_context: DialogContext, options: object = None
152
+ ) -> DialogTurnResult:
153
+ if dialog_context is None:
154
+ raise TypeError(
155
+ f"OAuthPrompt.begin_dialog(): Expected DialogContext but got NoneType instead"
156
+ )
157
+
158
+ prompt_options = (
159
+ options if isinstance(options, PromptOptions) else None
160
+ ) or PromptOptions()
161
+
162
+ # Ensure prompts have input hint set
163
+ if prompt_options.prompt and not prompt_options.prompt.input_hint:
164
+ prompt_options.prompt.input_hint = InputHints.accepting_input
165
+
166
+ if prompt_options.retry_prompt and not prompt_options.retry_prompt.input_hint:
167
+ prompt_options.retry_prompt.input_hint = InputHints.accepting_input
168
+
169
+ # Initialize prompt state
170
+ timeout = (
171
+ self._settings.timeout
172
+ if isinstance(self._settings.timeout, int)
173
+ else 900000
174
+ )
175
+ assert dialog_context.active_dialog is not None
176
+ state = dialog_context.active_dialog.state
177
+ state[OAuthPrompt.PERSISTED_STATE] = {}
178
+ state[OAuthPrompt.PERSISTED_OPTIONS] = prompt_options
179
+ state[OAuthPrompt.PERSISTED_EXPIRES] = datetime.now() + timedelta(
180
+ seconds=timeout / 1000
181
+ )
182
+ state[OAuthPrompt.PERSISTED_CALLER] = OAuthPrompt.__create_caller_info(
183
+ dialog_context.context
184
+ )
185
+
186
+ flow, flow_storage_client = await self._load_flow(dialog_context.context)
187
+
188
+ flow_response: _FlowResponse = await flow.begin_flow(
189
+ dialog_context.context.activity
190
+ )
191
+
192
+ await flow_storage_client.write(flow_response.flow_state)
193
+
194
+ if flow_response.flow_state.tag == _FlowStateTag.COMPLETE:
195
+ return await dialog_context.end_dialog(flow_response.token_response)
196
+
197
+ await self._send_oauth_card(
198
+ dialog_context.context, flow_response, prompt_options.prompt
199
+ )
200
+ return Dialog.end_of_turn
201
+
202
+ async def continue_dialog(self, dialog_context: DialogContext) -> DialogTurnResult:
203
+
204
+ assert dialog_context.active_dialog is not None
205
+ state = dialog_context.active_dialog.state
206
+
207
+ # Check for timeout
208
+ expires = state.get(OAuthPrompt.PERSISTED_EXPIRES)
209
+ if expires and datetime.now() > expires:
210
+ return await dialog_context.end_dialog(None)
211
+
212
+ flow_response = await self._continue_flow(dialog_context.context)
213
+
214
+ if (
215
+ flow_response is not None
216
+ and flow_response.flow_state.tag == _FlowStateTag.COMPLETE
217
+ ):
218
+ return await dialog_context.end_dialog(flow_response.token_response)
219
+
220
+ if (
221
+ dialog_context.context.activity.type == ActivityTypes.message
222
+ and self._settings.end_on_invalid_message
223
+ ):
224
+ return await dialog_context.end_dialog(None)
225
+
226
+ if (
227
+ not dialog_context.context.responded
228
+ and dialog_context.context.activity.type == ActivityTypes.message
229
+ and state[OAuthPrompt.PERSISTED_OPTIONS].retry_prompt is not None
230
+ ):
231
+ await dialog_context.context.send_activity(
232
+ state[OAuthPrompt.PERSISTED_OPTIONS].retry_prompt
233
+ )
234
+
235
+ return Dialog.end_of_turn
236
+
237
+ async def get_user_token(
238
+ self, context: TurnContext, code: str = ""
239
+ ) -> TokenResponse:
240
+ """
241
+ Gets the user's token.
242
+ """
243
+ flow, _ = await self._load_flow(context)
244
+ return await flow.get_user_token(code)
245
+
246
+ async def sign_out_user(self, context: TurnContext):
247
+ """
248
+ Signs out the user.
249
+ """
250
+ flow, flow_storage_client = await self._load_flow(context)
251
+ await flow.sign_out()
252
+ await flow_storage_client.delete(
253
+ self._id
254
+ ) # Clear flow state from storage after signing out
255
+
256
+ @staticmethod
257
+ def __create_caller_info(context: TurnContext) -> CallerInfo | None:
258
+ bot_identity = cast(
259
+ ClaimsIdentity | None,
260
+ context.turn_state.get(ChannelAdapter.AGENT_IDENTITY_KEY),
261
+ )
262
+ if bot_identity and bot_identity.is_agent_claim():
263
+ return CallerInfo(
264
+ caller_service_url=context.activity.service_url,
265
+ scope=bot_identity.get_app_id(),
266
+ )
267
+
268
+ return None
269
+
270
+ async def _send_oauth_card(
271
+ self,
272
+ context: TurnContext,
273
+ flow_response: _FlowResponse,
274
+ prompt: Activity | str | None = None,
275
+ ):
276
+ if not isinstance(prompt, Activity):
277
+ prompt = MessageFactory.text(prompt or "", None, InputHints.accepting_input)
278
+ else:
279
+ prompt.input_hint = prompt.input_hint or InputHints.accepting_input
280
+
281
+ prompt.attachments = prompt.attachments or []
282
+
283
+ if OAuthPrompt._channel_suppports_oauth_card(context.activity.channel_id or ""):
284
+ if not any(
285
+ att.content_type == CardFactory.content_types.oauth_card
286
+ for att in prompt.attachments
287
+ ):
288
+ card_action_type = ActionTypes.signin
289
+ sign_in_resource = flow_response.sign_in_resource
290
+ link = sign_in_resource.sign_in_link
291
+ bot_identity = cast(
292
+ ClaimsIdentity | None,
293
+ context.turn_state.get(ChannelAdapter.AGENT_IDENTITY_KEY),
294
+ )
295
+
296
+ # use the SignInLink when in speech channel or bot is a skill or
297
+ # an extra OAuthAppCredentials is being passed in
298
+ if (
299
+ (bot_identity and bot_identity.is_agent_claim())
300
+ or not context.activity.service_url.startswith("http")
301
+ or (
302
+ hasattr(self._settings, "oauth_app_credentials")
303
+ and self._settings.oauth_app_credentials
304
+ )
305
+ ):
306
+ if context.activity.channel_id == Channels.emulator:
307
+ card_action_type = ActionTypes.open_url
308
+ elif not OAuthPrompt._channel_requires_sign_in_link(
309
+ context.activity.channel_id or ""
310
+ ):
311
+ link = None
312
+
313
+ json_token_ex_resource = (
314
+ sign_in_resource.token_exchange_resource.model_dump()
315
+ if sign_in_resource.token_exchange_resource
316
+ else None
317
+ )
318
+
319
+ json_token_ex_post = (
320
+ sign_in_resource.token_post_resource.model_dump()
321
+ if hasattr(sign_in_resource, "token_post_resource")
322
+ and sign_in_resource.token_post_resource
323
+ else None
324
+ )
325
+
326
+ card_action_kwargs = {
327
+ "title": self._settings.title,
328
+ "type": card_action_type,
329
+ "value": link,
330
+ }
331
+ if self._settings.text:
332
+ card_action_kwargs["text"] = self._settings.text
333
+ oauth_card_kwargs = {
334
+ "connection_name": self._settings.connection_name,
335
+ "buttons": [CardAction(**card_action_kwargs)],
336
+ "token_exchange_resource": json_token_ex_resource,
337
+ }
338
+ if self._settings.text:
339
+ oauth_card_kwargs["text"] = self._settings.text
340
+ if json_token_ex_post:
341
+ oauth_card_kwargs["token_post_resource"] = json_token_ex_post
342
+ prompt.attachments.append(
343
+ CardFactory.oauth_card(OAuthCard(**oauth_card_kwargs))
344
+ )
345
+ else:
346
+ if not any(
347
+ att.content_type == CardFactory.content_types.signin_card
348
+ for att in prompt.attachments
349
+ ):
350
+ if not hasattr(context.adapter, "get_oauth_sign_in_link"):
351
+ raise Exception(
352
+ "OAuthPrompt._send_oauth_card(): get_oauth_sign_in_link() not supported by the current adapter"
353
+ )
354
+
355
+ link = await context.adapter.get_oauth_sign_in_link(
356
+ context,
357
+ self._settings.connection_name,
358
+ )
359
+ prompt.attachments.append(
360
+ CardFactory.signin_card(
361
+ SigninCard(
362
+ text=self._settings.text or "",
363
+ buttons=[
364
+ CardAction(
365
+ title=self._settings.title,
366
+ value=link,
367
+ type=ActionTypes.signin,
368
+ )
369
+ ],
370
+ )
371
+ )
372
+ )
373
+
374
+ # Send prompt
375
+ await context.send_activity(prompt)
376
+
377
+ @staticmethod
378
+ def _validate_token_exchange_invoke_response(
379
+ activity: Activity,
380
+ ) -> TokenExchangeInvokeRequest:
381
+ activity_value = activity.value
382
+ if isinstance(activity_value, dict):
383
+ activity_value = TokenExchangeInvokeRequest.model_validate(activity_value)
384
+ return cast(TokenExchangeInvokeRequest, activity_value)
385
+
386
+ def _validate_continue_flow(self, context: TurnContext) -> Activity | None:
387
+ if self._is_token_exchange_request_invoke(context):
388
+ activity_value = context.activity.value
389
+ if isinstance(activity_value, dict):
390
+ activity_value = TokenExchangeInvokeRequest.model_validate(
391
+ activity_value
392
+ )
393
+
394
+ token_exchange_invoke_request = (
395
+ OAuthPrompt._validate_token_exchange_invoke_response(context.activity)
396
+ )
397
+
398
+ if not (
399
+ token_exchange_invoke_request
400
+ and self._is_token_exchange_request(token_exchange_invoke_request)
401
+ ):
402
+ # Received activity is not a token exchange request.
403
+ return self._get_token_exchange_invoke_response(
404
+ int(HTTPStatus.BAD_REQUEST),
405
+ "The bot received an InvokeActivity that is missing a TokenExchangeInvokeRequest value."
406
+ " This is required to be sent with the InvokeActivity.",
407
+ )
408
+ elif (
409
+ token_exchange_invoke_request.connection_name
410
+ != self._settings.connection_name
411
+ ):
412
+ # Connection name on activity does not match that of setting.
413
+ return self._get_token_exchange_invoke_response(
414
+ int(HTTPStatus.BAD_REQUEST),
415
+ "The bot received an InvokeActivity with a TokenExchangeInvokeRequest containing a"
416
+ " ConnectionName that does not match the ConnectionName expected by the bots active"
417
+ " OAuthPrompt. Ensure these names match when sending the InvokeActivity.",
418
+ )
419
+
420
+ async def _exchange_token(
421
+ self, context: TurnContext, input_token_response: TokenResponse | None
422
+ ) -> TokenResponse | None:
423
+ if not input_token_response:
424
+ return input_token_response
425
+
426
+ user_id = context.activity.from_property.id
427
+ channel_id = (
428
+ context.activity.channel_id.channel if context.activity.channel_id else ""
429
+ )
430
+
431
+ user_token_client = OAuthPrompt._get_user_token_client(context)
432
+
433
+ return await user_token_client.user_token.exchange_token(
434
+ user_id,
435
+ self._settings.connection_name,
436
+ channel_id,
437
+ {"token": input_token_response.token},
438
+ )
439
+
440
+ async def _continue_flow(
441
+ self,
442
+ context: TurnContext,
443
+ ) -> _FlowResponse | None:
444
+
445
+ flow_response: _FlowResponse | None = None
446
+
447
+ error_response = self._validate_continue_flow(context)
448
+ if error_response:
449
+ await context.send_activity(error_response)
450
+
451
+ # do something here
452
+
453
+ if error_response is None:
454
+
455
+ flow, flow_storage_client = await self._load_flow(context)
456
+
457
+ try:
458
+ flow_response = await flow.continue_flow(context.activity)
459
+ except Exception:
460
+ error_response = Activity( # type: ignore[call-arg]
461
+ type=ActivityTypes.invoke_response,
462
+ value=InvokeResponse(status=HTTPStatus.INTERNAL_SERVER_ERROR),
463
+ )
464
+
465
+ if error_response is None:
466
+ assert flow_response is not None
467
+ await flow_storage_client.write(flow.flow_state)
468
+
469
+ token_response: TokenResponse | None = flow_response.token_response
470
+
471
+ if OAuthPrompt._is_teams_verification_invoke(context):
472
+ if token_response:
473
+ await context.send_activity(
474
+ Activity( # type: ignore[call-arg]
475
+ type=ActivityTypes.invoke_response,
476
+ value=InvokeResponse(status=HTTPStatus.OK),
477
+ )
478
+ )
479
+ else:
480
+ await context.send_activity(
481
+ Activity( # type: ignore[call-arg]
482
+ type=ActivityTypes.invoke_response,
483
+ value=InvokeResponse(status=HTTPStatus.NOT_FOUND),
484
+ )
485
+ )
486
+ elif self._is_token_exchange_request_invoke(context):
487
+
488
+ token_exchange_response: TokenResponse | None = None
489
+
490
+ token_exchange_response = await self._exchange_token(
491
+ context, token_response
492
+ )
493
+
494
+ if token_exchange_response:
495
+ await context.send_activity(
496
+ Activity( # type: ignore[call-arg]
497
+ type=ActivityTypes.invoke_response,
498
+ value=InvokeResponse(status=HTTPStatus.OK),
499
+ )
500
+ )
501
+ else:
502
+ await context.send_activity(
503
+ self._get_token_exchange_invoke_response(
504
+ int(HTTPStatus.PRECONDITION_FAILED),
505
+ "The bot is unable to exchange token. Proceed with regular login.",
506
+ )
507
+ )
508
+
509
+ return flow_response
510
+
511
+ def _get_token_exchange_invoke_response(
512
+ self, status: int, failure_detail: str | None
513
+ ) -> Activity:
514
+ body = {"connectionName": self._settings.connection_name}
515
+ if failure_detail:
516
+ body["failureDetail"] = failure_detail
517
+ return Activity( # type: ignore[call-arg]
518
+ type=ActivityTypes.invoke_response,
519
+ value=InvokeResponse(status=status, body=body),
520
+ )
521
+
522
+ @staticmethod
523
+ def _is_token_response_event(context: TurnContext) -> bool:
524
+ activity = context.activity
525
+
526
+ return (
527
+ activity.type == ActivityTypes.event
528
+ and activity.name == SignInConstants.token_response_event_name
529
+ )
530
+
531
+ @staticmethod
532
+ def _is_teams_verification_invoke(context: TurnContext) -> bool:
533
+ activity = context.activity
534
+
535
+ return (
536
+ activity.type == ActivityTypes.invoke
537
+ and activity.name == SignInConstants.verify_state_operation_name
538
+ )
539
+
540
+ @staticmethod
541
+ def _channel_suppports_oauth_card(channel_id: str) -> bool:
542
+ if channel_id in [
543
+ Channels.cortana,
544
+ Channels.skype,
545
+ Channels.skype_for_business,
546
+ ]:
547
+ return False
548
+
549
+ return True
550
+
551
+ @staticmethod
552
+ def _channel_requires_sign_in_link(channel_id: str) -> bool:
553
+ if channel_id in [Channels.ms_teams]:
554
+ return True
555
+
556
+ return False
557
+
558
+ @staticmethod
559
+ def _is_token_exchange_request_invoke(context: TurnContext) -> bool:
560
+ activity = context.activity
561
+
562
+ return (
563
+ activity.type == ActivityTypes.invoke
564
+ and activity.name == SignInConstants.token_exchange_operation_name
565
+ )
566
+
567
+ @staticmethod
568
+ def _is_token_exchange_request(obj: TokenExchangeInvokeRequest) -> bool:
569
+ return bool(obj.connection_name) and bool(obj.token)