Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
175 changes: 165 additions & 10 deletions libraries/botbuilder-dialogs/botbuilder/dialogs/component_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,24 @@


class ComponentDialog(Dialog):
"""
A :class:`Dialog` that is composed of other dialogs

A component dialog has an inner :class:`DialogSet` :class:`DialogContext`,
which provides an inner dialog stack that is hidden from the parent dialog.

:var persisted_dialog state:
:vartype persisted_dialog_state: str
"""
persisted_dialog_state = "dialogs"

def __init__(self, dialog_id: str):
"""
Initializes a new instance of the :class:`ComponentDialog`

:param dialog_id: The ID to assign to the new dialog within the parent dialog set.
:type dialog_id: str
"""
super(ComponentDialog, self).__init__(dialog_id)

if dialog_id is None:
Expand All @@ -30,6 +45,19 @@ def __init__(self, dialog_id: str):
async def begin_dialog(
self, dialog_context: DialogContext, options: object = None
) -> DialogTurnResult:
"""
Called when the dialog is started and pushed onto the parent's dialog stack.

If the task is successful, the result indicates whether the dialog is still
active after the turn has been processed by the dialog.

:param dialog_context: The :class:`DialogContext` for the current turn of the conversation.
:type dialog_context: :class:`DialogContext`
:param options: Optional, initial information to pass to the dialog.
:type options: object
:return: Signals the end of the turn
:rtype: :class:`Dialog.end_of_turn`
"""
if dialog_context is None:
raise TypeError("ComponentDialog.begin_dialog(): outer_dc cannot be None.")

