Skip to content

DialogManager #1409

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Feb 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3d20366
Initial commit for dialog manager **WIP state**
axelsrz Oct 15, 2020
7e884bb
Adding more memory classes
axelsrz Oct 16, 2020
faf1b54
memory scopes and path resolvers added
axelsrz Oct 28, 2020
e867a0e
Updates on try_get_value
axelsrz Nov 5, 2020
3abc979
DialogStateManager code complete
axelsrz Nov 9, 2020
40fa13d
Dialog manager code complete (tests pending)
axelsrz Nov 10, 2020
80f2ba5
Solved circular dependency issues, bugfix in DialogCOmponentRegistration
axelsrz Nov 10, 2020
6153d76
Pylint compliance and bugfixing
axelsrz Nov 13, 2020
a3f08b8
Reverting regression in DialogManager
axelsrz Nov 13, 2020
ff3f5b5
Compatibility with 3.6 typing
axelsrz Nov 13, 2020
6311528
Merge branch 'main' into axsuarez/DialogManager
axelsrz Nov 13, 2020
d353076
General DialogManager testing added. Several bugfixes
axelsrz Nov 19, 2020
d9919fb
Added tests for Dialog Manager
axelsrz Nov 24, 2020
7d40eed
Fixing ClassMemoryScope binding, adding tests for scopes classes
axelsrz Nov 25, 2020
17c343f
ConversationState scope test
axelsrz Dec 2, 2020
ef26d5f
Adding more scopes tests
axelsrz Dec 8, 2020
1961529
Added all scopes tests
axelsrz Dec 19, 2020
5b40e2d
Merge branch 'main' into axsuarez/DialogManager
axelsrz Jan 6, 2021
ec96e65
Fixing printing because of merge conflict
axelsrz Jan 7, 2021
9538fc6
PR comments fixes
axelsrz Jan 24, 2021
a76883b
Merge branch 'main' into axsuarez/DialogManager
axelsrz Jan 24, 2021
1ac9243
Merge branch 'main' into axsuarez/DialogManager
tracyboehrer Feb 1, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions libraries/botbuilder-core/botbuilder/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .bot_telemetry_client import BotTelemetryClient, Severity
from .card_factory import CardFactory
from .channel_service_handler import BotActionNotImplementedError, ChannelServiceHandler
from .component_registration import ComponentRegistration
from .conversation_state import ConversationState
from .oauth.extended_user_token_provider import ExtendedUserTokenProvider
from .oauth.user_token_provider import UserTokenProvider
Expand Down Expand Up @@ -62,6 +63,7 @@
"calculate_change_hash",
"CardFactory",
"ChannelServiceHandler",
"ComponentRegistration",
"ConversationState",
"conversation_reference_extension",
"ExtendedUserTokenProvider",
Expand Down
2 changes: 1 addition & 1 deletion libraries/botbuilder-core/botbuilder/core/bot_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

class CachedBotState:
"""
Internal cached bot state.
Internal cached bot state.
"""

def __init__(self, state: Dict[str, object] = None):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from typing import Dict, Iterable, Type


class ComponentRegistration:
@staticmethod
def get_components() -> Iterable["ComponentRegistration"]:
return _components.values()

@staticmethod
def add(component_registration: "ComponentRegistration"):
_components[component_registration.__class__] = component_registration


_components: Dict[Type, ComponentRegistration] = {}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def __init__(self, storage: Storage):
:param storage: The storage containing the conversation state.
:type storage: :class:`Storage`
"""
super(ConversationState, self).__init__(storage, "ConversationState")
super(ConversationState, self).__init__(storage, "Internal.ConversationState")

def get_storage_key(self, turn_context: TurnContext) -> object:
"""
Expand Down
2 changes: 1 addition & 1 deletion libraries/botbuilder-core/botbuilder/core/user_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def __init__(self, storage: Storage, namespace=""):
"""
self.namespace = namespace

super(UserState, self).__init__(storage, "UserState")
super(UserState, self).__init__(storage, "Internal.UserState")

def get_storage_key(self, turn_context: TurnContext) -> str:
"""
Expand Down
14 changes: 14 additions & 0 deletions libraries/botbuilder-dialogs/botbuilder/dialogs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,22 @@

