Skip to content

Commit 34e6109

Browse files
DialogManager (#1409)
* Initial commit for dialog manager **WIP state** * Adding more memory classes * memory scopes and path resolvers added * Updates on try_get_value * DialogStateManager code complete * Dialog manager code complete (tests pending) * Solved circular dependency issues, bugfix in DialogCOmponentRegistration * Pylint compliance and bugfixing * Reverting regression in DialogManager * Compatibility with 3.6 typing * General DialogManager testing added. Several bugfixes * Added tests for Dialog Manager * Fixing ClassMemoryScope binding, adding tests for scopes classes * ConversationState scope test * Adding more scopes tests * Added all scopes tests * Fixing printing because of merge conflict * PR comments fixes Co-authored-by: tracyboehrer <tracyboehrer@users.noreply.github.com>
1 parent 1e6fa28 commit 34e6109

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+3337
-24
lines changed

libraries/botbuilder-core/botbuilder/core/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from .bot_telemetry_client import BotTelemetryClient, Severity
1919
from .card_factory import CardFactory
2020
from .channel_service_handler import BotActionNotImplementedError, ChannelServiceHandler
21+
from .component_registration import ComponentRegistration
2122
from .conversation_state import ConversationState
2223
from .oauth.extended_user_token_provider import ExtendedUserTokenProvider
2324
from .oauth.user_token_provider import UserTokenProvider
@@ -62,6 +63,7 @@
6263
"calculate_change_hash",
6364
"CardFactory",
6465
"ChannelServiceHandler",
66+
"ComponentRegistration",
6567
"ConversationState",
6668
"conversation_reference_extension",
6769
"ExtendedUserTokenProvider",

libraries/botbuilder-core/botbuilder/core/bot_state.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
class CachedBotState:
1616
"""
17-
Internal cached bot state.
17+
Internal cached bot state.
1818
"""
1919

2020
def __init__(self, state: Dict[str, object] = None):
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from typing import Dict, Iterable, Type
5+
6+
7+
class ComponentRegistration:
8+
@staticmethod
9+
def get_components() -> Iterable["ComponentRegistration"]:
10+
return _components.values()
11+
12+
@staticmethod
13+
def add(component_registration: "ComponentRegistration"):
14+
_components[component_registration.__class__] = component_registration
15+
16+
17+
_components: Dict[Type, ComponentRegistration] = {}

libraries/botbuilder-core/botbuilder/core/conversation_state.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def __init__(self, storage: Storage):
2525
:param storage: The storage containing the conversation state.
2626
:type storage: :class:`Storage`
2727
"""
28-
super(ConversationState, self).__init__(storage, "ConversationState")
28+
super(ConversationState, self).__init__(storage, "Internal.ConversationState")
2929

3030
def get_storage_key(self, turn_context: TurnContext) -> object:
3131
"""

libraries/botbuilder-core/botbuilder/core/user_state.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def __init__(self, storage: Storage, namespace=""):
2323
"""
2424
self.namespace = namespace
2525

26-
super(UserState, self).__init__(storage, "UserState")
26+
super(UserState, self).__init__(storage, "Internal.UserState")
2727

2828
def get_storage_key(self, turn_context: TurnContext) -> str:
2929
"""

libraries/botbuilder-dialogs/botbuilder/dialogs/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,22 @@
77

88
from .about import __version__
99
from .component_dialog import ComponentDialog
10+
from .dialog_container import DialogContainer
1011
from .dialog_context import DialogContext
12+
from .dialog_event import DialogEvent
1113
from .dialog_events import DialogEvents
1214
from .dialog_instance import DialogInstance
1315
from .dialog_reason import DialogReason
1416
from .dialog_set import DialogSet
1517
from .dialog_state import DialogState
1618
from .dialog_turn_result import DialogTurnResult
1719
from .dialog_turn_status import DialogTurnStatus
20+
from .dialog_manager import DialogManager
21+
from .dialog_manager_result import DialogManagerResult
1822
from .dialog import Dialog
23+
from .dialogs_component_registration import DialogsComponentRegistration
24+
from .persisted_state_keys import PersistedStateKeys
25+
from .persisted_state import PersistedState
1926
from .waterfall_dialog import WaterfallDialog
2027
from .waterfall_step_context import WaterfallStepContext
2128
from .dialog_extensions import DialogExtensions
@@ -26,15 +33,20 @@
2633

2734
__all__ = [
2835
"ComponentDialog",
36+
"DialogContainer",
2937
"DialogContext",
38+
"DialogEvent",
3039
"DialogEvents",
3140
"DialogInstance",
3241
"DialogReason",
3342
"DialogSet",
3443
"DialogState",
3544
"DialogTurnResult",
3645
"DialogTurnStatus",
46+
"DialogManager",
47+
"DialogManagerResult",
3748
"Dialog",
49+
"DialogsComponentRegistration",
3850
"WaterfallDialog",
3951
"WaterfallStepContext",
4052
"ConfirmPrompt",
@@ -43,6 +55,8 @@
4355
"NumberPrompt",
4456
"OAuthPrompt",
4557
"OAuthPromptSettings",
58+
"PersistedStateKeys",
59+
"PersistedState",
4660
"PromptRecognizerResult",
4761
"PromptValidatorContext",
4862
"Prompt",

libraries/botbuilder-dialogs/botbuilder/dialogs/dialog.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from botbuilder.core import TurnContext, NullTelemetryClient, BotTelemetryClient
66
from .dialog_reason import DialogReason
7+
from .dialog_event import DialogEvent
78
from .dialog_turn_status import DialogTurnStatus
89
from .dialog_turn_result import DialogTurnResult
910
from .dialog_instance import DialogInstance
@@ -105,3 +106,83 @@ async def end_dialog( # pylint: disable=unused-argument
105106
"""
106107
# No-op by default
107108
return
109+
110+
def get_version(self) -> str:
111+
return self.id
112+
113+
async def on_dialog_event(
114+
self, dialog_context: "DialogContext", dialog_event: DialogEvent
115+
) -> bool:
116+
"""
117+
Called when an event has been raised, using `DialogContext.emitEvent()`, by either the current dialog or a
118+
dialog that the current dialog started.
119+
:param dialog_context: The dialog context for the current turn of conversation.
120+
:param dialog_event: The event being raised.
121+
:return: True if the event is handled by the current dialog and bubbling should stop.
122+
"""
123+
# Before bubble
124+
handled = await self._on_pre_bubble_event(dialog_context, dialog_event)
125+
126+
# Bubble as needed
127+
if (not handled) and dialog_event.bubble and dialog_context.parent:
128+
handled = await dialog_context.parent.emit(
129+
dialog_event.name, dialog_event.value, True, False
130+
)
131+
132+
# Post bubble
133+
if not handled:
134+
handled = await self._on_post_bubble_event(dialog_context, dialog_event)
135+
136+
return handled
137+
138+
async def _on_pre_bubble_event( # pylint: disable=unused-argument
139+
self, dialog_context: "DialogContext", dialog_event: DialogEvent
140+
) -> bool:
141+
"""
142+
Called before an event is bubbled to its parent.
143+
This is a good place to perform interception of an event as returning `true` will prevent
144+
any further bubbling of the event to the dialogs parents and will also prevent any child
145+
dialogs from performing their default processing.
146+
:param dialog_context: The dialog context for the current turn of conversation.
147+
:param dialog_event: The event being raised.
148+
:return: Whether the event is handled by the current dialog and further processing should stop.
149+
"""
150+
return False
151+
152+
async def _on_post_bubble_event( # pylint: disable=unused-argument
153+
self, dialog_context: "DialogContext", dialog_event: DialogEvent
154+
) -> bool:
155+
"""
156+
Called after an event was bubbled to all parents and wasn't handled.
157+
This is a good place to perform default processing logic for an event. Returning `true` will
158+
prevent any processing of the event by child dialogs.
159+
:param dialog_context: The dialog context for the current turn of conversation.
160+
:param dialog_event: The event being raised.
161+
:return: Whether the event is handled by the current dialog and further processing should stop.
162+
"""
163+
return False
164+
165+
def _on_compute_id(self) -> str:
166+
"""
167+
Computes an unique ID for a dialog.
168+
:return: An unique ID for a dialog
169+
"""
170+
return self.__class__.__name__
171+
172+
def _register_source_location(
173+
self, path: str, line_number: int
174+
): # pylint: disable=unused-argument
175+
"""
176+
Registers a SourceRange in the provided location.
177+
:param path: The path to the source file.
178+
:param line_number: The line number where the source will be located on the file.
179+
:return:
180+
"""
181+
if path:
182+
# This will be added when debbuging support is ported.
183+
# DebugSupport.source_map.add(self, SourceRange(
184+
# path = path,
185+
# start_point = SourcePoint(line_index = line_number, char_index = 0 ),
186+
# end_point = SourcePoint(line_index = line_number + 1, char_index = 0 ),
187+
# )
188+
return
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from abc import ABC, abstractmethod
5+
6+
7+
from .dialog import Dialog
8+
from .dialog_context import DialogContext
9+
from .dialog_event import DialogEvent
10+
from .dialog_events import DialogEvents
11+
from .dialog_set import DialogSet
12+
13+
14+
class DialogContainer(Dialog, ABC):
15+
def __init__(self, dialog_id: str = None):
16+
super().__init__(dialog_id)
17+
18+
self.dialogs = DialogSet()
19+
20+
@abstractmethod
21+
def create_child_context(self, dialog_context: DialogContext) -> DialogContext:
22+
raise NotImplementedError()
23+
24+
def find_dialog(self, dialog_id: str) -> Dialog:
25+
# TODO: deprecate DialogSet.find
26+
return self.dialogs.find_dialog(dialog_id)
27+
28+
async def on_dialog_event(
29+
self, dialog_context: DialogContext, dialog_event: DialogEvent
30+
) -> bool:
31+
"""
32+
Called when an event has been raised, using `DialogContext.emitEvent()`, by either the current dialog or a
33+
dialog that the current dialog started.
34+
:param dialog_context: The dialog context for the current turn of conversation.
35+
:param dialog_event: The event being raised.
36+
:return: True if the event is handled by the current dialog and bubbling should stop.
37+
"""
38+
handled = await super().on_dialog_event(dialog_context, dialog_event)
39+
40+
# Trace unhandled "versionChanged" events.
41+
if not handled and dialog_event.name == DialogEvents.version_changed:
42+
43+
trace_message = (
44+
f"Unhandled dialog event: {dialog_event.name}. Active Dialog: "
45+
f"{dialog_context.active_dialog.id}"
46+
)
47+
48+
await dialog_context.context.send_trace_activity(trace_message)
49+
50+
return handled
51+
52+
def get_internal_version(self) -> str:
53+
"""
54+
GetInternalVersion - Returns internal version identifier for this container.
55+
DialogContainers detect changes of all sub-components in the container and map that to an DialogChanged event.
56+
Because they do this, DialogContainers "hide" the internal changes and just have the .id. This isolates changes
57+
to the container level unless a container doesn't handle it. To support this DialogContainers define a
58+
protected virtual method GetInternalVersion() which computes if this dialog or child dialogs have changed
59+
which is then examined via calls to check_for_version_change_async().
60+
:return: version which represents the change of the internals of this container.
61+
"""
62+
return self.dialogs.get_version()
63+
64+
async def check_for_version_change_async(self, dialog_context: DialogContext):
65+
"""
66+
:param dialog_context: dialog context.
67+
:return: task.
68+
Checks to see if a containers child dialogs have changed since the current dialog instance
69+
was started.
70+
71+
This should be called at the start of `beginDialog()`, `continueDialog()`, and `resumeDialog()`.
72+
"""
73+
current = dialog_context.active_dialog.version
74+
dialog_context.active_dialog.version = self.get_internal_version()
75+
76+
# Check for change of previously stored hash
77+
if current and current != dialog_context.active_dialog.version:
78+
# Give bot an opportunity to handle the change.
79+
# - If bot handles it the changeHash will have been updated as to avoid triggering the
80+
# change again.
81+
await dialog_context.emit_event(
82+
DialogEvents.version_changed, self.id, True, False
83+
)

0 commit comments

Comments
 (0)