Expand All @@ -49,6 +77,27 @@ async def begin_dialog(
return Dialog.end_of_turn

async def continue_dialog(self, dialog_context: DialogContext) -> DialogTurnResult:
"""
Called when the dialog is continued, where it is the active dialog and the
user replies with a new activity.

.. note::
If the task is successful, the result indicates whether the dialog is still
active after the turn has been processed by the dialog. The result may also
contain a return value.

If this method is *not* overriden the component dialog calls the
:meth:`DialogContext.continue_dialog` method on it's inner dialog
context. If the inner dialog stack is empty, the component dialog ends,
and if a :class:`DialogTurnResult.result` is available, the component dialog
uses that as it's return value.


:param dialog_context: The parent :class:`DialogContext` for the current turn of the conversation.
:type dialog_context: :class:`DialogContext`
:return: Signals the end of the turn
:rtype: :class:`Dialog.end_of_turn`
"""
if dialog_context is None:
raise TypeError("ComponentDialog.begin_dialog(): outer_dc cannot be None.")
# Continue execution of inner dialog.
Expand All @@ -65,17 +114,51 @@ async def continue_dialog(self, dialog_context: DialogContext) -> DialogTurnResu
async def resume_dialog(
self, dialog_context: DialogContext, reason: DialogReason, result: object = None
) -> DialogTurnResult:
# Containers are typically leaf nodes on the stack but the dev is free to push other dialogs
# on top of the stack which will result in the container receiving an unexpected call to
# resume_dialog() when the pushed on dialog ends.
# To avoid the container prematurely ending we need to implement this method and simply
# ask our inner dialog stack to re-prompt.
"""
Called when a child dialog on the parent's dialog stack completed this turn, returning
control to this dialog component.

.. note::
Containers are typically leaf nodes on the stack but the dev is free to push other dialogs
on top of the stack which will result in the container receiving an unexpected call to
:meth:resume_dialog() when the pushed on dialog ends.
To avoid the container prematurely ending we need to implement this method and simply
ask our inner dialog stack to re-prompt.

If the task is successful, the result indicates whether this dialog is still
active after this dialog turn has been processed.

Generally, the child dialog was started with a call to :meth:`def async begin_dialog()`
in the parent's context. However, if the :meth:`DialogContext.replace_dialog()` method is
is called, the logical child dialog may be different than the original.

If this method is *not* overridden, the dialog automatically calls its
:meth:`asyn def reprompt_dialog()` when the user replies.

:param dialog_context: The :class:`DialogContext` for the current turn of the conversation.
:type dialog_context: :class:`DialogContext`
:param reason: Reason why the dialog resumed.
:type reason: :class:`DialogReason`
:param result: Optional, value returned from the dialog that was called. The type of the value returned is dependent on the child dialog.
:type result: object
:return: Signals the end of the turn
:rtype: :class:`Dialog.end_of_turn`
"""

await self.reprompt_dialog(dialog_context.context, dialog_context.active_dialog)
return Dialog.end_of_turn

async def reprompt_dialog(
self, context: TurnContext, instance: DialogInstance
) -> None:
"""
Called when the dialog should re-prompt the user for input.

:param context: The context object for this turn.
:type context: :class:`TurnContext`
:param instance: State information for this dialog.
:type instance: :class:`DialogInstance`
"""
# Delegate to inner dialog.
dialog_state = instance.state[self.persisted_dialog_state]
inner_dc = DialogContext(self._dialogs, context, dialog_state)
Expand All @@ -87,7 +170,17 @@ async def reprompt_dialog(
async def end_dialog(
self, context: TurnContext, instance: DialogInstance, reason: DialogReason
) -> None:
# Forward cancel to inner dialogs
"""
Called when the dialog is ending.

:param context: The context object for this turn.
:type context: :class:`TurnContext`
:param instance: State information associated with the instance of this component dialog on its parent's dialog stack.
:type instance: :class:`DialogInstance`
:param reason: Reason why the dialog ended.
:type reason: :class:`DialogReason`
"""
# Forward cancel to inner dialog
if reason == DialogReason.CancelCalled:
dialog_state = instance.state[self.persisted_dialog_state]
inner_dc = DialogContext(self._dialogs, context, dialog_state)
Expand All @@ -96,10 +189,12 @@ async def end_dialog(

def add_dialog(self, dialog: Dialog) -> object:
"""
Adds a dialog to the component dialog.
Adding a new dialog will inherit the BotTelemetryClient of the ComponentDialog.
Adds a :class:`Dialog` to the component dialog and returns the updated component.
Adding a new dialog will inherit the :class:`BotTelemetryClient` of the :class:`ComponentDialog`.

:param dialog: The dialog to add.
:return: The updated ComponentDialog
:return: The updated :class:`ComponentDialog`
:rtype: :class:`ComponentDialog`
"""
self._dialogs.add(dialog)
if not self.initial_dialog_id:
Expand All @@ -109,15 +204,36 @@ def add_dialog(self, dialog: Dialog) -> object:
def find_dialog(self, dialog_id: str) -> Dialog:
"""
Finds a dialog by ID.
Adding a new dialog will inherit the BotTelemetryClient of the ComponentDialog.
Adding a new dialog will inherit the :class:`BotTelemetryClient` of the :class:`ComponentDialog`.

:param dialog_id: The dialog to add.
:return: The dialog; or None if there is not a match for the ID.
:rtype: :class:Dialog
"""
return self._dialogs.find(dialog_id)

async def on_begin_dialog(
self, inner_dc: DialogContext, options: object
) -> DialogTurnResult:
"""
Called when the dialog is started and pushed onto the parent's dialog stack.

.. note::
If the task is successful, the result indicates whether the dialog is still
active after the turn has been processed by the dialog.

By default, this calls the :meth:`Dialog.begin_dialog()` method of the component
dialog's initial dialog.

Override this method in a derived class to implement interrupt logic.

:param inner_dc: The inner :class:`DialogContext` for the current turn of conversation.
:type inner_dc: :class:`DialogContext`
:param options: Optional, initial information to pass to the dialog.
:type options: object
:return: ?
:rtype: ?
"""
return await inner_dc.begin_dialog(self.initial_dialog_id, options)

async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult:
Expand All @@ -126,14 +242,53 @@ async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult:
async def on_end_dialog( # pylint: disable=unused-argument
self, context: TurnContext, instance: DialogInstance, reason: DialogReason
) -> None:
"""
Ends the component dialog in its parent's context.

:param turn_context: The :class:`TurnContext` for the current turn of the conversation.
:type turn_context: :class:`TurnContext`
:param instance: State information associated with the instance of this component dialog on its parent's dialog stack.
:type instance: :class:`DialogInstance`
:param reason: Reason why the dialog ended.
:type reason: :class:`DialogReason`
"""
return

async def on_reprompt_dialog( # pylint: disable=unused-argument
self, turn_context: TurnContext, instance: DialogInstance
) -> None:
"""
:param turn_context: The :class:`TurnContext` for the current turn of the conversation.
:type turn_context: :class:`DialogInstance`
:param instance: State information associated with the instance of this component dialog on its parent's dialog stack.
:type instance: :class:`DialogInstance`
"""
return

async def end_component(
self, outer_dc: DialogContext, result: object # pylint: disable=unused-argument
) -> DialogTurnResult:
"""
Ends the component dialog in its parent's context.

.. note::
If the task is successful, the result indicates that the dialog ended after the
turn was processed by the dialog.

In general, the parent context is the dialog or bot turn handler that started the dialog.
If the parent is a dialog, the stack calls the parent's :meth:`Dialog.resume_dialog()` method
to return a result to the parent dialog. If the parent dialog does not implement
:meth:`Dialog.resume_dialog()`, then the parent will end, too, and the result is passed to the next
parent context, if one exists.

The returned :class:`DialogTurnResult`contains the return value in its
:class:`DialogTurnResult.result` property.

:param outer_dc: The parent class:`DialogContext` for the current turn of conversation.
:type outer_dc: class:`DialogContext`
:param result: Optional, value to return from the dialog component to the parent context.
:type result: object
:return: Value to return.
:rtype: :class:`DialogTurnResult.result`
"""
return await outer_dc.end_dialog(result)
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,15 @@ class ActivityPrompt(Dialog, ABC):
"""
Waits for an activity to be received.

This prompt requires a validator be passed in and is useful when waiting for non-message
activities like an event to be received. The validator can ignore received events until the
expected activity is received.
..remarks:
This prompt requires a validator be passed in and is useful when waiting for non-message
activities like an event to be received. The validator can ignore received events until the
expected activity is received.

:var persisted_options: ?
:typevar persisted_options: str
:var persisted_state: ?
:vartype persisted_state: str
"""

persisted_options = "options"
Expand All @@ -36,13 +42,12 @@ def __init__(
self, dialog_id: str, validator: Callable[[PromptValidatorContext], bool]
):
"""
Initializes a new instance of the ActivityPrompt class.
Initializes a new instance of the :class:`ActivityPrompt` class.

Parameters:
----------
dialog_id (str): Unique ID of the dialog within its parent DialogSet or ComponentDialog.

validator: Validator that will be called each time a new activity is received.
:param dialog_id: Unique ID of the dialog within its parent :class:`DialogSet` or :class:`ComponentDialog`.
:type dialog_id: str
:param validator: Validator that will be called each time a new activity is received.
:type validator: Callable[[PromptValidatorContext], bool]
"""
Dialog.__init__(self, dialog_id)

Expand All @@ -53,6 +58,16 @@ def __init__(
async def begin_dialog(
self, dialog_context: DialogContext, options: PromptOptions = None
) -> DialogTurnResult:
"""
Called when a prompt dialog is pushed onto the dialog stack and is being activated.

:param dialog_context: The dialog context for the current turn of the conversation.
:type dialog_context: :class:`DialogContext`
:param options: Optional, additional information to pass to the prompt being started.
:type options: :class:`PromptOptions`
:return Dialog.end_of_turn:
:rtype Dialog.end_of_turn: :class:`Dialog.DialogTurnResult`
"""
if not dialog_context:
raise TypeError("ActivityPrompt.begin_dialog(): dc cannot be None.")
if not isinstance(options, PromptOptions):
Expand Down Expand Up @@ -84,6 +99,14 @@ async def begin_dialog(

async def continue_dialog(self, dialog_context: DialogContext) -> DialogTurnResult:
if not dialog_context:
"""
Called when a prompt dialog is the active dialog and the user replied with a new activity.

:param dialog_context: The dialog context for the current turn of the conversation.
:type dialog_context: :class:`DialogContext`
:return Dialog.end_of_turn:
:rtype Dialog.end_of_turn: :class:`Dialog.DialogTurnResult`
"""
raise TypeError(
"ActivityPrompt.continue_dialog(): DialogContext cannot be None."
)
Expand Down Expand Up @@ -130,11 +153,19 @@ async def resume_dialog( # pylint: disable=unused-argument
self, dialog_context: DialogContext, reason: DialogReason, result: object = None
):
"""
Prompts are typically leaf nodes on the stack but the dev is free to push other dialogs
on top of the stack which will result in the prompt receiving an unexpected call to
resume_dialog() when the pushed on dialog ends.
To avoid the prompt prematurely ending, we need to implement this method and
simply re-prompt the user
Called when a prompt dialog resumes being the active dialog on the dialog stack, such as when the previous active dialog on the stack completes.
..remarks:
Prompts are typically leaf nodes on the stack but the dev is free to push other dialogs
on top of the stack which will result in the prompt receiving an unexpected call to
resume_dialog() when the pushed on dialog ends.
To avoid the prompt prematurely ending, we need to implement this method and
simply re-prompt the user.
:param dialog_context: The dialog context for the current turn of the conversation
:type dialog_context: :class:`DialogContext`
:param reason: An enum indicating why the dialog resumed.
:type reason: :class:`DialogReason`
:param result: >Optional, value returned from the previous dialog on the stack. The type of the value returned is dependent on the previous dialog.
:type result: object
"""
await self.reprompt_dialog(dialog_context.context, dialog_context.active_dialog)

Expand All @@ -155,15 +186,14 @@ async def on_prompt(
"""
Called anytime the derived class should send the user a prompt.

Parameters:
----------
context: Context for the current turn of conversation with the user.

state: Additional state being persisted for the prompt.

options: Options that the prompt started with in the call to `DialogContext.prompt()`.

isRetry: If `true` the users response wasn't recognized and the re-prompt should be sent.
:param dialog_context: The dialog context for the current turn of the conversation
:type dialog_context: :class:`DialogContext`
:param state: Additional state being persisted for the prompt.
:type state: Dict[str, dict]
:param options: Options that the prompt started with in the call to `DialogContext.prompt()`.
:type options: :class:`PromptOptions`
:param isRetry: If `true` the users response wasn't recognized and the re-prompt should be sent.
:type isRetry: bool
"""
if is_retry and options.retry_prompt:
options.retry_prompt.input_hint = InputHints.expecting_input
Expand All @@ -175,7 +205,18 @@ async def on_prompt(
async def on_recognize( # pylint: disable=unused-argument
self, context: TurnContext, state: Dict[str, object], options: PromptOptions
) -> PromptRecognizerResult:

"""
When overridden in a derived class, attempts to recognize the incoming activity.

:param context: Context for the current turn of conversation with the user.
:type context: :class:`TurnContext`
:param state: Contains state for the current instance of the prompt on the dialog stack.
:type state: Dict[str, object]
:param options: A prompt options object
:type options: :class:`PromptOptions`
:return result: constructed from the options initially provided in the call to `async def on_prompt()`
:rtype result: :class:`PromptRecognizerResult`
"""
result = PromptRecognizerResult()
result.succeeded = (True,)
result.value = context.activity
Expand Down