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,426 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from microsoft_agents.hosting.core.turn_context import TurnContext
|
|
7
|
+
from microsoft_agents.hosting.dialogs.memory import DialogStateManager
|
|
8
|
+
|
|
9
|
+
from .models.dialog_event import DialogEvent
|
|
10
|
+
from .models.dialog_events import DialogEvents
|
|
11
|
+
from .dialog_set import DialogSet
|
|
12
|
+
from .dialog_state import DialogState
|
|
13
|
+
from .models.dialog_turn_status import DialogTurnStatus
|
|
14
|
+
from .models.dialog_turn_result import DialogTurnResult
|
|
15
|
+
from .models.dialog_reason import DialogReason
|
|
16
|
+
from .models.dialog_instance import DialogInstance
|
|
17
|
+
from .dialog import Dialog
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DialogContext:
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self, dialog_set: DialogSet, turn_context: TurnContext, state: DialogState
|
|
24
|
+
):
|
|
25
|
+
if dialog_set is None:
|
|
26
|
+
raise TypeError("DialogContext(): dialog_set cannot be None.")
|
|
27
|
+
# TODO: Circular dependency with dialog_set: Check type.
|
|
28
|
+
if turn_context is None:
|
|
29
|
+
raise TypeError("DialogContext(): turn_context cannot be None.")
|
|
30
|
+
self._turn_context = turn_context
|
|
31
|
+
self._dialogs = dialog_set
|
|
32
|
+
self._stack = state.dialog_stack
|
|
33
|
+
self.services = {}
|
|
34
|
+
self.parent: DialogContext | None = None
|
|
35
|
+
self.state = DialogStateManager(self)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def dialogs(self) -> DialogSet:
|
|
39
|
+
"""Gets the set of dialogs that can be called from this context.
|
|
40
|
+
|
|
41
|
+
:param:
|
|
42
|
+
:return DialogSet:
|
|
43
|
+
"""
|
|
44
|
+
return self._dialogs
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def context(self) -> TurnContext:
|
|
48
|
+
"""Gets the context for the current turn of conversation.
|
|
49
|
+
|
|
50
|
+
:param:
|
|
51
|
+
:return TurnContext:
|
|
52
|
+
"""
|
|
53
|
+
return self._turn_context
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def stack(self) -> list[DialogInstance]:
|
|
57
|
+
"""Gets the current dialog stack.
|
|
58
|
+
|
|
59
|
+
:param:
|
|
60
|
+
:return list:
|
|
61
|
+
"""
|
|
62
|
+
return self._stack
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def active_dialog(self) -> DialogInstance | None:
|
|
66
|
+
"""Gets the instance of the active (top-of-stack) dialog, or None if the stack is empty.
|
|
67
|
+
|
|
68
|
+
:return: The active DialogInstance, or None if no dialog is active.
|
|
69
|
+
"""
|
|
70
|
+
if self._stack:
|
|
71
|
+
return self._stack[0]
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def child(self) -> DialogContext | None:
|
|
76
|
+
"""Gets the DialogContext for the active dialog's inner dialog stack, if the active
|
|
77
|
+
dialog is a DialogContainer (e.g. ComponentDialog). Returns None if there is no
|
|
78
|
+
active dialog or the active dialog is not a container.
|
|
79
|
+
|
|
80
|
+
:return: The child DialogContext, or None.
|
|
81
|
+
"""
|
|
82
|
+
# pylint: disable=import-outside-toplevel
|
|
83
|
+
instance = self.active_dialog
|
|
84
|
+
|
|
85
|
+
if instance:
|
|
86
|
+
dialog = self.find_dialog_sync(instance.id)
|
|
87
|
+
|
|
88
|
+
# This import prevents circular dependency issues
|
|
89
|
+
from .dialog_container import DialogContainer
|
|
90
|
+
|
|
91
|
+
if isinstance(dialog, DialogContainer):
|
|
92
|
+
return dialog.create_child_context(self)
|
|
93
|
+
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
async def begin_dialog(self, dialog_id: str, options: object = None):
|
|
97
|
+
"""
|
|
98
|
+
Pushes a new dialog onto the dialog stack.
|
|
99
|
+
:param dialog_id: ID of the dialog to start
|
|
100
|
+
:param options: (Optional) additional argument(s) to pass to the dialog being started.
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
if not dialog_id:
|
|
104
|
+
raise TypeError("Dialog(): dialog_id cannot be None.")
|
|
105
|
+
# Look up dialog
|
|
106
|
+
dialog = await self.find_dialog(dialog_id)
|
|
107
|
+
if dialog is None:
|
|
108
|
+
raise Exception(
|
|
109
|
+
"'DialogContext.begin_dialog(): A dialog with an id of '%s' wasn't found."
|
|
110
|
+
" The dialog must be included in the current or parent DialogSet."
|
|
111
|
+
" For example, if subclassing a ComponentDialog you can call add_dialog() within your constructor."
|
|
112
|
+
% dialog_id
|
|
113
|
+
)
|
|
114
|
+
# Push new instance onto stack
|
|
115
|
+
instance = DialogInstance()
|
|
116
|
+
instance.id = dialog_id
|
|
117
|
+
instance.state = {}
|
|
118
|
+
|
|
119
|
+
self._stack.insert(0, (instance))
|
|
120
|
+
|
|
121
|
+
# Call dialog's begin_dialog() method
|
|
122
|
+
return await dialog.begin_dialog(self, options)
|
|
123
|
+
except Exception as err:
|
|
124
|
+
self.__set_exception_context_data(err)
|
|
125
|
+
raise
|
|
126
|
+
|
|
127
|
+
# TODO: Fix options: PromptOptions instead of object
|
|
128
|
+
async def prompt(self, dialog_id: str, options) -> DialogTurnResult:
|
|
129
|
+
"""
|
|
130
|
+
Helper function to simplify formatting the options for calling a prompt dialog. This helper will
|
|
131
|
+
take a `PromptOptions` argument and then call.
|
|
132
|
+
:param dialog_id: ID of the prompt to start.
|
|
133
|
+
:param options: Contains a Prompt, potentially a RetryPrompt and if using ChoicePrompt, Choices.
|
|
134
|
+
:return:
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
if not dialog_id:
|
|
138
|
+
raise TypeError("DialogContext.prompt(): dialogId cannot be None.")
|
|
139
|
+
|
|
140
|
+
if not options:
|
|
141
|
+
raise TypeError("DialogContext.prompt(): options cannot be None.")
|
|
142
|
+
|
|
143
|
+
return await self.begin_dialog(dialog_id, options)
|
|
144
|
+
except Exception as err:
|
|
145
|
+
self.__set_exception_context_data(err)
|
|
146
|
+
raise
|
|
147
|
+
|
|
148
|
+
async def continue_dialog(self):
|
|
149
|
+
"""
|
|
150
|
+
Continues execution of the active dialog, if there is one, by passing the context object to
|
|
151
|
+
its `Dialog.continue_dialog()` method. You can check `turn_context.responded` after the call completes
|
|
152
|
+
to determine if a dialog was run and a reply was sent to the user.
|
|
153
|
+
:return:
|
|
154
|
+
"""
|
|
155
|
+
try:
|
|
156
|
+
# Check for a dialog on the stack
|
|
157
|
+
if self.active_dialog is not None:
|
|
158
|
+
# Look up dialog
|
|
159
|
+
dialog = await self.find_dialog(self.active_dialog.id)
|
|
160
|
+
if not dialog:
|
|
161
|
+
raise Exception(
|
|
162
|
+
"DialogContext.continue_dialog(): Can't continue dialog. "
|
|
163
|
+
"A dialog with an id of '%s' wasn't found."
|
|
164
|
+
% self.active_dialog.id
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Continue execution of dialog
|
|
168
|
+
return await dialog.continue_dialog(self)
|
|
169
|
+
|
|
170
|
+
return DialogTurnResult(DialogTurnStatus.Empty)
|
|
171
|
+
except Exception as err:
|
|
172
|
+
self.__set_exception_context_data(err)
|
|
173
|
+
raise
|
|
174
|
+
|
|
175
|
+
# TODO: instance is DialogInstance
|
|
176
|
+
async def end_dialog(self, result: object = None):
|
|
177
|
+
"""
|
|
178
|
+
Ends a dialog by popping it off the stack and returns an optional result to the dialog's
|
|
179
|
+
parent. The parent dialog is the dialog that started the dialog being ended via a call to
|
|
180
|
+
either "begin_dialog" or "prompt".
|
|
181
|
+
The parent dialog will have its `Dialog.resume_dialog()` method invoked with any returned
|
|
182
|
+
result. If the parent dialog hasn't implemented a `resume_dialog()` method then it will be
|
|
183
|
+
automatically ended as well and the result passed to its parent. If there are no more
|
|
184
|
+
parent dialogs on the stack then processing of the turn will end.
|
|
185
|
+
:param result: (Optional) result to pass to the parent dialogs.
|
|
186
|
+
:return:
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
await self.end_active_dialog(DialogReason.EndCalled)
|
|
190
|
+
|
|
191
|
+
# Resume previous dialog
|
|
192
|
+
if self.active_dialog is not None:
|
|
193
|
+
# Look up dialog
|
|
194
|
+
dialog = await self.find_dialog(self.active_dialog.id)
|
|
195
|
+
if not dialog:
|
|
196
|
+
raise Exception(
|
|
197
|
+
"DialogContext.EndDialogAsync(): Can't resume previous dialog."
|
|
198
|
+
" A dialog with an id of '%s' wasn't found."
|
|
199
|
+
% self.active_dialog.id
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Return result to previous dialog
|
|
203
|
+
return await dialog.resume_dialog(self, DialogReason.EndCalled, result)
|
|
204
|
+
|
|
205
|
+
return DialogTurnResult(DialogTurnStatus.Complete, result)
|
|
206
|
+
except Exception as err:
|
|
207
|
+
self.__set_exception_context_data(err)
|
|
208
|
+
raise
|
|
209
|
+
|
|
210
|
+
async def cancel_all_dialogs(
|
|
211
|
+
self,
|
|
212
|
+
cancel_parents: bool | None = None,
|
|
213
|
+
event_name: str | None = None,
|
|
214
|
+
event_value: object = None,
|
|
215
|
+
):
|
|
216
|
+
"""
|
|
217
|
+
Deletes any existing dialog stack thus cancelling all dialogs on the stack.
|
|
218
|
+
:param cancel_parents:
|
|
219
|
+
:param event_name:
|
|
220
|
+
:param event_value:
|
|
221
|
+
:return:
|
|
222
|
+
"""
|
|
223
|
+
try:
|
|
224
|
+
event_name = event_name or DialogEvents.cancel_dialog
|
|
225
|
+
if self.stack or self.parent:
|
|
226
|
+
# Cancel all local and parent dialogs while checking for interception
|
|
227
|
+
notify = False
|
|
228
|
+
dialog_context = self
|
|
229
|
+
|
|
230
|
+
while dialog_context:
|
|
231
|
+
if dialog_context.stack:
|
|
232
|
+
# Check to see if the dialog wants to handle the event
|
|
233
|
+
if notify:
|
|
234
|
+
event_handled = await dialog_context.emit_event(
|
|
235
|
+
event_name,
|
|
236
|
+
event_value,
|
|
237
|
+
bubble=False,
|
|
238
|
+
from_leaf=False,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if event_handled:
|
|
242
|
+
break
|
|
243
|
+
|
|
244
|
+
# End the active dialog
|
|
245
|
+
await dialog_context.end_active_dialog(
|
|
246
|
+
DialogReason.CancelCalled
|
|
247
|
+
)
|
|
248
|
+
else:
|
|
249
|
+
dialog_context = (
|
|
250
|
+
dialog_context.parent if cancel_parents else None
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
notify = True
|
|
254
|
+
|
|
255
|
+
return DialogTurnResult(DialogTurnStatus.Cancelled)
|
|
256
|
+
|
|
257
|
+
# Stack was empty and no parent
|
|
258
|
+
return DialogTurnResult(DialogTurnStatus.Empty)
|
|
259
|
+
except Exception as err:
|
|
260
|
+
self.__set_exception_context_data(err)
|
|
261
|
+
raise
|
|
262
|
+
|
|
263
|
+
async def find_dialog(self, dialog_id: str | None) -> Dialog | None:
|
|
264
|
+
"""
|
|
265
|
+
If the dialog cannot be found within the current `DialogSet`, the parent `DialogContext`
|
|
266
|
+
will be searched if there is one.
|
|
267
|
+
:param dialog_id: ID of the dialog to search for.
|
|
268
|
+
:return:
|
|
269
|
+
"""
|
|
270
|
+
try:
|
|
271
|
+
dialog = await self.dialogs.find(dialog_id)
|
|
272
|
+
|
|
273
|
+
if dialog is None and self.parent is not None:
|
|
274
|
+
dialog = await self.parent.find_dialog(dialog_id)
|
|
275
|
+
return dialog
|
|
276
|
+
except Exception as err:
|
|
277
|
+
self.__set_exception_context_data(err)
|
|
278
|
+
raise
|
|
279
|
+
|
|
280
|
+
def find_dialog_sync(self, dialog_id: str | None) -> Dialog | None:
|
|
281
|
+
"""
|
|
282
|
+
If the dialog cannot be found within the current `DialogSet`, the parent `DialogContext`
|
|
283
|
+
will be searched if there is one.
|
|
284
|
+
:param dialog_id: ID of the dialog to search for.
|
|
285
|
+
:return:
|
|
286
|
+
"""
|
|
287
|
+
dialog = self.dialogs.find_dialog(dialog_id)
|
|
288
|
+
|
|
289
|
+
if dialog is None and self.parent is not None:
|
|
290
|
+
dialog = self.parent.find_dialog_sync(dialog_id)
|
|
291
|
+
return dialog
|
|
292
|
+
|
|
293
|
+
async def replace_dialog(
|
|
294
|
+
self, dialog_id: str, options: object = None
|
|
295
|
+
) -> DialogTurnResult:
|
|
296
|
+
"""
|
|
297
|
+
Ends the active dialog and starts a new dialog in its place. This is particularly useful
|
|
298
|
+
for creating loops or redirecting to another dialog.
|
|
299
|
+
:param dialog_id: ID of the dialog to search for.
|
|
300
|
+
:param options: (Optional) additional argument(s) to pass to the new dialog.
|
|
301
|
+
:return:
|
|
302
|
+
"""
|
|
303
|
+
try:
|
|
304
|
+
# End the current dialog and giving the reason.
|
|
305
|
+
await self.end_active_dialog(DialogReason.ReplaceCalled)
|
|
306
|
+
|
|
307
|
+
# Start replacement dialog
|
|
308
|
+
return await self.begin_dialog(dialog_id, options)
|
|
309
|
+
except Exception as err:
|
|
310
|
+
self.__set_exception_context_data(err)
|
|
311
|
+
raise
|
|
312
|
+
|
|
313
|
+
async def reprompt_dialog(self):
|
|
314
|
+
"""
|
|
315
|
+
Calls reprompt on the currently active dialog, if there is one. Used with Prompts that have a reprompt behavior.
|
|
316
|
+
:return:
|
|
317
|
+
"""
|
|
318
|
+
try:
|
|
319
|
+
# Check for a dialog on the stack
|
|
320
|
+
if self.active_dialog is not None:
|
|
321
|
+
# Look up dialog
|
|
322
|
+
dialog = await self.find_dialog(self.active_dialog.id)
|
|
323
|
+
if not dialog:
|
|
324
|
+
raise Exception(
|
|
325
|
+
"DialogSet.reprompt_dialog(): Can't find A dialog with an id of '%s'."
|
|
326
|
+
% self.active_dialog.id
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Ask dialog to re-prompt if supported
|
|
330
|
+
await dialog.reprompt_dialog(self.context, self.active_dialog)
|
|
331
|
+
except Exception as err:
|
|
332
|
+
self.__set_exception_context_data(err)
|
|
333
|
+
raise
|
|
334
|
+
|
|
335
|
+
async def end_active_dialog(self, reason: DialogReason):
|
|
336
|
+
"""Pops the active dialog off the stack and notifies it of the reason it ended.
|
|
337
|
+
|
|
338
|
+
:param reason: The reason the dialog is ending (e.g. EndCalled, CancelCalled).
|
|
339
|
+
"""
|
|
340
|
+
instance = self.active_dialog
|
|
341
|
+
if instance is not None:
|
|
342
|
+
# Look up dialog
|
|
343
|
+
dialog = await self.find_dialog(instance.id)
|
|
344
|
+
if dialog is not None:
|
|
345
|
+
# Notify dialog of end
|
|
346
|
+
await dialog.end_dialog(self.context, instance, reason)
|
|
347
|
+
|
|
348
|
+
# Pop dialog off stack
|
|
349
|
+
self._stack.pop(0)
|
|
350
|
+
|
|
351
|
+
async def emit_event(
|
|
352
|
+
self,
|
|
353
|
+
name: str,
|
|
354
|
+
value: object = None,
|
|
355
|
+
bubble: bool = True,
|
|
356
|
+
from_leaf: bool = False,
|
|
357
|
+
) -> bool:
|
|
358
|
+
"""
|
|
359
|
+
Searches for a dialog with a given ID.
|
|
360
|
+
Emits a named event for the current dialog, or someone who started it, to handle.
|
|
361
|
+
:param name: Name of the event to raise.
|
|
362
|
+
:param value: Value to send along with the event.
|
|
363
|
+
:param bubble: Flag to control whether the event should be bubbled to its parent if not handled locally.
|
|
364
|
+
Defaults to a value of `True`.
|
|
365
|
+
:param from_leaf: Whether the event is emitted from a leaf node.
|
|
366
|
+
:param cancellationToken: The cancellation token.
|
|
367
|
+
:return: True if the event was handled.
|
|
368
|
+
"""
|
|
369
|
+
try:
|
|
370
|
+
# Initialize event
|
|
371
|
+
dialog_event = DialogEvent(
|
|
372
|
+
bubble=bubble,
|
|
373
|
+
name=name,
|
|
374
|
+
value=value,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
dialog_context = self
|
|
378
|
+
|
|
379
|
+
# Find starting dialog
|
|
380
|
+
if from_leaf:
|
|
381
|
+
while True:
|
|
382
|
+
child_dc = dialog_context.child
|
|
383
|
+
|
|
384
|
+
if child_dc:
|
|
385
|
+
dialog_context = child_dc
|
|
386
|
+
else:
|
|
387
|
+
break
|
|
388
|
+
|
|
389
|
+
# Dispatch to active dialog first
|
|
390
|
+
instance = dialog_context.active_dialog
|
|
391
|
+
|
|
392
|
+
if instance:
|
|
393
|
+
dialog = await dialog_context.find_dialog(instance.id)
|
|
394
|
+
|
|
395
|
+
if dialog:
|
|
396
|
+
return await dialog.on_dialog_event(dialog_context, dialog_event)
|
|
397
|
+
|
|
398
|
+
return False
|
|
399
|
+
except Exception as err:
|
|
400
|
+
self.__set_exception_context_data(err)
|
|
401
|
+
raise
|
|
402
|
+
|
|
403
|
+
def __set_exception_context_data(self, exception: Exception):
|
|
404
|
+
if not hasattr(exception, "data"):
|
|
405
|
+
setattr(exception, "data", {})
|
|
406
|
+
|
|
407
|
+
data = getattr(exception, "data")
|
|
408
|
+
if not type(self).__name__ in data:
|
|
409
|
+
stack = []
|
|
410
|
+
current_dc = self
|
|
411
|
+
|
|
412
|
+
while current_dc is not None:
|
|
413
|
+
stack = stack + [x.id for x in current_dc.stack]
|
|
414
|
+
current_dc = current_dc.parent
|
|
415
|
+
|
|
416
|
+
parent_active_id = None
|
|
417
|
+
if self.parent is not None and self.parent.active_dialog is not None:
|
|
418
|
+
parent_active_id = self.parent.active_dialog.id
|
|
419
|
+
|
|
420
|
+
data[type(self).__name__] = {
|
|
421
|
+
"active_dialog": (
|
|
422
|
+
None if self.active_dialog is None else self.active_dialog.id
|
|
423
|
+
),
|
|
424
|
+
"parent": parent_active_id,
|
|
425
|
+
"stack": self.stack,
|
|
426
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
from microsoft_agents.hosting.core import (
|
|
5
|
+
ClaimsIdentity,
|
|
6
|
+
ChannelAdapter,
|
|
7
|
+
StatePropertyAccessor,
|
|
8
|
+
TurnContext,
|
|
9
|
+
)
|
|
10
|
+
from microsoft_agents.activity import Activity, ActivityTypes, EndOfConversationCodes
|
|
11
|
+
|
|
12
|
+
from microsoft_agents.hosting.dialogs.memory import DialogStateManager
|
|
13
|
+
from .dialog import Dialog
|
|
14
|
+
from .dialog_context import DialogContext
|
|
15
|
+
from .models import DialogTurnResult
|
|
16
|
+
from .models.dialog_events import DialogEvents
|
|
17
|
+
from .dialog_set import DialogSet
|
|
18
|
+
from .models.dialog_turn_status import DialogTurnStatus
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DialogExtensions:
|
|
22
|
+
@staticmethod
|
|
23
|
+
async def run_dialog(
|
|
24
|
+
dialog: Dialog,
|
|
25
|
+
turn_context: TurnContext,
|
|
26
|
+
accessor: StatePropertyAccessor,
|
|
27
|
+
):
|
|
28
|
+
"""
|
|
29
|
+
Creates a dialog stack and starts a dialog, pushing it onto the stack.
|
|
30
|
+
"""
|
|
31
|
+
dialog_set = DialogSet(accessor)
|
|
32
|
+
dialog_set.add(dialog)
|
|
33
|
+
|
|
34
|
+
dialog_context: DialogContext = await dialog_set.create_context(turn_context)
|
|
35
|
+
|
|
36
|
+
await DialogExtensions._internal_run(turn_context, dialog.id, dialog_context)
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
async def _internal_run(
|
|
40
|
+
context: TurnContext, dialog_id: str, dialog_context: DialogContext
|
|
41
|
+
) -> DialogTurnResult:
|
|
42
|
+
# map TurnState into root dialog context.services
|
|
43
|
+
for key, service in context.turn_state.items():
|
|
44
|
+
dialog_context.services[key] = service
|
|
45
|
+
|
|
46
|
+
# get the DialogStateManager configuration
|
|
47
|
+
dialog_state_manager = DialogStateManager(dialog_context)
|
|
48
|
+
await dialog_state_manager.load_all_scopes()
|
|
49
|
+
dialog_context.context.turn_state[dialog_state_manager.__class__.__name__] = (
|
|
50
|
+
dialog_state_manager
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Loop as long as we are getting valid OnError handled we should continue executing the actions for the turn.
|
|
54
|
+
end_of_turn = False
|
|
55
|
+
dialog_turn_result: DialogTurnResult = DialogTurnResult(DialogTurnStatus.Empty)
|
|
56
|
+
while not end_of_turn:
|
|
57
|
+
try:
|
|
58
|
+
dialog_turn_result = await DialogExtensions.__inner_run(
|
|
59
|
+
context, dialog_id, dialog_context
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# turn successfully completed, break the loop
|
|
63
|
+
end_of_turn = True
|
|
64
|
+
except Exception as err:
|
|
65
|
+
# fire error event, bubbling from the leaf.
|
|
66
|
+
handled = await dialog_context.emit_event(
|
|
67
|
+
DialogEvents.error, err, bubble=True, from_leaf=True
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if not handled:
|
|
71
|
+
# error was NOT handled, throw the exception and end the turn. (This will trigger the
|
|
72
|
+
# Adapter.OnError handler and end the entire dialog stack)
|
|
73
|
+
raise
|
|
74
|
+
|
|
75
|
+
# save all state scopes to their respective AgentState locations.
|
|
76
|
+
await dialog_state_manager.save_all_changes()
|
|
77
|
+
|
|
78
|
+
# return the result
|
|
79
|
+
return dialog_turn_result
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
async def __inner_run(
|
|
83
|
+
turn_context: TurnContext, dialog_id: str, dialog_context: DialogContext
|
|
84
|
+
) -> DialogTurnResult:
|
|
85
|
+
# Handle EoC and Reprompt event from a parent bot (can be root bot to skill or skill to skill)
|
|
86
|
+
if DialogExtensions.__is_from_parent_to_skill(turn_context):
|
|
87
|
+
# Handle remote cancellation request from parent.
|
|
88
|
+
if turn_context.activity.type == ActivityTypes.end_of_conversation:
|
|
89
|
+
if not dialog_context.stack:
|
|
90
|
+
# No dialogs to cancel, just return.
|
|
91
|
+
return DialogTurnResult(DialogTurnStatus.Empty)
|
|
92
|
+
|
|
93
|
+
# Send cancellation message to the dialog to ensure all the parents are canceled
|
|
94
|
+
# in the right order.
|
|
95
|
+
return await dialog_context.cancel_all_dialogs(True)
|
|
96
|
+
|
|
97
|
+
# Handle a reprompt event sent from the parent.
|
|
98
|
+
if (
|
|
99
|
+
turn_context.activity.type == ActivityTypes.event
|
|
100
|
+
and turn_context.activity.name == DialogEvents.reprompt_dialog
|
|
101
|
+
):
|
|
102
|
+
if not dialog_context.stack:
|
|
103
|
+
# No dialogs to reprompt, just return.
|
|
104
|
+
return DialogTurnResult(DialogTurnStatus.Empty)
|
|
105
|
+
|
|
106
|
+
await dialog_context.reprompt_dialog()
|
|
107
|
+
return DialogTurnResult(DialogTurnStatus.Waiting)
|
|
108
|
+
|
|
109
|
+
# Continue or start the dialog.
|
|
110
|
+
result = await dialog_context.continue_dialog()
|
|
111
|
+
if result.status == DialogTurnStatus.Empty:
|
|
112
|
+
result = await dialog_context.begin_dialog(dialog_id)
|
|
113
|
+
|
|
114
|
+
await DialogExtensions._send_state_snapshot_trace(dialog_context)
|
|
115
|
+
|
|
116
|
+
# Skills should send EoC when the dialog completes.
|
|
117
|
+
if (
|
|
118
|
+
result.status == DialogTurnStatus.Complete
|
|
119
|
+
or result.status == DialogTurnStatus.Cancelled
|
|
120
|
+
):
|
|
121
|
+
if DialogExtensions.__send_eoc_to_parent(turn_context):
|
|
122
|
+
activity = Activity( # type: ignore[call-arg]
|
|
123
|
+
type=ActivityTypes.end_of_conversation,
|
|
124
|
+
value=result.result,
|
|
125
|
+
locale=turn_context.activity.locale,
|
|
126
|
+
code=(
|
|
127
|
+
EndOfConversationCodes.completed_successfully
|
|
128
|
+
if result.status == DialogTurnStatus.Complete
|
|
129
|
+
else EndOfConversationCodes.user_cancelled
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
await turn_context.send_activity(activity)
|
|
133
|
+
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def __is_from_parent_to_skill(turn_context: TurnContext) -> bool:
|
|
138
|
+
"""
|
|
139
|
+
Determines if this turn is an incoming request from a parent bot to this skill.
|
|
140
|
+
"""
|
|
141
|
+
claims_identity = turn_context.turn_state.get(
|
|
142
|
+
ChannelAdapter.AGENT_IDENTITY_KEY, None
|
|
143
|
+
)
|
|
144
|
+
return (
|
|
145
|
+
isinstance(claims_identity, ClaimsIdentity)
|
|
146
|
+
and claims_identity.is_agent_claim()
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
async def _send_state_snapshot_trace(dialog_context: DialogContext):
|
|
151
|
+
"""
|
|
152
|
+
Helper to send a trace activity with a memory snapshot of the active dialog DC.
|
|
153
|
+
"""
|
|
154
|
+
claims_identity = dialog_context.context.turn_state.get(
|
|
155
|
+
ChannelAdapter.AGENT_IDENTITY_KEY, None
|
|
156
|
+
)
|
|
157
|
+
trace_label = (
|
|
158
|
+
"Skill State"
|
|
159
|
+
if isinstance(claims_identity, ClaimsIdentity)
|
|
160
|
+
and claims_identity.is_agent_claim()
|
|
161
|
+
else "Bot State"
|
|
162
|
+
)
|
|
163
|
+
# send trace of memory
|
|
164
|
+
snapshot = DialogExtensions._get_active_dialog_context(
|
|
165
|
+
dialog_context
|
|
166
|
+
).state.get_memory_snapshot()
|
|
167
|
+
trace_activity = Activity( # type: ignore[call-arg]
|
|
168
|
+
type=ActivityTypes.trace,
|
|
169
|
+
name="BotState",
|
|
170
|
+
value_type="https://www.botframework.com/schemas/botState",
|
|
171
|
+
value=snapshot,
|
|
172
|
+
label=trace_label,
|
|
173
|
+
)
|
|
174
|
+
await dialog_context.context.send_activity(trace_activity)
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def __send_eoc_to_parent(turn_context: TurnContext) -> bool:
|
|
178
|
+
"""
|
|
179
|
+
Determines whether to send an EndOfConversation to the parent bot.
|
|
180
|
+
"""
|
|
181
|
+
claims_identity = turn_context.turn_state.get(
|
|
182
|
+
ChannelAdapter.AGENT_IDENTITY_KEY, None
|
|
183
|
+
)
|
|
184
|
+
if (
|
|
185
|
+
isinstance(claims_identity, ClaimsIdentity)
|
|
186
|
+
and claims_identity.is_agent_claim()
|
|
187
|
+
):
|
|
188
|
+
return True
|
|
189
|
+
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def _get_active_dialog_context(dialog_context: DialogContext) -> DialogContext:
|
|
194
|
+
"""
|
|
195
|
+
Recursively walk up the DC stack to find the active DC.
|
|
196
|
+
"""
|
|
197
|
+
child = dialog_context.child
|
|
198
|
+
if not child:
|
|
199
|
+
return dialog_context
|
|
200
|
+
|
|
201
|
+
return DialogExtensions._get_active_dialog_context(child)
|