Spaces:
Build error
Build error
Validify-testbot-1
/
botbuilder-python
/libraries
/botbuilder-dialogs
/botbuilder
/dialogs
/prompts
/prompt.py
# Copyright (c) Microsoft Corporation. All rights reserved. | |
# Licensed under the MIT License. | |
from abc import abstractmethod | |
import copy | |
from typing import Dict, List | |
from botbuilder.core.turn_context import TurnContext | |
from botbuilder.schema import InputHints, ActivityTypes | |
from botbuilder.dialogs.choices import ( | |
Choice, | |
ChoiceFactory, | |
ChoiceFactoryOptions, | |
ListStyle, | |
) | |
from botbuilder.schema import Activity | |
from .prompt_options import PromptOptions | |
from .prompt_validator_context import PromptValidatorContext | |
from ..dialog_reason import DialogReason | |
from ..dialog import Dialog | |
from ..dialog_instance import DialogInstance | |
from ..dialog_turn_result import DialogTurnResult | |
from ..dialog_context import DialogContext | |
class Prompt(Dialog): | |
""" | |
Defines the core behavior of prompt dialogs. Extends the :class:`Dialog` base class. | |
.. remarks:: | |
When the prompt ends, it returns an object that represents the value it was prompted for. | |
Use :meth:`DialogSet.add()` or :meth:`ComponentDialog.add_dialog()` to add a prompt to | |
a dialog set or component dialog, respectively. | |
Use :meth:`DialogContext.prompt()` or :meth:`DialogContext.begin_dialog()` to start the prompt. | |
If you start a prompt from a :class:`WaterfallStep` in a :class:`WaterfallDialog`, then the | |
prompt result will be available in the next step of the waterfall. | |
""" | |
ATTEMPT_COUNT_KEY = "AttemptCount" | |
persisted_options = "options" | |
persisted_state = "state" | |
def __init__(self, dialog_id: str, validator: object = None): | |
""" | |
Creates a new :class:`Prompt` instance. | |
:param dialog_id: Unique Id of the prompt within its parent :class:`DialogSet` | |
:class:`ComponentDialog` | |
:type dialog_id: str | |
:param validator: Optionally provide additional validation and re-prompting logic | |
:type validator: Object | |
""" | |
super(Prompt, self).__init__(dialog_id) | |
self._validator = validator | |
async def begin_dialog( | |
self, dialog_context: DialogContext, options: object = None | |
) -> DialogTurnResult: | |
""" | |
Starts a prompt dialog. 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: Object | |
:return: The dialog turn result | |
:rtype: :class:`DialogTurnResult` | |
.. note:: | |
The result indicates whether the prompt is still active after the turn has been processed. | |
""" | |
if not dialog_context: | |
raise TypeError("Prompt(): dc cannot be None.") | |
if not isinstance(options, PromptOptions): | |
raise TypeError("Prompt(): Prompt options are required for Prompt dialogs.") | |
# Ensure prompts have input hint set | |
if options.prompt is not None and not options.prompt.input_hint: | |
options.prompt.input_hint = InputHints.expecting_input | |
if options.retry_prompt is not None and not options.retry_prompt.input_hint: | |
options.retry_prompt.input_hint = InputHints.expecting_input | |
# Initialize prompt state | |
state = dialog_context.active_dialog.state | |
state[self.persisted_options] = options | |
state[self.persisted_state] = {} | |
# Send initial prompt | |
await self.on_prompt( | |
dialog_context.context, | |
state[self.persisted_state], | |
state[self.persisted_options], | |
False, | |
) | |
return Dialog.end_of_turn | |
async def continue_dialog(self, dialog_context: DialogContext): | |
""" | |
Continues a dialog. | |
:param dialog_context: The dialog context for the current turn of the conversation | |
:type dialog_context: :class:`DialogContext` | |
:return: The dialog turn result | |
:rtype: :class:`DialogTurnResult` | |
.. remarks:: | |
Called when a prompt dialog is the active dialog and the user replied with a new activity. | |
If the task is successful, the result indicates whether the dialog is still active after | |
the turn has been processed by the dialog. | |
The prompt generally continues to receive the user's replies until it accepts the | |
user's reply as valid input for the prompt. | |
""" | |
if not dialog_context: | |
raise TypeError("Prompt(): dc cannot be None.") | |
# Don't do anything for non-message activities | |
if dialog_context.context.activity.type != ActivityTypes.message: | |
return Dialog.end_of_turn | |
# Perform base recognition | |
instance = dialog_context.active_dialog | |
state = instance.state[self.persisted_state] | |
options = instance.state[self.persisted_options] | |
recognized = await self.on_recognize(dialog_context.context, state, options) | |
# Validate the return value | |
is_valid = False | |
if self._validator is not None: | |
prompt_context = PromptValidatorContext( | |
dialog_context.context, recognized, state, options | |
) | |
is_valid = await self._validator(prompt_context) | |
if options is None: | |
options = PromptOptions() | |
options.number_of_attempts += 1 | |
else: | |
if recognized.succeeded: | |
is_valid = True | |
# Return recognized value or re-prompt | |
if is_valid: | |
return await dialog_context.end_dialog(recognized.value) | |
if not dialog_context.context.responded: | |
await self.on_prompt(dialog_context.context, state, options, True) | |
return Dialog.end_of_turn | |
async def resume_dialog( | |
self, dialog_context: DialogContext, reason: DialogReason, result: object | |
) -> DialogTurnResult: | |
""" | |
Resumes a dialog. | |
: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. | |
:type result: object | |
:return: The dialog turn result | |
:rtype: :class:`DialogTurnResult` | |
.. remarks:: | |
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. | |
If the task is successful, the result indicates whether the dialog is still | |
active after the turn has been processed by the dialog. | |
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 | |
:meth:resume_dialog() when the pushed on dialog ends. | |
Simply re-prompt the user to avoid that the prompt ends prematurely. | |
""" | |
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): | |
""" | |
Reprompts user for input. | |
:param context: Context for the current turn of conversation with the user | |
:type context: :class:`botbuilder.core.TurnContext` | |
:param instance: The instance of the dialog on the stack | |
:type instance: :class:`DialogInstance` | |
:return: A task representing the asynchronous operation | |
""" | |
state = instance.state[self.persisted_state] | |
options = instance.state[self.persisted_options] | |
await self.on_prompt(context, state, options, False) | |
async def on_prompt( | |
self, | |
turn_context: TurnContext, | |
state: Dict[str, object], | |
options: PromptOptions, | |
is_retry: bool, | |
): | |
""" | |
Prompts user for input. When overridden in a derived class, prompts the user for input. | |
:param turn_context: Context for the current turn of conversation with the user | |
:type turn_context: :class:`botbuilder.core.TurnContext` | |
:param state: Contains state for the current instance of the prompt on the dialog stack | |
:type state: :class:`Dict` | |
:param options: A prompt options object constructed from:meth:`DialogContext.prompt()` | |
:type options: :class:`PromptOptions` | |
:param is_retry: Determines whether `prompt` or `retry_prompt` should be used | |
:type is_retry: bool | |
:return: A task representing the asynchronous operation. | |
""" | |
async def on_recognize( | |
self, | |
turn_context: TurnContext, | |
state: Dict[str, object], | |
options: PromptOptions, | |
): | |
""" | |
Recognizes the user's input. | |
:param turn_context: Context for the current turn of conversation with the user | |
:type turn_context: :class:`botbuilder.core.TurnContext` | |
:param state: Contains state for the current instance of the prompt on the dialog stack | |
:type state: :class:`Dict` | |
:param options: A prompt options object constructed from :meth:`DialogContext.prompt()` | |
:type options: :class:`PromptOptions` | |
:return: A task representing the asynchronous operation. | |
.. note:: | |
When overridden in a derived class, attempts to recognize the user's input. | |
""" | |
def append_choices( | |
self, | |
prompt: Activity, | |
channel_id: str, | |
choices: List[Choice], | |
style: ListStyle, | |
options: ChoiceFactoryOptions = None, | |
) -> Activity: | |
""" | |
Composes an output activity containing a set of choices. | |
:param prompt: The prompt to append the user's choice to | |
:type prompt: | |
:param channel_id: Id of the channel the prompt is being sent to | |
:type channel_id: str | |
:param: choices: List of choices to append | |
:type choices: :class:`List` | |
:param: style: Configured style for the list of choices | |
:type style: :class:`ListStyle` | |
:param: options: Optional formatting options to use when presenting the choices | |
:type style: :class:`ChoiceFactoryOptions` | |
:return: A task representing the asynchronous operation | |
.. remarks:: | |
If the task is successful, the result contains the updated activity. | |
When overridden in a derived class, appends choices to the activity when the user | |
is prompted for input. This is an helper function to compose an output activity | |
containing a set of choices. | |
""" | |
# Get base prompt text (if any) | |
text = prompt.text if prompt is not None and prompt.text else "" | |
# Create temporary msg | |
# TODO: fix once ChoiceFactory complete | |
def inline() -> Activity: | |
return ChoiceFactory.inline(choices, text, None, options) | |
def list_style() -> Activity: | |
return ChoiceFactory.list_style(choices, text, None, options) | |
def suggested_action() -> Activity: | |
return ChoiceFactory.suggested_action(choices, text) | |
def hero_card() -> Activity: | |
return ChoiceFactory.hero_card(choices, text) | |
def list_style_none() -> Activity: | |
activity = Activity(type=ActivityTypes.message) | |
activity.text = text | |
return activity | |
def default() -> Activity: | |
return ChoiceFactory.for_channel(channel_id, choices, text, None, options) | |
# Maps to values in ListStyle Enum | |
switcher = { | |
0: list_style_none, | |
1: default, | |
2: inline, | |
3: list_style, | |
4: suggested_action, | |
5: hero_card, | |
} | |
msg = switcher.get(int(style.value), default)() | |
# Update prompt with text, actions and attachments | |
if prompt: | |
# clone the prompt the set in the options (note ActivityEx has Properties so this is the safest mechanism) | |
prompt = copy.copy(prompt) | |
prompt.text = msg.text | |
if ( | |
msg.suggested_actions is not None | |
and msg.suggested_actions.actions is not None | |
and msg.suggested_actions.actions | |
): | |
prompt.suggested_actions = msg.suggested_actions | |
if msg.attachments: | |
if prompt.attachments: | |
prompt.attachments.extend(msg.attachments) | |
else: | |
prompt.attachments = msg.attachments | |
return prompt | |
# TODO: Update to InputHints.ExpectingInput; | |
msg.input_hint = None | |
return msg | |