diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_recognizers.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_recognizers.py index 8ac8eb1a4..02fb71e6e 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_recognizers.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_recognizers.py @@ -1,140 +1,141 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from typing import List, Union -from recognizers_number import NumberModel, NumberRecognizer, OrdinalModel -from recognizers_text import Culture - - -from .choice import Choice -from .find import Find -from .find_choices_options import FindChoicesOptions -from .found_choice import FoundChoice -from .model_result import ModelResult - - -class ChoiceRecognizers: - """ Contains methods for matching user input against a list of choices. """ - - @staticmethod - def recognize_choices( - utterance: str, - choices: List[Union[str, Choice]], - options: FindChoicesOptions = None, - ) -> List[ModelResult]: - """ - Matches user input against a list of choices. - - This is layered above the `Find.find_choices()` function, and adds logic to let the user specify - their choice by index (they can say "one" to pick `choice[0]`) or ordinal position - (they can say "the second one" to pick `choice[1]`.) - The user's utterance is recognized in the following order: - - - By name using `find_choices()` - - By 1's based ordinal position. - - By 1's based index position. - - Parameters: - ----------- - - utterance: The input. - - choices: The list of choices. - - options: (Optional) Options to control the recognition strategy. - - Returns: - -------- - A list of found choices, sorted by most relevant first. - """ - if utterance is None: - utterance = "" - - # Normalize list of choices - choices_list = [ - Choice(value=choice) if isinstance(choice, str) else choice - for choice in choices - ] - - # Try finding choices by text search first - # - We only want to use a single strategy for returning results to avoid issues where utterances - # like the "the third one" or "the red one" or "the first division book" would miss-recognize as - # a numerical index or ordinal as well. - locale = options.locale if (options and options.locale) else Culture.English - matched = Find.find_choices(utterance, choices_list, options) - if not matched: - # Next try finding by ordinal - matches = ChoiceRecognizers._recognize_ordinal(utterance, locale) - - if matches: - for match in matches: - ChoiceRecognizers._match_choice_by_index( - choices_list, matched, match - ) - else: - # Finally try by numerical index - matches = ChoiceRecognizers._recognize_number(utterance, locale) - - for match in matches: - ChoiceRecognizers._match_choice_by_index( - choices_list, matched, match - ) - - # Sort any found matches by their position within the utterance. - # - The results from find_choices() are already properly sorted so we just need this - # for ordinal & numerical lookups. - matched = sorted(matched, key=lambda model_result: model_result.start) - - return matched - - @staticmethod - def _recognize_ordinal(utterance: str, culture: str) -> List[ModelResult]: - model: OrdinalModel = NumberRecognizer(culture).get_ordinal_model(culture) - - return list( - map(ChoiceRecognizers._found_choice_constructor, model.parse(utterance)) - ) - - @staticmethod - def _match_choice_by_index( - choices: List[Choice], matched: List[ModelResult], match: ModelResult - ): - try: - index: int = int(match.resolution.value) - 1 - if 0 <= index < len(choices): - choice = choices[index] - - matched.append( - ModelResult( - start=match.start, - end=match.end, - type_name="choice", - text=match.text, - resolution=FoundChoice( - value=choice.value, index=index, score=1.0 - ), - ) - ) - except: - # noop here, as in dotnet/node repos - pass - - @staticmethod - def _recognize_number(utterance: str, culture: str) -> List[ModelResult]: - model: NumberModel = NumberRecognizer(culture).get_number_model(culture) - - return list( - map(ChoiceRecognizers._found_choice_constructor, model.parse(utterance)) - ) - - @staticmethod - def _found_choice_constructor(value_model: ModelResult) -> ModelResult: - return ModelResult( - start=value_model.start, - end=value_model.end, - type_name="choice", - text=value_model.text, - resolution=FoundChoice( - value=value_model.resolution["value"], index=0, score=1.0 - ), - ) +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import List, Union +from recognizers_number import NumberModel, NumberRecognizer, OrdinalModel +from recognizers_text import Culture + + +from .choice import Choice +from .find import Find +from .find_choices_options import FindChoicesOptions +from .found_choice import FoundChoice +from .model_result import ModelResult + + +class ChoiceRecognizers: + """ Contains methods for matching user input against a list of choices. """ + + @staticmethod + def recognize_choices( + utterance: str, + choices: List[Union[str, Choice]], + options: FindChoicesOptions = None, + ) -> List[ModelResult]: + """ + Matches user input against a list of choices. + + This is layered above the `Find.find_choices()` function, and adds logic to let the user specify + their choice by index (they can say "one" to pick `choice[0]`) or ordinal position + (they can say "the second one" to pick `choice[1]`.) + The user's utterance is recognized in the following order: + + - By name using `find_choices()` + - By 1's based ordinal position. + - By 1's based index position. + + Parameters: + ----------- + + utterance: The input. + + choices: The list of choices. + + options: (Optional) Options to control the recognition strategy. + + Returns: + -------- + A list of found choices, sorted by most relevant first. + """ + if utterance is None: + utterance = "" + + # Normalize list of choices + choices_list = [ + Choice(value=choice) if isinstance(choice, str) else choice + for choice in choices + ] + + # Try finding choices by text search first + # - We only want to use a single strategy for returning results to avoid issues where utterances + # like the "the third one" or "the red one" or "the first division book" would miss-recognize as + # a numerical index or ordinal as well. + locale = options.locale if (options and options.locale) else Culture.English + matched = Find.find_choices(utterance, choices_list, options) + if not matched: + matches = [] + + if not options or options.recognize_ordinals: + # Next try finding by ordinal + matches = ChoiceRecognizers._recognize_ordinal(utterance, locale) + for match in matches: + ChoiceRecognizers._match_choice_by_index( + choices_list, matched, match + ) + + if not matches and (not options or options.recognize_numbers): + # Then try by numerical index + matches = ChoiceRecognizers._recognize_number(utterance, locale) + for match in matches: + ChoiceRecognizers._match_choice_by_index( + choices_list, matched, match + ) + + # Sort any found matches by their position within the utterance. + # - The results from find_choices() are already properly sorted so we just need this + # for ordinal & numerical lookups. + matched = sorted(matched, key=lambda model_result: model_result.start) + + return matched + + @staticmethod + def _recognize_ordinal(utterance: str, culture: str) -> List[ModelResult]: + model: OrdinalModel = NumberRecognizer(culture).get_ordinal_model(culture) + + return list( + map(ChoiceRecognizers._found_choice_constructor, model.parse(utterance)) + ) + + @staticmethod + def _match_choice_by_index( + choices: List[Choice], matched: List[ModelResult], match: ModelResult + ): + try: + index: int = int(match.resolution.value) - 1 + if 0 <= index < len(choices): + choice = choices[index] + + matched.append( + ModelResult( + start=match.start, + end=match.end, + type_name="choice", + text=match.text, + resolution=FoundChoice( + value=choice.value, index=index, score=1.0 + ), + ) + ) + except: + # noop here, as in dotnet/node repos + pass + + @staticmethod + def _recognize_number(utterance: str, culture: str) -> List[ModelResult]: + model: NumberModel = NumberRecognizer(culture).get_number_model(culture) + + return list( + map(ChoiceRecognizers._found_choice_constructor, model.parse(utterance)) + ) + + @staticmethod + def _found_choice_constructor(value_model: ModelResult) -> ModelResult: + return ModelResult( + start=value_model.start, + end=value_model.end, + type_name="choice", + text=value_model.text, + resolution=FoundChoice( + value=value_model.resolution["value"], index=0, score=1.0 + ), + ) diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/find_choices_options.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/find_choices_options.py index 8a51fce8e..418781ddb 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/find_choices_options.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/find_choices_options.py @@ -1,23 +1,38 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .find_values_options import FindValuesOptions - - -class FindChoicesOptions(FindValuesOptions): - """ Contains options to control how input is matched against a list of choices """ - - def __init__(self, no_value: bool = None, no_action: bool = None, **kwargs): - """ - Parameters: - ----------- - - no_value: (Optional) If `True`, the choices `value` field will NOT be search over. Defaults to `False`. - - no_action: (Optional) If `True`, the choices `action.title` field will NOT be searched over. - Defaults to `False`. - """ - - super().__init__(**kwargs) - self.no_value = no_value - self.no_action = no_action +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .find_values_options import FindValuesOptions + + +class FindChoicesOptions(FindValuesOptions): + """ Contains options to control how input is matched against a list of choices """ + + def __init__( + self, + no_value: bool = None, + no_action: bool = None, + recognize_numbers: bool = True, + recognize_ordinals: bool = True, + **kwargs, + ): + """ + Parameters: + ----------- + + no_value: (Optional) If `True`, the choices `value` field will NOT be search over. Defaults to `False`. + + no_action: (Optional) If `True`, the choices `action.title` field will NOT be searched over. + Defaults to `False`. + + recognize_numbers: (Optional) Indicates whether the recognizer should check for Numbers using the + NumberRecognizer's NumberModel. + + recognize_ordinals: (Options) Indicates whether the recognizer should check for Ordinal Numbers using + the NumberRecognizer's OrdinalModel. + """ + + super().__init__(**kwargs) + self.no_value = no_value + self.no_action = no_action + self.recognize_numbers = recognize_numbers + self.recognize_ordinals = recognize_ordinals diff --git a/libraries/botbuilder-dialogs/tests/test_choice_prompt.py b/libraries/botbuilder-dialogs/tests/test_choice_prompt.py index d0f581647..16cb16c9e 100644 --- a/libraries/botbuilder-dialogs/tests/test_choice_prompt.py +++ b/libraries/botbuilder-dialogs/tests/test_choice_prompt.py @@ -1,695 +1,717 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from typing import List - -import aiounittest -from recognizers_text import Culture - -from botbuilder.core import CardFactory, ConversationState, MemoryStorage, TurnContext -from botbuilder.core.adapters import TestAdapter -from botbuilder.dialogs import DialogSet, DialogTurnResult, DialogTurnStatus -from botbuilder.dialogs.choices import Choice, ListStyle -from botbuilder.dialogs.prompts import ( - ChoicePrompt, - PromptOptions, - PromptValidatorContext, -) -from botbuilder.schema import Activity, ActivityTypes - -_color_choices: List[Choice] = [ - Choice(value="red"), - Choice(value="green"), - Choice(value="blue"), -] - -_answer_message: Activity = Activity(text="red", type=ActivityTypes.message) -_invalid_message: Activity = Activity(text="purple", type=ActivityTypes.message) - - -class ChoicePromptTest(aiounittest.AsyncTestCase): - def test_choice_prompt_with_empty_id_should_fail(self): - empty_id = "" - - with self.assertRaises(TypeError): - ChoicePrompt(empty_id) - - def test_choice_prompt_with_none_id_should_fail(self): - none_id = None - - with self.assertRaises(TypeError): - ChoicePrompt(none_id) - - async def test_should_call_choice_prompt_using_dc_prompt(self): - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - choices=_color_choices, - ) - await dialog_context.prompt("ChoicePrompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save_changes(turn_context) - - # Initialize TestAdapter. - adapter = TestAdapter(exec_test) - - # Create new ConversationState with MemoryStorage and register the state as middleware. - convo_state = ConversationState(MemoryStorage()) - - # Create a DialogState property, DialogSet, and ChoicePrompt. - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - choice_prompt = ChoicePrompt("ChoicePrompt") - dialogs.add(choice_prompt) - - step1 = await adapter.send("hello") - step2 = await step1.assert_reply( - "Please choose a color. (1) red, (2) green, or (3) blue" - ) - step3 = await step2.send(_answer_message) - await step3.assert_reply("red") - - async def test_should_call_choice_prompt_with_custom_validator(self): - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - choices=_color_choices, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save_changes(turn_context) - - adapter = TestAdapter(exec_test) - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - async def validator(prompt: PromptValidatorContext) -> bool: - assert prompt - - return prompt.recognized.succeeded - - choice_prompt = ChoicePrompt("prompt", validator) - - dialogs.add(choice_prompt) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply( - "Please choose a color. (1) red, (2) green, or (3) blue" - ) - step3 = await step2.send(_invalid_message) - step4 = await step3.assert_reply( - "Please choose a color. (1) red, (2) green, or (3) blue" - ) - step5 = await step4.send(_answer_message) - await step5.assert_reply("red") - - async def test_should_send_custom_retry_prompt(self): - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - retry_prompt=Activity( - type=ActivityTypes.message, - text="Please choose red, blue, or green.", - ), - choices=_color_choices, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save_changes(turn_context) - - adapter = TestAdapter(exec_test) - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - choice_prompt = ChoicePrompt("prompt") - dialogs.add(choice_prompt) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply( - "Please choose a color. (1) red, (2) green, or (3) blue" - ) - step3 = await step2.send(_invalid_message) - step4 = await step3.assert_reply( - "Please choose red, blue, or green. (1) red, (2) green, or (3) blue" - ) - step5 = await step4.send(_answer_message) - await step5.assert_reply("red") - - async def test_should_send_ignore_retry_prompt_if_validator_replies(self): - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - retry_prompt=Activity( - type=ActivityTypes.message, - text="Please choose red, blue, or green.", - ), - choices=_color_choices, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save_changes(turn_context) - - adapter = TestAdapter(exec_test) - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - async def validator(prompt: PromptValidatorContext) -> bool: - assert prompt - - if not prompt.recognized.succeeded: - await prompt.context.send_activity("Bad input.") - - return prompt.recognized.succeeded - - choice_prompt = ChoicePrompt("prompt", validator) - - dialogs.add(choice_prompt) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply( - "Please choose a color. (1) red, (2) green, or (3) blue" - ) - step3 = await step2.send(_invalid_message) - step4 = await step3.assert_reply("Bad input.") - step5 = await step4.send(_answer_message) - await step5.assert_reply("red") - - async def test_should_use_default_locale_when_rendering_choices(self): - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - choices=_color_choices, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save_changes(turn_context) - - adapter = TestAdapter(exec_test) - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - async def validator(prompt: PromptValidatorContext) -> bool: - assert prompt - - if not prompt.recognized.succeeded: - await prompt.context.send_activity("Bad input.") - - return prompt.recognized.succeeded - - choice_prompt = ChoicePrompt( - "prompt", validator, default_locale=Culture.Spanish - ) - - dialogs.add(choice_prompt) - - step1 = await adapter.send(Activity(type=ActivityTypes.message, text="Hello")) - step2 = await step1.assert_reply( - "Please choose a color. (1) red, (2) green, o (3) blue" - ) - step3 = await step2.send(_invalid_message) - step4 = await step3.assert_reply("Bad input.") - step5 = await step4.send(Activity(type=ActivityTypes.message, text="red")) - await step5.assert_reply("red") - - async def test_should_use_context_activity_locale_when_rendering_choices(self): - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - choices=_color_choices, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save_changes(turn_context) - - adapter = TestAdapter(exec_test) - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - async def validator(prompt: PromptValidatorContext) -> bool: - assert prompt - - if not prompt.recognized.succeeded: - await prompt.context.send_activity("Bad input.") - - return prompt.recognized.succeeded - - choice_prompt = ChoicePrompt("prompt", validator) - dialogs.add(choice_prompt) - - step1 = await adapter.send( - Activity(type=ActivityTypes.message, text="Hello", locale=Culture.Spanish) - ) - step2 = await step1.assert_reply( - "Please choose a color. (1) red, (2) green, o (3) blue" - ) - step3 = await step2.send(_answer_message) - await step3.assert_reply("red") - - async def test_should_use_context_activity_locale_over_default_locale_when_rendering_choices( - self, - ): - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - choices=_color_choices, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save_changes(turn_context) - - adapter = TestAdapter(exec_test) - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - async def validator(prompt: PromptValidatorContext) -> bool: - assert prompt - - if not prompt.recognized.succeeded: - await prompt.context.send_activity("Bad input.") - - return prompt.recognized.succeeded - - choice_prompt = ChoicePrompt( - "prompt", validator, default_locale=Culture.Spanish - ) - dialogs.add(choice_prompt) - - step1 = await adapter.send( - Activity(type=ActivityTypes.message, text="Hello", locale=Culture.English) - ) - step2 = await step1.assert_reply( - "Please choose a color. (1) red, (2) green, or (3) blue" - ) - step3 = await step2.send(_answer_message) - await step3.assert_reply("red") - - async def test_should_not_render_choices_if_list_style_none_is_specified(self): - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - choices=_color_choices, - style=ListStyle.none, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save_changes(turn_context) - - adapter = TestAdapter(exec_test) - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - choice_prompt = ChoicePrompt("prompt") - - dialogs.add(choice_prompt) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply("Please choose a color.") - step3 = await step2.send(_answer_message) - await step3.assert_reply("red") - - async def test_should_create_prompt_with_inline_choices_when_specified(self): - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - choices=_color_choices, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save_changes(turn_context) - - adapter = TestAdapter(exec_test) - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - choice_prompt = ChoicePrompt("prompt") - choice_prompt.style = ListStyle.in_line - - dialogs.add(choice_prompt) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply( - "Please choose a color. (1) red, (2) green, or (3) blue" - ) - step3 = await step2.send(_answer_message) - await step3.assert_reply("red") - - async def test_should_create_prompt_with_list_choices_when_specified(self): - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - choices=_color_choices, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save_changes(turn_context) - - adapter = TestAdapter(exec_test) - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - choice_prompt = ChoicePrompt("prompt") - choice_prompt.style = ListStyle.list_style - - dialogs.add(choice_prompt) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply( - "Please choose a color.\n\n 1. red\n 2. green\n 3. blue" - ) - step3 = await step2.send(_answer_message) - await step3.assert_reply("red") - - async def test_should_create_prompt_with_suggested_action_style_when_specified( - self, - ): - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - choices=_color_choices, - style=ListStyle.suggested_action, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save_changes(turn_context) - - adapter = TestAdapter(exec_test) - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - choice_prompt = ChoicePrompt("prompt") - - dialogs.add(choice_prompt) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply("Please choose a color.") - step3 = await step2.send(_answer_message) - await step3.assert_reply("red") - - async def test_should_create_prompt_with_auto_style_when_specified(self): - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - choices=_color_choices, - style=ListStyle.auto, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save_changes(turn_context) - - adapter = TestAdapter(exec_test) - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - choice_prompt = ChoicePrompt("prompt") - - dialogs.add(choice_prompt) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply( - "Please choose a color. (1) red, (2) green, or (3) blue" - ) - step3 = await step2.send(_answer_message) - await step3.assert_reply("red") - - async def test_should_recognize_valid_number_choice(self): - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - choices=_color_choices, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save_changes(turn_context) - - adapter = TestAdapter(exec_test) - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - choice_prompt = ChoicePrompt("prompt") - - dialogs.add(choice_prompt) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply( - "Please choose a color. (1) red, (2) green, or (3) blue" - ) - step3 = await step2.send("1") - await step3.assert_reply("red") - - async def test_should_display_choices_on_hero_card(self): - size_choices = ["large", "medium", "small"] - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a size." - ), - choices=size_choices, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save_changes(turn_context) - - def assert_expected_activity( - activity: Activity, description - ): # pylint: disable=unused-argument - assert len(activity.attachments) == 1 - assert ( - activity.attachments[0].content_type - == CardFactory.content_types.hero_card - ) - assert activity.attachments[0].content.text == "Please choose a size." - - adapter = TestAdapter(exec_test) - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - choice_prompt = ChoicePrompt("prompt") - - # Change the ListStyle of the prompt to ListStyle.none. - choice_prompt.style = ListStyle.hero_card - - dialogs.add(choice_prompt) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply(assert_expected_activity) - step3 = await step2.send("1") - await step3.assert_reply(size_choices[0]) - - async def test_should_display_choices_on_hero_card_with_additional_attachment(self): - size_choices = ["large", "medium", "small"] - card = CardFactory.adaptive_card( - { - "type": "AdaptiveCard", - "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", - "version": "1.2", - "body": [], - } - ) - card_activity = Activity(attachments=[card]) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions(prompt=card_activity, choices=size_choices) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save_changes(turn_context) - - def assert_expected_activity( - activity: Activity, description - ): # pylint: disable=unused-argument - assert len(activity.attachments) == 2 - assert ( - activity.attachments[0].content_type - == CardFactory.content_types.adaptive_card - ) - assert ( - activity.attachments[1].content_type - == CardFactory.content_types.hero_card - ) - - adapter = TestAdapter(exec_test) - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - choice_prompt = ChoicePrompt("prompt") - - # Change the ListStyle of the prompt to ListStyle.none. - choice_prompt.style = ListStyle.hero_card - - dialogs.add(choice_prompt) - - step1 = await adapter.send("Hello") - await step1.assert_reply(assert_expected_activity) +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import List + +import aiounittest +from recognizers_text import Culture + +from botbuilder.core import CardFactory, ConversationState, MemoryStorage, TurnContext +from botbuilder.core.adapters import TestAdapter +from botbuilder.dialogs import ( + DialogSet, + DialogTurnResult, + DialogTurnStatus, + ChoiceRecognizers, + FindChoicesOptions, +) +from botbuilder.dialogs.choices import Choice, ListStyle +from botbuilder.dialogs.prompts import ( + ChoicePrompt, + PromptOptions, + PromptValidatorContext, +) +from botbuilder.schema import Activity, ActivityTypes + +_color_choices: List[Choice] = [ + Choice(value="red"), + Choice(value="green"), + Choice(value="blue"), +] + +_answer_message: Activity = Activity(text="red", type=ActivityTypes.message) +_invalid_message: Activity = Activity(text="purple", type=ActivityTypes.message) + + +class ChoicePromptTest(aiounittest.AsyncTestCase): + def test_choice_prompt_with_empty_id_should_fail(self): + empty_id = "" + + with self.assertRaises(TypeError): + ChoicePrompt(empty_id) + + def test_choice_prompt_with_none_id_should_fail(self): + none_id = None + + with self.assertRaises(TypeError): + ChoicePrompt(none_id) + + async def test_should_call_choice_prompt_using_dc_prompt(self): + async def exec_test(turn_context: TurnContext): + dialog_context = await dialogs.create_context(turn_context) + + results: DialogTurnResult = await dialog_context.continue_dialog() + + if results.status == DialogTurnStatus.Empty: + options = PromptOptions( + prompt=Activity( + type=ActivityTypes.message, text="Please choose a color." + ), + choices=_color_choices, + ) + await dialog_context.prompt("ChoicePrompt", options) + elif results.status == DialogTurnStatus.Complete: + selected_choice = results.result + await turn_context.send_activity(selected_choice.value) + + await convo_state.save_changes(turn_context) + + # Initialize TestAdapter. + adapter = TestAdapter(exec_test) + + # Create new ConversationState with MemoryStorage and register the state as middleware. + convo_state = ConversationState(MemoryStorage()) + + # Create a DialogState property, DialogSet, and ChoicePrompt. + dialog_state = convo_state.create_property("dialogState") + dialogs = DialogSet(dialog_state) + choice_prompt = ChoicePrompt("ChoicePrompt") + dialogs.add(choice_prompt) + + step1 = await adapter.send("hello") + step2 = await step1.assert_reply( + "Please choose a color. (1) red, (2) green, or (3) blue" + ) + step3 = await step2.send(_answer_message) + await step3.assert_reply("red") + + async def test_should_call_choice_prompt_with_custom_validator(self): + async def exec_test(turn_context: TurnContext): + dialog_context = await dialogs.create_context(turn_context) + + results: DialogTurnResult = await dialog_context.continue_dialog() + + if results.status == DialogTurnStatus.Empty: + options = PromptOptions( + prompt=Activity( + type=ActivityTypes.message, text="Please choose a color." + ), + choices=_color_choices, + ) + await dialog_context.prompt("prompt", options) + elif results.status == DialogTurnStatus.Complete: + selected_choice = results.result + await turn_context.send_activity(selected_choice.value) + + await convo_state.save_changes(turn_context) + + adapter = TestAdapter(exec_test) + + convo_state = ConversationState(MemoryStorage()) + dialog_state = convo_state.create_property("dialogState") + dialogs = DialogSet(dialog_state) + + async def validator(prompt: PromptValidatorContext) -> bool: + assert prompt + + return prompt.recognized.succeeded + + choice_prompt = ChoicePrompt("prompt", validator) + + dialogs.add(choice_prompt) + + step1 = await adapter.send("Hello") + step2 = await step1.assert_reply( + "Please choose a color. (1) red, (2) green, or (3) blue" + ) + step3 = await step2.send(_invalid_message) + step4 = await step3.assert_reply( + "Please choose a color. (1) red, (2) green, or (3) blue" + ) + step5 = await step4.send(_answer_message) + await step5.assert_reply("red") + + async def test_should_send_custom_retry_prompt(self): + async def exec_test(turn_context: TurnContext): + dialog_context = await dialogs.create_context(turn_context) + + results: DialogTurnResult = await dialog_context.continue_dialog() + + if results.status == DialogTurnStatus.Empty: + options = PromptOptions( + prompt=Activity( + type=ActivityTypes.message, text="Please choose a color." + ), + retry_prompt=Activity( + type=ActivityTypes.message, + text="Please choose red, blue, or green.", + ), + choices=_color_choices, + ) + await dialog_context.prompt("prompt", options) + elif results.status == DialogTurnStatus.Complete: + selected_choice = results.result + await turn_context.send_activity(selected_choice.value) + + await convo_state.save_changes(turn_context) + + adapter = TestAdapter(exec_test) + + convo_state = ConversationState(MemoryStorage()) + dialog_state = convo_state.create_property("dialogState") + dialogs = DialogSet(dialog_state) + choice_prompt = ChoicePrompt("prompt") + dialogs.add(choice_prompt) + + step1 = await adapter.send("Hello") + step2 = await step1.assert_reply( + "Please choose a color. (1) red, (2) green, or (3) blue" + ) + step3 = await step2.send(_invalid_message) + step4 = await step3.assert_reply( + "Please choose red, blue, or green. (1) red, (2) green, or (3) blue" + ) + step5 = await step4.send(_answer_message) + await step5.assert_reply("red") + + async def test_should_send_ignore_retry_prompt_if_validator_replies(self): + async def exec_test(turn_context: TurnContext): + dialog_context = await dialogs.create_context(turn_context) + + results: DialogTurnResult = await dialog_context.continue_dialog() + + if results.status == DialogTurnStatus.Empty: + options = PromptOptions( + prompt=Activity( + type=ActivityTypes.message, text="Please choose a color." + ), + retry_prompt=Activity( + type=ActivityTypes.message, + text="Please choose red, blue, or green.", + ), + choices=_color_choices, + ) + await dialog_context.prompt("prompt", options) + elif results.status == DialogTurnStatus.Complete: + selected_choice = results.result + await turn_context.send_activity(selected_choice.value) + + await convo_state.save_changes(turn_context) + + adapter = TestAdapter(exec_test) + + convo_state = ConversationState(MemoryStorage()) + dialog_state = convo_state.create_property("dialogState") + dialogs = DialogSet(dialog_state) + + async def validator(prompt: PromptValidatorContext) -> bool: + assert prompt + + if not prompt.recognized.succeeded: + await prompt.context.send_activity("Bad input.") + + return prompt.recognized.succeeded + + choice_prompt = ChoicePrompt("prompt", validator) + + dialogs.add(choice_prompt) + + step1 = await adapter.send("Hello") + step2 = await step1.assert_reply( + "Please choose a color. (1) red, (2) green, or (3) blue" + ) + step3 = await step2.send(_invalid_message) + step4 = await step3.assert_reply("Bad input.") + step5 = await step4.send(_answer_message) + await step5.assert_reply("red") + + async def test_should_use_default_locale_when_rendering_choices(self): + async def exec_test(turn_context: TurnContext): + dialog_context = await dialogs.create_context(turn_context) + + results: DialogTurnResult = await dialog_context.continue_dialog() + + if results.status == DialogTurnStatus.Empty: + options = PromptOptions( + prompt=Activity( + type=ActivityTypes.message, text="Please choose a color." + ), + choices=_color_choices, + ) + await dialog_context.prompt("prompt", options) + elif results.status == DialogTurnStatus.Complete: + selected_choice = results.result + await turn_context.send_activity(selected_choice.value) + + await convo_state.save_changes(turn_context) + + adapter = TestAdapter(exec_test) + + convo_state = ConversationState(MemoryStorage()) + dialog_state = convo_state.create_property("dialogState") + dialogs = DialogSet(dialog_state) + + async def validator(prompt: PromptValidatorContext) -> bool: + assert prompt + + if not prompt.recognized.succeeded: + await prompt.context.send_activity("Bad input.") + + return prompt.recognized.succeeded + + choice_prompt = ChoicePrompt( + "prompt", validator, default_locale=Culture.Spanish + ) + + dialogs.add(choice_prompt) + + step1 = await adapter.send(Activity(type=ActivityTypes.message, text="Hello")) + step2 = await step1.assert_reply( + "Please choose a color. (1) red, (2) green, o (3) blue" + ) + step3 = await step2.send(_invalid_message) + step4 = await step3.assert_reply("Bad input.") + step5 = await step4.send(Activity(type=ActivityTypes.message, text="red")) + await step5.assert_reply("red") + + async def test_should_use_context_activity_locale_when_rendering_choices(self): + async def exec_test(turn_context: TurnContext): + dialog_context = await dialogs.create_context(turn_context) + + results: DialogTurnResult = await dialog_context.continue_dialog() + + if results.status == DialogTurnStatus.Empty: + options = PromptOptions( + prompt=Activity( + type=ActivityTypes.message, text="Please choose a color." + ), + choices=_color_choices, + ) + await dialog_context.prompt("prompt", options) + elif results.status == DialogTurnStatus.Complete: + selected_choice = results.result + await turn_context.send_activity(selected_choice.value) + + await convo_state.save_changes(turn_context) + + adapter = TestAdapter(exec_test) + + convo_state = ConversationState(MemoryStorage()) + dialog_state = convo_state.create_property("dialogState") + dialogs = DialogSet(dialog_state) + + async def validator(prompt: PromptValidatorContext) -> bool: + assert prompt + + if not prompt.recognized.succeeded: + await prompt.context.send_activity("Bad input.") + + return prompt.recognized.succeeded + + choice_prompt = ChoicePrompt("prompt", validator) + dialogs.add(choice_prompt) + + step1 = await adapter.send( + Activity(type=ActivityTypes.message, text="Hello", locale=Culture.Spanish) + ) + step2 = await step1.assert_reply( + "Please choose a color. (1) red, (2) green, o (3) blue" + ) + step3 = await step2.send(_answer_message) + await step3.assert_reply("red") + + async def test_should_use_context_activity_locale_over_default_locale_when_rendering_choices( + self, + ): + async def exec_test(turn_context: TurnContext): + dialog_context = await dialogs.create_context(turn_context) + + results: DialogTurnResult = await dialog_context.continue_dialog() + + if results.status == DialogTurnStatus.Empty: + options = PromptOptions( + prompt=Activity( + type=ActivityTypes.message, text="Please choose a color." + ), + choices=_color_choices, + ) + await dialog_context.prompt("prompt", options) + elif results.status == DialogTurnStatus.Complete: + selected_choice = results.result + await turn_context.send_activity(selected_choice.value) + + await convo_state.save_changes(turn_context) + + adapter = TestAdapter(exec_test) + + convo_state = ConversationState(MemoryStorage()) + dialog_state = convo_state.create_property("dialogState") + dialogs = DialogSet(dialog_state) + + async def validator(prompt: PromptValidatorContext) -> bool: + assert prompt + + if not prompt.recognized.succeeded: + await prompt.context.send_activity("Bad input.") + + return prompt.recognized.succeeded + + choice_prompt = ChoicePrompt( + "prompt", validator, default_locale=Culture.Spanish + ) + dialogs.add(choice_prompt) + + step1 = await adapter.send( + Activity(type=ActivityTypes.message, text="Hello", locale=Culture.English) + ) + step2 = await step1.assert_reply( + "Please choose a color. (1) red, (2) green, or (3) blue" + ) + step3 = await step2.send(_answer_message) + await step3.assert_reply("red") + + async def test_should_not_render_choices_if_list_style_none_is_specified(self): + async def exec_test(turn_context: TurnContext): + dialog_context = await dialogs.create_context(turn_context) + + results: DialogTurnResult = await dialog_context.continue_dialog() + + if results.status == DialogTurnStatus.Empty: + options = PromptOptions( + prompt=Activity( + type=ActivityTypes.message, text="Please choose a color." + ), + choices=_color_choices, + style=ListStyle.none, + ) + await dialog_context.prompt("prompt", options) + elif results.status == DialogTurnStatus.Complete: + selected_choice = results.result + await turn_context.send_activity(selected_choice.value) + + await convo_state.save_changes(turn_context) + + adapter = TestAdapter(exec_test) + + convo_state = ConversationState(MemoryStorage()) + dialog_state = convo_state.create_property("dialogState") + dialogs = DialogSet(dialog_state) + + choice_prompt = ChoicePrompt("prompt") + + dialogs.add(choice_prompt) + + step1 = await adapter.send("Hello") + step2 = await step1.assert_reply("Please choose a color.") + step3 = await step2.send(_answer_message) + await step3.assert_reply("red") + + async def test_should_create_prompt_with_inline_choices_when_specified(self): + async def exec_test(turn_context: TurnContext): + dialog_context = await dialogs.create_context(turn_context) + + results: DialogTurnResult = await dialog_context.continue_dialog() + + if results.status == DialogTurnStatus.Empty: + options = PromptOptions( + prompt=Activity( + type=ActivityTypes.message, text="Please choose a color." + ), + choices=_color_choices, + ) + await dialog_context.prompt("prompt", options) + elif results.status == DialogTurnStatus.Complete: + selected_choice = results.result + await turn_context.send_activity(selected_choice.value) + + await convo_state.save_changes(turn_context) + + adapter = TestAdapter(exec_test) + + convo_state = ConversationState(MemoryStorage()) + dialog_state = convo_state.create_property("dialogState") + dialogs = DialogSet(dialog_state) + + choice_prompt = ChoicePrompt("prompt") + choice_prompt.style = ListStyle.in_line + + dialogs.add(choice_prompt) + + step1 = await adapter.send("Hello") + step2 = await step1.assert_reply( + "Please choose a color. (1) red, (2) green, or (3) blue" + ) + step3 = await step2.send(_answer_message) + await step3.assert_reply("red") + + async def test_should_create_prompt_with_list_choices_when_specified(self): + async def exec_test(turn_context: TurnContext): + dialog_context = await dialogs.create_context(turn_context) + + results: DialogTurnResult = await dialog_context.continue_dialog() + + if results.status == DialogTurnStatus.Empty: + options = PromptOptions( + prompt=Activity( + type=ActivityTypes.message, text="Please choose a color." + ), + choices=_color_choices, + ) + await dialog_context.prompt("prompt", options) + elif results.status == DialogTurnStatus.Complete: + selected_choice = results.result + await turn_context.send_activity(selected_choice.value) + + await convo_state.save_changes(turn_context) + + adapter = TestAdapter(exec_test) + + convo_state = ConversationState(MemoryStorage()) + dialog_state = convo_state.create_property("dialogState") + dialogs = DialogSet(dialog_state) + + choice_prompt = ChoicePrompt("prompt") + choice_prompt.style = ListStyle.list_style + + dialogs.add(choice_prompt) + + step1 = await adapter.send("Hello") + step2 = await step1.assert_reply( + "Please choose a color.\n\n 1. red\n 2. green\n 3. blue" + ) + step3 = await step2.send(_answer_message) + await step3.assert_reply("red") + + async def test_should_create_prompt_with_suggested_action_style_when_specified( + self, + ): + async def exec_test(turn_context: TurnContext): + dialog_context = await dialogs.create_context(turn_context) + + results: DialogTurnResult = await dialog_context.continue_dialog() + + if results.status == DialogTurnStatus.Empty: + options = PromptOptions( + prompt=Activity( + type=ActivityTypes.message, text="Please choose a color." + ), + choices=_color_choices, + style=ListStyle.suggested_action, + ) + await dialog_context.prompt("prompt", options) + elif results.status == DialogTurnStatus.Complete: + selected_choice = results.result + await turn_context.send_activity(selected_choice.value) + + await convo_state.save_changes(turn_context) + + adapter = TestAdapter(exec_test) + + convo_state = ConversationState(MemoryStorage()) + dialog_state = convo_state.create_property("dialogState") + dialogs = DialogSet(dialog_state) + + choice_prompt = ChoicePrompt("prompt") + + dialogs.add(choice_prompt) + + step1 = await adapter.send("Hello") + step2 = await step1.assert_reply("Please choose a color.") + step3 = await step2.send(_answer_message) + await step3.assert_reply("red") + + async def test_should_create_prompt_with_auto_style_when_specified(self): + async def exec_test(turn_context: TurnContext): + dialog_context = await dialogs.create_context(turn_context) + + results: DialogTurnResult = await dialog_context.continue_dialog() + + if results.status == DialogTurnStatus.Empty: + options = PromptOptions( + prompt=Activity( + type=ActivityTypes.message, text="Please choose a color." + ), + choices=_color_choices, + style=ListStyle.auto, + ) + await dialog_context.prompt("prompt", options) + elif results.status == DialogTurnStatus.Complete: + selected_choice = results.result + await turn_context.send_activity(selected_choice.value) + + await convo_state.save_changes(turn_context) + + adapter = TestAdapter(exec_test) + + convo_state = ConversationState(MemoryStorage()) + dialog_state = convo_state.create_property("dialogState") + dialogs = DialogSet(dialog_state) + + choice_prompt = ChoicePrompt("prompt") + + dialogs.add(choice_prompt) + + step1 = await adapter.send("Hello") + step2 = await step1.assert_reply( + "Please choose a color. (1) red, (2) green, or (3) blue" + ) + step3 = await step2.send(_answer_message) + await step3.assert_reply("red") + + async def test_should_recognize_valid_number_choice(self): + async def exec_test(turn_context: TurnContext): + dialog_context = await dialogs.create_context(turn_context) + + results: DialogTurnResult = await dialog_context.continue_dialog() + + if results.status == DialogTurnStatus.Empty: + options = PromptOptions( + prompt=Activity( + type=ActivityTypes.message, text="Please choose a color." + ), + choices=_color_choices, + ) + await dialog_context.prompt("prompt", options) + elif results.status == DialogTurnStatus.Complete: + selected_choice = results.result + await turn_context.send_activity(selected_choice.value) + + await convo_state.save_changes(turn_context) + + adapter = TestAdapter(exec_test) + + convo_state = ConversationState(MemoryStorage()) + dialog_state = convo_state.create_property("dialogState") + dialogs = DialogSet(dialog_state) + + choice_prompt = ChoicePrompt("prompt") + + dialogs.add(choice_prompt) + + step1 = await adapter.send("Hello") + step2 = await step1.assert_reply( + "Please choose a color. (1) red, (2) green, or (3) blue" + ) + step3 = await step2.send("1") + await step3.assert_reply("red") + + async def test_should_display_choices_on_hero_card(self): + size_choices = ["large", "medium", "small"] + + async def exec_test(turn_context: TurnContext): + dialog_context = await dialogs.create_context(turn_context) + + results: DialogTurnResult = await dialog_context.continue_dialog() + + if results.status == DialogTurnStatus.Empty: + options = PromptOptions( + prompt=Activity( + type=ActivityTypes.message, text="Please choose a size." + ), + choices=size_choices, + ) + await dialog_context.prompt("prompt", options) + elif results.status == DialogTurnStatus.Complete: + selected_choice = results.result + await turn_context.send_activity(selected_choice.value) + + await convo_state.save_changes(turn_context) + + def assert_expected_activity( + activity: Activity, description + ): # pylint: disable=unused-argument + assert len(activity.attachments) == 1 + assert ( + activity.attachments[0].content_type + == CardFactory.content_types.hero_card + ) + assert activity.attachments[0].content.text == "Please choose a size." + + adapter = TestAdapter(exec_test) + + convo_state = ConversationState(MemoryStorage()) + dialog_state = convo_state.create_property("dialogState") + dialogs = DialogSet(dialog_state) + + choice_prompt = ChoicePrompt("prompt") + + # Change the ListStyle of the prompt to ListStyle.none. + choice_prompt.style = ListStyle.hero_card + + dialogs.add(choice_prompt) + + step1 = await adapter.send("Hello") + step2 = await step1.assert_reply(assert_expected_activity) + step3 = await step2.send("1") + await step3.assert_reply(size_choices[0]) + + async def test_should_display_choices_on_hero_card_with_additional_attachment(self): + size_choices = ["large", "medium", "small"] + card = CardFactory.adaptive_card( + { + "type": "AdaptiveCard", + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.2", + "body": [], + } + ) + card_activity = Activity(attachments=[card]) + + async def exec_test(turn_context: TurnContext): + dialog_context = await dialogs.create_context(turn_context) + + results: DialogTurnResult = await dialog_context.continue_dialog() + + if results.status == DialogTurnStatus.Empty: + options = PromptOptions(prompt=card_activity, choices=size_choices) + await dialog_context.prompt("prompt", options) + elif results.status == DialogTurnStatus.Complete: + selected_choice = results.result + await turn_context.send_activity(selected_choice.value) + + await convo_state.save_changes(turn_context) + + def assert_expected_activity( + activity: Activity, description + ): # pylint: disable=unused-argument + assert len(activity.attachments) == 2 + assert ( + activity.attachments[0].content_type + == CardFactory.content_types.adaptive_card + ) + assert ( + activity.attachments[1].content_type + == CardFactory.content_types.hero_card + ) + + adapter = TestAdapter(exec_test) + + convo_state = ConversationState(MemoryStorage()) + dialog_state = convo_state.create_property("dialogState") + dialogs = DialogSet(dialog_state) + + choice_prompt = ChoicePrompt("prompt") + + # Change the ListStyle of the prompt to ListStyle.none. + choice_prompt.style = ListStyle.hero_card + + dialogs.add(choice_prompt) + + step1 = await adapter.send("Hello") + await step1.assert_reply(assert_expected_activity) + + async def test_should_not_find_a_choice_in_an_utterance_by_ordinal(self): + found = ChoiceRecognizers.recognize_choices( + "the first one please", + _color_choices, + FindChoicesOptions(recognize_numbers=False, recognize_ordinals=False), + ) + assert not found + + async def test_should_not_find_a_choice_in_an_utterance_by_numerical_index(self): + found = ChoiceRecognizers.recognize_choices( + "one", + _color_choices, + FindChoicesOptions(recognize_numbers=False, recognize_ordinals=False), + ) + assert not found