from .about import __version__
from .component_dialog import ComponentDialog
from .dialog_container import DialogContainer
from .dialog_context import DialogContext
from .dialog_event import DialogEvent
from .dialog_events import DialogEvents
from .dialog_instance import DialogInstance
from .dialog_reason import DialogReason
from .dialog_set import DialogSet
from .dialog_state import DialogState
from .dialog_turn_result import DialogTurnResult
from .dialog_turn_status import DialogTurnStatus
from .dialog_manager import DialogManager
from .dialog_manager_result import DialogManagerResult
from .dialog import Dialog
from .dialogs_component_registration import DialogsComponentRegistration
from .persisted_state_keys import PersistedStateKeys
from .persisted_state import PersistedState
from .waterfall_dialog import WaterfallDialog
from .waterfall_step_context import WaterfallStepContext
from .dialog_extensions import DialogExtensions
Expand All @@ -26,15 +33,20 @@

__all__ = [
"ComponentDialog",
"DialogContainer",
"DialogContext",
"DialogEvent",
"DialogEvents",
"DialogInstance",
"DialogReason",
"DialogSet",
"DialogState",
"DialogTurnResult",
"DialogTurnStatus",
"DialogManager",
"DialogManagerResult",
"Dialog",
"DialogsComponentRegistration",
"WaterfallDialog",
"WaterfallStepContext",
"ConfirmPrompt",
Expand All @@ -43,6 +55,8 @@
"NumberPrompt",
"OAuthPrompt",
"OAuthPromptSettings",
"PersistedStateKeys",
"PersistedState",
"PromptRecognizerResult",
"PromptValidatorContext",
"Prompt",
Expand Down
81 changes: 81 additions & 0 deletions libraries/botbuilder-dialogs/botbuilder/dialogs/dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from botbuilder.core import TurnContext, NullTelemetryClient, BotTelemetryClient
from .dialog_reason import DialogReason
from .dialog_event import DialogEvent
from .dialog_turn_status import DialogTurnStatus
from .dialog_turn_result import DialogTurnResult
from .dialog_instance import DialogInstance
Expand Down Expand Up @@ -105,3 +106,83 @@ async def end_dialog( # pylint: disable=unused-argument
"""
# No-op by default
return

def get_version(self) -> str:
return self.id

async def on_dialog_event(
self, dialog_context: "DialogContext", dialog_event: DialogEvent
) -> bool:
"""
Called when an event has been raised, using `DialogContext.emitEvent()`, by either the current dialog or a
dialog that the current dialog started.
:param dialog_context: The dialog context for the current turn of conversation.
:param dialog_event: The event being raised.
:return: True if the event is handled by the current dialog and bubbling should stop.
"""
# Before bubble
handled = await self._on_pre_bubble_event(dialog_context, dialog_event)

# Bubble as needed
if (not handled) and dialog_event.bubble and dialog_context.parent:
handled = await dialog_context.parent.emit(
dialog_event.name, dialog_event.value, True, False
)

# Post bubble
if not handled:
handled = await self._on_post_bubble_event(dialog_context, dialog_event)

return handled

async def _on_pre_bubble_event( # pylint: disable=unused-argument
self, dialog_context: "DialogContext", dialog_event: DialogEvent
) -> bool:
"""
Called before an event is bubbled to its parent.
This is a good place to perform interception of an event as returning `true` will prevent
any further bubbling of the event to the dialogs parents and will also prevent any child
dialogs from performing their default processing.
:param dialog_context: The dialog context for the current turn of conversation.
:param dialog_event: The event being raised.
:return: Whether the event is handled by the current dialog and further processing should stop.
"""
return False

async def _on_post_bubble_event( # pylint: disable=unused-argument
self, dialog_context: "DialogContext", dialog_event: DialogEvent
) -> bool:
"""
Called after an event was bubbled to all parents and wasn't handled.
This is a good place to perform default processing logic for an event. Returning `true` will
prevent any processing of the event by child dialogs.
:param dialog_context: The dialog context for the current turn of conversation.
:param dialog_event: The event being raised.
:return: Whether the event is handled by the current dialog and further processing should stop.
"""
return False

def _on_compute_id(self) -> str:
"""
Computes an unique ID for a dialog.
:return: An unique ID for a dialog
"""
return self.__class__.__name__

def _register_source_location(
self, path: str, line_number: int
): # pylint: disable=unused-argument
"""
Registers a SourceRange in the provided location.
:param path: The path to the source file.
:param line_number: The line number where the source will be located on the file.
:return:
"""
if path:
# This will be added when debbuging support is ported.
# DebugSupport.source_map.add(self, SourceRange(
# path = path,
# start_point = SourcePoint(line_index = line_number, char_index = 0 ),
# end_point = SourcePoint(line_index = line_number + 1, char_index = 0 ),
# )
return
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from abc import ABC, abstractmethod


from .dialog import Dialog
from .dialog_context import DialogContext
from .dialog_event import DialogEvent
from .dialog_events import DialogEvents
from .dialog_set import DialogSet


class DialogContainer(Dialog, ABC):
def __init__(self, dialog_id: str = None):
super().__init__(dialog_id)

self.dialogs = DialogSet()

@abstractmethod
def create_child_context(self, dialog_context: DialogContext) -> DialogContext:
raise NotImplementedError()

def find_dialog(self, dialog_id: str) -> Dialog:
# TODO: deprecate DialogSet.find
return self.dialogs.find_dialog(dialog_id)

async def on_dialog_event(
self, dialog_context: DialogContext, dialog_event: DialogEvent
) -> bool:
"""
Called when an event has been raised, using `DialogContext.emitEvent()`, by either the current dialog or a
dialog that the current dialog started.
:param dialog_context: The dialog context for the current turn of conversation.
:param dialog_event: The event being raised.
:return: True if the event is handled by the current dialog and bubbling should stop.
"""
handled = await super().on_dialog_event(dialog_context, dialog_event)

# Trace unhandled "versionChanged" events.
if not handled and dialog_event.name == DialogEvents.version_changed:

trace_message = (
f"Unhandled dialog event: {dialog_event.name}. Active Dialog: "
f"{dialog_context.active_dialog.id}"
)

await dialog_context.context.send_trace_activity(trace_message)

return handled

def get_internal_version(self) -> str:
"""
GetInternalVersion - Returns internal version identifier for this container.
DialogContainers detect changes of all sub-components in the container and map that to an DialogChanged event.
Because they do this, DialogContainers "hide" the internal changes and just have the .id. This isolates changes
to the container level unless a container doesn't handle it. To support this DialogContainers define a
protected virtual method GetInternalVersion() which computes if this dialog or child dialogs have changed
which is then examined via calls to check_for_version_change_async().
:return: version which represents the change of the internals of this container.
"""
return self.dialogs.get_version()

async def check_for_version_change_async(self, dialog_context: DialogContext):
"""
:param dialog_context: dialog context.
:return: task.
Checks to see if a containers child dialogs have changed since the current dialog instance
was started.

This should be called at the start of `beginDialog()`, `continueDialog()`, and `resumeDialog()`.
"""
current = dialog_context.active_dialog.version
dialog_context.active_dialog.version = self.get_internal_version()

# Check for change of previously stored hash
if current and current != dialog_context.active_dialog.version:
# Give bot an opportunity to handle the change.
# - If bot handles it the changeHash will have been updated as to avoid triggering the
# change again.
await dialog_context.emit_event(
DialogEvents.version_changed, self.id, True, False
)
Loading