From 3c0d7ae9ac35f02f9d356793ff51e6175d7dcc81 Mon Sep 17 00:00:00 2001 From: Michael Richardson Date: Thu, 4 Mar 2021 10:51:20 -0800 Subject: [PATCH 1/2] add PromptCultureModels, rework prompt locales --- .../botbuilder/dialogs/prompts/__init__.py | 3 + .../dialogs/prompts/choice_prompt.py | 101 +++----- .../dialogs/prompts/confirm_prompt.py | 82 +++---- .../dialogs/prompts/prompt_culture_models.py | 190 +++++++++++++++ .../tests/test_choice_prompt.py | 219 ++++++++++++++++- .../tests/test_confirm_prompt.py | 227 +++++++++++++++++- 6 files changed, 703 insertions(+), 119 deletions(-) create mode 100644 libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt_culture_models.py diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/__init__.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/__init__.py index 457121fad..9f9dc6242 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/__init__.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/__init__.py @@ -14,6 +14,7 @@ from .number_prompt import NumberPrompt from .oauth_prompt import OAuthPrompt from .oauth_prompt_settings import OAuthPromptSettings +from .prompt_culture_models import PromptCultureModel, PromptCultureModels from .prompt_options import PromptOptions from .prompt_recognizer_result import PromptRecognizerResult from .prompt_validator_context import PromptValidatorContext @@ -30,6 +31,8 @@ "NumberPrompt", "OAuthPrompt", "OAuthPromptSettings", + "PromptCultureModel", + "PromptCultureModels", "PromptOptions", "PromptRecognizerResult", "PromptValidatorContext", diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/choice_prompt.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/choice_prompt.py index 93bf929dd..3d5f562ad 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/choice_prompt.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/choice_prompt.py @@ -1,9 +1,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from typing import Callable, Dict, List, Union +from typing import Callable, Dict, List -from recognizers_text import Culture from botbuilder.core import TurnContext from botbuilder.dialogs.choices import ( Choice, @@ -15,6 +14,7 @@ from botbuilder.schema import Activity, ActivityTypes from .prompt import Prompt +from .prompt_culture_models import PromptCultureModels from .prompt_options import PromptOptions from .prompt_validator_context import PromptValidatorContext from .prompt_recognizer_result import PromptRecognizerResult @@ -29,54 +29,13 @@ class ChoicePrompt(Prompt): """ _default_choice_options: Dict[str, ChoiceFactoryOptions] = { - Culture.Spanish: ChoiceFactoryOptions( - inline_separator=", ", - inline_or=" o ", - inline_or_more=", o ", + c.locale: ChoiceFactoryOptions( + inline_separator=c.separator, + inline_or=c.inline_or_more, + inline_or_more=c.inline_or_more, include_numbers=True, - ), - Culture.Dutch: ChoiceFactoryOptions( - inline_separator=", ", - inline_or=" of ", - inline_or_more=", of ", - include_numbers=True, - ), - Culture.English: ChoiceFactoryOptions( - inline_separator=", ", - inline_or=" or ", - inline_or_more=", or ", - include_numbers=True, - ), - Culture.French: ChoiceFactoryOptions( - inline_separator=", ", - inline_or=" ou ", - inline_or_more=", ou ", - include_numbers=True, - ), - "de-de": ChoiceFactoryOptions( - inline_separator=", ", - inline_or=" oder ", - inline_or_more=", oder ", - include_numbers=True, - ), - Culture.Japanese: ChoiceFactoryOptions( - inline_separator="、 ", - inline_or=" または ", - inline_or_more="、 または ", - include_numbers=True, - ), - Culture.Portuguese: ChoiceFactoryOptions( - inline_separator=", ", - inline_or=" ou ", - inline_or_more=", ou ", - include_numbers=True, - ), - Culture.Chinese: ChoiceFactoryOptions( - inline_separator=", ", - inline_or=" 要么 ", - inline_or_more=", 要么 ", - include_numbers=True, - ), + ) + for c in PromptCultureModels.get_supported_cultures() } def __init__( @@ -84,7 +43,19 @@ def __init__( dialog_id: str, validator: Callable[[PromptValidatorContext], bool] = None, default_locale: str = None, + choice_defaults: Dict[str, ChoiceFactoryOptions] = None, ): + """ + :param dialog_id: Unique ID of the dialog within its parent `DialogSet`. + :param validator: (Optional) validator that will be called each time the user responds to the prompt. + If the validator replies with a message no additional retry prompt will be sent. + :param default_locale: (Optional) locale to use if `dc.context.activity.locale` not specified. + Defaults to a value of `en-us`. + :param choice_defaults: (Optional) Overrides the dictionary of + Bot Framework SDK-supported _default_choice_options. + As type Dict[str, ChoiceFactoryOptions], the key is a string of the locale, such as "en-us". + * Must be passed in to each ConfirmPrompt that needs the custom choice defaults. + """ super().__init__(dialog_id, validator) self.style = ListStyle.auto @@ -92,6 +63,9 @@ def __init__( self.choice_options: ChoiceFactoryOptions = None self.recognizer_options: FindChoicesOptions = None + if choice_defaults is not None: + self._default_choice_options = choice_defaults + async def on_prompt( self, turn_context: TurnContext, @@ -106,12 +80,7 @@ async def on_prompt( raise TypeError("ChoicePrompt.on_prompt(): options cannot be None.") # Determine culture - culture: Union[ - str, None - ] = turn_context.activity.locale if turn_context.activity.locale else self.default_locale - - if not culture or culture not in ChoicePrompt._default_choice_options: - culture = Culture.English + culture = self._determine_culture(turn_context.activity) # Format prompt to send choices: List[Choice] = options.choices if options.choices else [] @@ -119,7 +88,7 @@ async def on_prompt( choice_options: ChoiceFactoryOptions = ( self.choice_options if self.choice_options - else ChoicePrompt._default_choice_options[culture] + else self._default_choice_options[culture] ) choice_style = ( 0 if options.style == 0 else options.style if options.style else self.style @@ -155,11 +124,7 @@ async def on_recognize( if not utterance: return result opt: FindChoicesOptions = self.recognizer_options if self.recognizer_options else FindChoicesOptions() - opt.locale = ( - activity.locale - if activity.locale - else (self.default_locale or Culture.English) - ) + opt.locale = self._determine_culture(turn_context.activity, opt) results = ChoiceRecognizers.recognize_choices(utterance, choices, opt) if results is not None and results: @@ -167,3 +132,17 @@ async def on_recognize( result.value = results[0].resolution return result + + def _determine_culture( + self, activity: Activity, opt: FindChoicesOptions = FindChoicesOptions() + ) -> str: + culture = ( + PromptCultureModels.map_to_nearest_language(activity.locale) + or opt.locale + or self.default_locale + or PromptCultureModels.English.locale + ) + if not culture or not self._default_choice_options.get(culture): + culture = PromptCultureModels.English.locale + + return culture diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/confirm_prompt.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/confirm_prompt.py index b5f902c50..30f2248e8 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/confirm_prompt.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/confirm_prompt.py @@ -12,60 +12,30 @@ ListStyle, ) from .prompt import Prompt +from .prompt_culture_models import PromptCultureModels from .prompt_options import PromptOptions from .prompt_recognizer_result import PromptRecognizerResult class ConfirmPrompt(Prompt): - # TODO: Fix to reference recognizer to use proper constants - choice_defaults: Dict[str, object] = { - "Spanish": ( - Choice("Si"), - Choice("No"), - ChoiceFactoryOptions(", ", " o ", ", o ", True), - ), - "Dutch": ( - Choice("Ja"), - Choice("Nee"), - ChoiceFactoryOptions(", ", " of ", ", of ", True), - ), - "English": ( - Choice("Yes"), - Choice("No"), - ChoiceFactoryOptions(", ", " or ", ", or ", True), - ), - "French": ( - Choice("Oui"), - Choice("Non"), - ChoiceFactoryOptions(", ", " ou ", ", ou ", True), - ), - "German": ( - Choice("Ja"), - Choice("Nein"), - ChoiceFactoryOptions(", ", " oder ", ", oder ", True), - ), - "Japanese": ( - Choice("はい"), - Choice("いいえ"), - ChoiceFactoryOptions("、 ", " または ", "、 または ", True), - ), - "Portuguese": ( - Choice("Sim"), - Choice("Não"), - ChoiceFactoryOptions(", ", " ou ", ", ou ", True), - ), - "Chinese": ( - Choice("是的"), - Choice("不"), - ChoiceFactoryOptions(", ", " 要么 ", ", 要么 ", True), - ), + _default_choice_options: Dict[str, object] = { + c.locale: ( + Choice(c.yes_in_language), + Choice(c.no_in_language), + ChoiceFactoryOptions(c.separator, c.inline_or, c.inline_or_more, True), + ) + for c in PromptCultureModels.get_supported_cultures() } # TODO: PromptValidator def __init__( - self, dialog_id: str, validator: object = None, default_locale: str = None + self, + dialog_id: str, + validator: object = None, + default_locale: str = None, + choice_defaults: Dict[str, object] = None, ): - super(ConfirmPrompt, self).__init__(dialog_id, validator) + super().__init__(dialog_id, validator) if dialog_id is None: raise TypeError("ConfirmPrompt(): dialog_id cannot be None.") # TODO: Port ListStyle @@ -75,6 +45,9 @@ def __init__( self.choice_options = None self.confirm_choices = None + if choice_defaults is not None: + self._default_choice_options = choice_defaults + async def on_prompt( self, turn_context: TurnContext, @@ -89,8 +62,8 @@ async def on_prompt( # Format prompt to send channel_id = turn_context.activity.channel_id - culture = self.determine_culture(turn_context.activity) - defaults = self.choice_defaults[culture] + culture = self._determine_culture(turn_context.activity) + defaults = self._default_choice_options[culture] choice_opts = ( self.choice_options if self.choice_options is not None else defaults[2] ) @@ -125,7 +98,7 @@ async def on_recognize( utterance = turn_context.activity.text if not utterance: return result - culture = self.determine_culture(turn_context.activity) + culture = self._determine_culture(turn_context.activity) results = recognize_boolean(utterance, culture) if results: first = results[0] @@ -135,7 +108,7 @@ async def on_recognize( else: # First check whether the prompt was sent to the user with numbers # if it was we should recognize numbers - defaults = self.choice_defaults[culture] + defaults = self._default_choice_options[culture] opts = ( self.choice_options if self.choice_options is not None @@ -161,12 +134,13 @@ async def on_recognize( return result - def determine_culture(self, activity: Activity) -> str: + def _determine_culture(self, activity: Activity) -> str: culture = ( - activity.locale if activity.locale is not None else self.default_locale + PromptCultureModels.map_to_nearest_language(activity.locale) + or self.default_locale + or PromptCultureModels.English.locale ) - if not culture or culture not in self.choice_defaults: - culture = ( - "English" # TODO: Fix to reference recognizer to use proper constants - ) + if not culture or not self._default_choice_options.get(culture): + culture = PromptCultureModels.English.locale + return culture diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt_culture_models.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt_culture_models.py new file mode 100644 index 000000000..1572ac688 --- /dev/null +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt_culture_models.py @@ -0,0 +1,190 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import List + +from recognizers_text import Culture + + +class PromptCultureModel: + """ + Culture model used in Choice and Confirm Prompts. + """ + + def __init__( + self, + locale: str, + separator: str, + inline_or: str, + inline_or_more: str, + yes_in_language: str, + no_in_language: str, + ): + """ + + :param locale: Culture Model's Locale. Example: "en-US". + :param separator: Culture Model's Inline Separator. Example: ", ". + :param inline_or: Culture Model's Inline Or. Example: " or ". + :param inline_or_more Culture Model's Inline Or More. Example: ", or ". + :param yes_in_language: Equivalent of "Yes" in Culture Model's Language. Example: "Yes". + :param no_in_language: Equivalent of "No" in Culture Model's Language. Example: "No". + """ + self.locale = locale + self.separator = separator + self.inline_or = inline_or + self.inline_or_more = inline_or_more + self.yes_in_language = yes_in_language + self.no_in_language = no_in_language + + +class PromptCultureModels: + """ + Class container for currently-supported Culture Models in Confirm and Choice Prompt. + """ + + Chinese = PromptCultureModel( + locale=Culture.Chinese, + inline_or=" 要么 ", + inline_or_more=", 要么 ", + separator=", ", + no_in_language="不", + yes_in_language="是的", + ) + + Dutch = PromptCultureModel( + locale=Culture.Dutch, + inline_or=" of ", + inline_or_more=", of ", + separator=", ", + no_in_language="Nee", + yes_in_language="Ja", + ) + + English = PromptCultureModel( + locale=Culture.English, + inline_or=" or ", + inline_or_more=", or ", + separator=", ", + no_in_language="No", + yes_in_language="Yes", + ) + + French = PromptCultureModel( + locale=Culture.French, + inline_or=" ou ", + inline_or_more=", ou ", + separator=", ", + no_in_language="Non", + yes_in_language="Oui", + ) + + German = PromptCultureModel( + # TODO: Replace with Culture.German after Recognizers-Text package updates. + locale="de-de", + inline_or=" oder ", + inline_or_more=", oder ", + separator=", ", + no_in_language="Nein", + yes_in_language="Ja", + ) + + Italian = PromptCultureModel( + locale=Culture.Italian, + inline_or=" o ", + inline_or_more=" o ", + separator=", ", + no_in_language="No", + yes_in_language="Si", + ) + + Japanese = PromptCultureModel( + locale=Culture.Japanese, + inline_or=" または ", + inline_or_more="、 または ", + separator="、 ", + no_in_language="いいえ", + yes_in_language="はい", + ) + + Korean = PromptCultureModel( + locale=Culture.Korean, + inline_or=" 또는 ", + inline_or_more=" 또는 ", + separator=", ", + no_in_language="아니", + yes_in_language="예", + ) + + Portuguese = PromptCultureModel( + locale=Culture.Portuguese, + inline_or=" ou ", + inline_or_more=", ou ", + separator=", ", + no_in_language="Não", + yes_in_language="Sim", + ) + + Spanish = PromptCultureModel( + locale=Culture.Spanish, + inline_or=" o ", + inline_or_more=", o ", + separator=", ", + no_in_language="No", + yes_in_language="Sí", + ) + + Turkish = PromptCultureModel( + locale=Culture.Turkish, + inline_or=" veya ", + inline_or_more=" veya ", + separator=", ", + no_in_language="Hayır", + yes_in_language="Evet", + ) + + @classmethod + def map_to_nearest_language(cls, culture_code: str) -> str: + """ + Normalize various potential locale strings to a standard. + :param culture_code: Represents locale. Examples: "en-US, en-us, EN". + :return: Normalized locale. + :rtype: str + + .. remarks:: + In our other SDKs, this method is a copy/paste of the ones from the Recognizers-Text library. + However, that doesn't exist in Python. + """ + if culture_code: + culture_code = culture_code.lower() + supported_culture_codes = cls._get_supported_locales() + + if culture_code not in supported_culture_codes: + culture_prefix = culture_code.split("-")[0] + + for supported_culture_code in supported_culture_codes: + if supported_culture_code.startswith(culture_prefix): + culture_code = supported_culture_code + + return culture_code + + @classmethod + def get_supported_cultures(cls) -> List[PromptCultureModel]: + """ + Gets a list of the supported culture models. + """ + return [ + cls.Chinese, + cls.Dutch, + cls.English, + cls.French, + cls.Italian, + cls.Japanese, + cls.Korean, + cls.Portuguese, + cls.Spanish, + cls.Turkish, + ] + + @classmethod + def _get_supported_locales(cls) -> List[str]: + return [c.locale for c in cls.get_supported_cultures()] diff --git a/libraries/botbuilder-dialogs/tests/test_choice_prompt.py b/libraries/botbuilder-dialogs/tests/test_choice_prompt.py index 16cb16c9e..d8b312688 100644 --- a/libraries/botbuilder-dialogs/tests/test_choice_prompt.py +++ b/libraries/botbuilder-dialogs/tests/test_choice_prompt.py @@ -15,9 +15,10 @@ ChoiceRecognizers, FindChoicesOptions, ) -from botbuilder.dialogs.choices import Choice, ListStyle +from botbuilder.dialogs.choices import Choice, ChoiceFactoryOptions, ListStyle from botbuilder.dialogs.prompts import ( ChoicePrompt, + PromptCultureModel, PromptOptions, PromptValidatorContext, ) @@ -370,6 +371,222 @@ async def validator(prompt: PromptValidatorContext) -> bool: step3 = await step2.send(_answer_message) await step3.assert_reply("red") + async def test_should_default_to_english_locale(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) + + async def validator(prompt: PromptValidatorContext) -> bool: + assert prompt + + if not prompt.recognized.succeeded: + await prompt.context.send_activity("Bad input.") + + return prompt.recognized.succeeded + + locales = [None, "", "not-supported"] + + for locale in locales: + adapter = TestAdapter(exec_test) + + convo_state = ConversationState(MemoryStorage()) + dialog_state = convo_state.create_property("dialogState") + dialogs = DialogSet(dialog_state) + + choice_prompt = ChoicePrompt("prompt", validator) + dialogs.add(choice_prompt) + + step1 = await adapter.send( + Activity(type=ActivityTypes.message, text="Hello", locale=locale) + ) + 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_locale_variations_of_correct_locales(self): + def cap_ending(locale: str) -> str: + return f"{locale.split('-')[0]}-{locale.split('-')[1].upper()}" + + def title_ending(locale: str) -> str: + return locale[:3] + locale[3].upper() + locale[4:] + + def cap_two_letter(locale: str) -> str: + return locale.split("-")[0].upper() + + def lower_two_letter(locale: str) -> str: + return locale.split("-")[0].upper() + + async def exec_test_for_locale(valid_locale: str, locale_variations: List): + # Hold the correct answer from when a valid locale is used + expected_answer = None + + def inspector(activity: Activity): + nonlocal expected_answer + + if valid_locale == test_locale: + expected_answer = activity.text + else: + # Ensure we're actually testing a variation. + assert activity.locale != valid_locale + + assert activity.text == expected_answer + return True + + 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) + + async def validator(prompt: PromptValidatorContext) -> bool: + assert prompt + + if not prompt.recognized.succeeded: + await prompt.context.send_activity("Bad input.") + + return prompt.recognized.succeeded + + test_locale = None + for test_locale in locale_variations: + adapter = TestAdapter(exec_test) + + convo_state = ConversationState(MemoryStorage()) + dialog_state = convo_state.create_property("dialogState") + dialogs = DialogSet(dialog_state) + + choice_prompt = ChoicePrompt("prompt", validator) + dialogs.add(choice_prompt) + + step1 = await adapter.send( + Activity( + type=ActivityTypes.message, text="Hello", locale=test_locale + ) + ) + await step1.assert_reply(inspector) + + locales = [ + "zh-cn", + "nl-nl", + "en-us", + "fr-fr", + "de-de", + "it-it", + "ja-jp", + "ko-kr", + "pt-br", + "es-es", + "tr-tr", + "de-de", + ] + + locale_tests = [] + for locale in locales: + locale_tests.append( + [ + locale, + cap_ending(locale), + title_ending(locale), + cap_two_letter(locale), + lower_two_letter(locale), + ] + ) + + # Test each valid locale + for locale_tests in locale_tests: + await exec_test_for_locale(locale_tests[0], locale_tests) + + async def test_should_recognize_and_use_custom_locale_dict(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) + + async def validator(prompt: PromptValidatorContext) -> bool: + assert prompt + + if not prompt.recognized.succeeded: + await prompt.context.send_activity("Bad input.") + + return prompt.recognized.succeeded + + adapter = TestAdapter(exec_test) + + convo_state = ConversationState(MemoryStorage()) + dialog_state = convo_state.create_property("dialogState") + dialogs = DialogSet(dialog_state) + + culture = PromptCultureModel( + locale="custom-locale", + no_in_language="customNo", + yes_in_language="customYes", + separator="customSeparator", + inline_or="customInlineOr", + inline_or_more="customInlineOrMore", + ) + + custom_dict = { + culture.locale: ChoiceFactoryOptions( + inline_or=culture.inline_or, + inline_or_more=culture.inline_or_more, + inline_separator=culture.separator, + include_numbers=True, + ) + } + + choice_prompt = ChoicePrompt("prompt", validator, choice_defaults=custom_dict) + dialogs.add(choice_prompt) + + step1 = await adapter.send( + Activity(type=ActivityTypes.message, text="Hello", locale=culture.locale) + ) + await step1.assert_reply( + "Please choose a color. (1) redcustomSeparator(2) greencustomInlineOrMore(3) blue" + ) + 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) diff --git a/libraries/botbuilder-dialogs/tests/test_confirm_prompt.py b/libraries/botbuilder-dialogs/tests/test_confirm_prompt.py index 4b464936a..d65e34fe8 100644 --- a/libraries/botbuilder-dialogs/tests/test_confirm_prompt.py +++ b/libraries/botbuilder-dialogs/tests/test_confirm_prompt.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +from typing import List import aiounittest from botbuilder.core import ( ConversationState, @@ -10,9 +11,13 @@ ) from botbuilder.core.adapters import TestAdapter from botbuilder.dialogs import DialogSet, DialogTurnResult, DialogTurnStatus -from botbuilder.dialogs.choices import ChoiceFactoryOptions, ListStyle -from botbuilder.dialogs.prompts import ConfirmPrompt -from botbuilder.dialogs.prompts import PromptOptions +from botbuilder.dialogs.choices import Choice, ChoiceFactoryOptions, ListStyle +from botbuilder.dialogs.prompts import ( + ConfirmPrompt, + PromptCultureModel, + PromptOptions, + PromptValidatorContext, +) from botbuilder.schema import Activity, ActivityTypes @@ -274,3 +279,219 @@ async def exec_test(turn_context: TurnContext): ) step5 = await step4.send("no") await step5.assert_reply("Not confirmed") + + async def test_confirm_prompt_should_default_to_english_locale(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 confirm."), + retry_prompt=Activity( + type=ActivityTypes.message, + text="Please confirm, say 'yes' or 'no' or something like that.", + ), + ) + await dialog_context.prompt("ConfirmPrompt", options) + elif results.status == DialogTurnStatus.Complete: + message_text = "Confirmed" if results.result else "Not confirmed" + await turn_context.send_activity(MessageFactory.text(message_text)) + + await convo_state.save_changes(turn_context) + + locales = [None, "", "not-supported"] + + for locale in locales: + # 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) + confirm_prompt = ConfirmPrompt("ConfirmPrompt") + confirm_prompt.choice_options = ChoiceFactoryOptions(include_numbers=True) + dialogs.add(confirm_prompt) + + step1 = await adapter.send( + Activity(type=ActivityTypes.message, text="Hello", locale=locale) + ) + step2 = await step1.assert_reply("Please confirm. (1) Yes or (2) No") + step3 = await step2.send("lala") + step4 = await step3.assert_reply( + "Please confirm, say 'yes' or 'no' or something like that. (1) Yes or (2) No" + ) + step5 = await step4.send("2") + await step5.assert_reply("Not confirmed") + + async def test_should_recognize_locale_variations_of_correct_locales(self): + def cap_ending(locale: str) -> str: + return f"{locale.split('-')[0]}-{locale.split('-')[1].upper()}" + + def title_ending(locale: str) -> str: + return locale[:3] + locale[3].upper() + locale[4:] + + def cap_two_letter(locale: str) -> str: + return locale.split("-")[0].upper() + + def lower_two_letter(locale: str) -> str: + return locale.split("-")[0].upper() + + async def exec_test_for_locale(valid_locale: str, locale_variations: List): + # Hold the correct answer from when a valid locale is used + expected_answer = None + + def inspector(activity: Activity): + nonlocal expected_answer + + if valid_locale == test_locale: + expected_answer = activity.text + else: + # Ensure we're actually testing a variation. + assert activity.locale != valid_locale + + assert activity.text == expected_answer + return True + + 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 confirm." + ) + ) + await dialog_context.prompt("prompt", options) + elif results.status == DialogTurnStatus.Complete: + confirmed = results.result + if confirmed: + await turn_context.send_activity("true") + else: + await turn_context.send_activity("false") + + await convo_state.save_changes(turn_context) + + async def validator(prompt: PromptValidatorContext) -> bool: + assert prompt + + if not prompt.recognized.succeeded: + await prompt.context.send_activity("Bad input.") + + return prompt.recognized.succeeded + + test_locale = None + for test_locale in locale_variations: + adapter = TestAdapter(exec_test) + + convo_state = ConversationState(MemoryStorage()) + dialog_state = convo_state.create_property("dialogState") + dialogs = DialogSet(dialog_state) + + choice_prompt = ConfirmPrompt("prompt", validator) + dialogs.add(choice_prompt) + + step1 = await adapter.send( + Activity( + type=ActivityTypes.message, text="Hello", locale=test_locale + ) + ) + await step1.assert_reply(inspector) + + locales = [ + "zh-cn", + "nl-nl", + "en-us", + "fr-fr", + "de-de", + "it-it", + "ja-jp", + "ko-kr", + "pt-br", + "es-es", + "tr-tr", + "de-de", + ] + + locale_tests = [] + for locale in locales: + locale_tests.append( + [ + locale, + cap_ending(locale), + title_ending(locale), + cap_two_letter(locale), + lower_two_letter(locale), + ] + ) + + # Test each valid locale + for locale_tests in locale_tests: + await exec_test_for_locale(locale_tests[0], locale_tests) + + async def test_should_recognize_and_use_custom_locale_dict(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 confirm.") + ) + 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) + + async def validator(prompt: PromptValidatorContext) -> bool: + assert prompt + + if not prompt.recognized.succeeded: + await prompt.context.send_activity("Bad input.") + + return prompt.recognized.succeeded + + adapter = TestAdapter(exec_test) + + convo_state = ConversationState(MemoryStorage()) + dialog_state = convo_state.create_property("dialogState") + dialogs = DialogSet(dialog_state) + + culture = PromptCultureModel( + locale="custom-locale", + no_in_language="customNo", + yes_in_language="customYes", + separator="customSeparator", + inline_or="customInlineOr", + inline_or_more="customInlineOrMore", + ) + + custom_dict = { + culture.locale: ( + Choice(culture.yes_in_language), + Choice(culture.no_in_language), + ChoiceFactoryOptions( + culture.separator, culture.inline_or, culture.inline_or_more, True + ), + ) + } + + confirm_prompt = ConfirmPrompt("prompt", validator, choice_defaults=custom_dict) + dialogs.add(confirm_prompt) + + step1 = await adapter.send( + Activity(type=ActivityTypes.message, text="Hello", locale=culture.locale) + ) + await step1.assert_reply( + "Please confirm. (1) customYescustomInlineOr(2) customNo" + ) From f5e4e2f4cf220d56dbc9e04acd841fcb7a3db056 Mon Sep 17 00:00:00 2001 From: Michael Richardson Date: Thu, 4 Mar 2021 12:05:05 -0800 Subject: [PATCH 2/2] add description back to inspector --- libraries/botbuilder-dialogs/tests/test_choice_prompt.py | 4 +++- libraries/botbuilder-dialogs/tests/test_confirm_prompt.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/libraries/botbuilder-dialogs/tests/test_choice_prompt.py b/libraries/botbuilder-dialogs/tests/test_choice_prompt.py index d8b312688..ce3035603 100644 --- a/libraries/botbuilder-dialogs/tests/test_choice_prompt.py +++ b/libraries/botbuilder-dialogs/tests/test_choice_prompt.py @@ -437,9 +437,11 @@ async def exec_test_for_locale(valid_locale: str, locale_variations: List): # Hold the correct answer from when a valid locale is used expected_answer = None - def inspector(activity: Activity): + def inspector(activity: Activity, description: str): nonlocal expected_answer + assert not description + if valid_locale == test_locale: expected_answer = activity.text else: diff --git a/libraries/botbuilder-dialogs/tests/test_confirm_prompt.py b/libraries/botbuilder-dialogs/tests/test_confirm_prompt.py index d65e34fe8..905eea4a0 100644 --- a/libraries/botbuilder-dialogs/tests/test_confirm_prompt.py +++ b/libraries/botbuilder-dialogs/tests/test_confirm_prompt.py @@ -346,9 +346,11 @@ async def exec_test_for_locale(valid_locale: str, locale_variations: List): # Hold the correct answer from when a valid locale is used expected_answer = None - def inspector(activity: Activity): + def inspector(activity: Activity, description: str): nonlocal expected_answer + assert not description + if valid_locale == test_locale: expected_answer = activity.text else: