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,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)