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.
- microsoft_agents/hosting/dialogs/__init__.py +76 -0
- microsoft_agents/hosting/dialogs/_component_registration.py +30 -0
- microsoft_agents/hosting/dialogs/_telemetry_client.py +78 -0
- microsoft_agents/hosting/dialogs/choices/__init__.py +38 -0
- microsoft_agents/hosting/dialogs/choices/channel.py +121 -0
- microsoft_agents/hosting/dialogs/choices/choice_factory.py +262 -0
- microsoft_agents/hosting/dialogs/choices/choice_recognizer.py +148 -0
- microsoft_agents/hosting/dialogs/choices/find.py +242 -0
- microsoft_agents/hosting/dialogs/choices/models/__init__.py +23 -0
- microsoft_agents/hosting/dialogs/choices/models/choice.py +14 -0
- microsoft_agents/hosting/dialogs/choices/models/choice_factory_options.py +13 -0
- microsoft_agents/hosting/dialogs/choices/models/find_choices_options.py +28 -0
- microsoft_agents/hosting/dialogs/choices/models/find_values_options.py +31 -0
- microsoft_agents/hosting/dialogs/choices/models/found_choice.py +22 -0
- microsoft_agents/hosting/dialogs/choices/models/found_value.py +20 -0
- microsoft_agents/hosting/dialogs/choices/models/list_style.py +15 -0
- microsoft_agents/hosting/dialogs/choices/models/model_result.py +16 -0
- microsoft_agents/hosting/dialogs/choices/models/sorted_value.py +16 -0
- microsoft_agents/hosting/dialogs/choices/models/token.py +20 -0
- microsoft_agents/hosting/dialogs/choices/tokenizer.py +92 -0
- microsoft_agents/hosting/dialogs/component_dialog.py +284 -0
- microsoft_agents/hosting/dialogs/dialog.py +198 -0
- microsoft_agents/hosting/dialogs/dialog_component_registration.py +52 -0
- microsoft_agents/hosting/dialogs/dialog_container.py +31 -0
- microsoft_agents/hosting/dialogs/dialog_context.py +426 -0
- microsoft_agents/hosting/dialogs/dialog_extensions.py +201 -0
- microsoft_agents/hosting/dialogs/dialog_manager.py +189 -0
- microsoft_agents/hosting/dialogs/dialog_manager_result.py +17 -0
- microsoft_agents/hosting/dialogs/dialog_set.py +174 -0
- microsoft_agents/hosting/dialogs/dialog_state.py +20 -0
- microsoft_agents/hosting/dialogs/memory/__init__.py +24 -0
- microsoft_agents/hosting/dialogs/memory/component_memory_scopes_base.py +14 -0
- microsoft_agents/hosting/dialogs/memory/component_path_resolvers_base.py +15 -0
- microsoft_agents/hosting/dialogs/memory/dialog_path.py +33 -0
- microsoft_agents/hosting/dialogs/memory/dialog_state_manager.py +563 -0
- microsoft_agents/hosting/dialogs/memory/dialog_state_manager_configuration.py +11 -0
- microsoft_agents/hosting/dialogs/memory/path_resolver_base.py +8 -0
- microsoft_agents/hosting/dialogs/memory/path_resolvers/__init__.py +19 -0
- microsoft_agents/hosting/dialogs/memory/path_resolvers/alias_path_resolver.py +53 -0
- microsoft_agents/hosting/dialogs/memory/path_resolvers/at_at_path_resolver.py +9 -0
- microsoft_agents/hosting/dialogs/memory/path_resolvers/at_path_resolver.py +44 -0
- microsoft_agents/hosting/dialogs/memory/path_resolvers/dollar_path_resolver.py +9 -0
- microsoft_agents/hosting/dialogs/memory/path_resolvers/hash_path_resolver.py +9 -0
- microsoft_agents/hosting/dialogs/memory/path_resolvers/percent_path_resolver.py +9 -0
- microsoft_agents/hosting/dialogs/memory/scope_path.py +38 -0
- microsoft_agents/hosting/dialogs/memory/scopes/__init__.py +31 -0
- microsoft_agents/hosting/dialogs/memory/scopes/bot_state_memory_scope.py +66 -0
- microsoft_agents/hosting/dialogs/memory/scopes/class_memory_scope.py +64 -0
- microsoft_agents/hosting/dialogs/memory/scopes/conversation_memory_scope.py +12 -0
- microsoft_agents/hosting/dialogs/memory/scopes/dialog_class_memory_scope.py +52 -0
- microsoft_agents/hosting/dialogs/memory/scopes/dialog_context_memory_scope.py +68 -0
- microsoft_agents/hosting/dialogs/memory/scopes/dialog_memory_scope.py +75 -0
- microsoft_agents/hosting/dialogs/memory/scopes/memory_scope.py +91 -0
- microsoft_agents/hosting/dialogs/memory/scopes/settings_memory_scope.py +38 -0
- microsoft_agents/hosting/dialogs/memory/scopes/this_memory_scope.py +36 -0
- microsoft_agents/hosting/dialogs/memory/scopes/turn_memory_scope.py +86 -0
- microsoft_agents/hosting/dialogs/memory/scopes/user_memory_scope.py +12 -0
- microsoft_agents/hosting/dialogs/models/__init__.py +15 -0
- microsoft_agents/hosting/dialogs/models/dialog_event.py +13 -0
- microsoft_agents/hosting/dialogs/models/dialog_events.py +12 -0
- microsoft_agents/hosting/dialogs/models/dialog_instance.py +28 -0
- microsoft_agents/hosting/dialogs/models/dialog_reason.py +34 -0
- microsoft_agents/hosting/dialogs/models/dialog_turn_result.py +17 -0
- microsoft_agents/hosting/dialogs/models/dialog_turn_status.py +26 -0
- microsoft_agents/hosting/dialogs/object_path.py +315 -0
- microsoft_agents/hosting/dialogs/persisted_state.py +22 -0
- microsoft_agents/hosting/dialogs/persisted_state_keys.py +8 -0
- microsoft_agents/hosting/dialogs/prompts/__init__.py +41 -0
- microsoft_agents/hosting/dialogs/prompts/activity_prompt.py +203 -0
- microsoft_agents/hosting/dialogs/prompts/attachment_prompt.py +87 -0
- microsoft_agents/hosting/dialogs/prompts/choice_prompt.py +156 -0
- microsoft_agents/hosting/dialogs/prompts/confirm_prompt.py +161 -0
- microsoft_agents/hosting/dialogs/prompts/datetime_prompt.py +90 -0
- microsoft_agents/hosting/dialogs/prompts/datetime_resolution.py +16 -0
- microsoft_agents/hosting/dialogs/prompts/number_prompt.py +81 -0
- microsoft_agents/hosting/dialogs/prompts/oauth_prompt.py +569 -0
- microsoft_agents/hosting/dialogs/prompts/oauth_prompt_settings.py +43 -0
- microsoft_agents/hosting/dialogs/prompts/prompt.py +224 -0
- microsoft_agents/hosting/dialogs/prompts/prompt_culture_models.py +222 -0
- microsoft_agents/hosting/dialogs/prompts/prompt_options.py +42 -0
- microsoft_agents/hosting/dialogs/prompts/prompt_recognizer_result.py +11 -0
- microsoft_agents/hosting/dialogs/prompts/prompt_validator.py +0 -0
- microsoft_agents/hosting/dialogs/prompts/prompt_validator_context.py +44 -0
- microsoft_agents/hosting/dialogs/prompts/text_prompt.py +82 -0
- microsoft_agents/hosting/dialogs/waterfall_dialog.py +266 -0
- microsoft_agents/hosting/dialogs/waterfall_step_context.py +109 -0
- microsoft_agents_hosting_dialogs-0.10.0.dev2.dist-info/METADATA +87 -0
- microsoft_agents_hosting_dialogs-0.10.0.dev2.dist-info/RECORD +91 -0
- microsoft_agents_hosting_dialogs-0.10.0.dev2.dist-info/WHEEL +5 -0
- microsoft_agents_hosting_dialogs-0.10.0.dev2.dist-info/licenses/LICENSE +21 -0
- 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)
|