From b13c1050b7c92edbf1cb09158f2d67d6252cb1f1 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Thu, 25 Jul 2019 15:56:56 -0700 Subject: [PATCH 1/2] recognizers versions updated, confirm prompt now working with tests --- .../dialogs/choices/choice_factory.py | 4 +- .../dialogs/prompts/confirm_prompt.py | 23 +- .../botbuilder/dialogs/prompts/prompt.py | 2 +- libraries/botbuilder-dialogs/setup.py | 10 +- .../tests/test_confirm_prompt.py | 276 ++++++++++++++++++ 5 files changed, 299 insertions(+), 16 deletions(-) create mode 100644 libraries/botbuilder-dialogs/tests/test_confirm_prompt.py diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_factory.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_factory.py index 556d6e37c..6acc1cbec 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_factory.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_factory.py @@ -99,7 +99,9 @@ def inline( inline_separator=options.inline_separator or ", ", inline_or=options.inline_or or " or ", inline_or_more=options.inline_or_more or ", or ", - include_numbers=options.include_numbers or True, + include_numbers=( + options.include_numbers if options.include_numbers is not None else True + ), ) # Format list of choices diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/confirm_prompt.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/confirm_prompt.py index 08c882019..f307e1f49 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/confirm_prompt.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/confirm_prompt.py @@ -2,9 +2,15 @@ # Licensed under the MIT License. from typing import Dict +from recognizers_choice import recognize_boolean from botbuilder.core.turn_context import TurnContext from botbuilder.schema import ActivityTypes, Activity -from botbuilder.dialogs.choices import Choice, ChoiceFactoryOptions, ListStyle +from botbuilder.dialogs.choices import ( + Choice, + ChoiceFactoryOptions, + ChoiceRecognizers, + ListStyle, +) from .prompt import Prompt from .prompt_options import PromptOptions from .prompt_recognizer_result import PromptRecognizerResult @@ -93,7 +99,7 @@ async def on_prompt( if self.confirm_choices is not None else (defaults[0], defaults[1]) ) - choices = {confirms[0], confirms[1]} + choices = [confirms[0], confirms[1]] if is_retry and options.retry_prompt is not None: prompt = self.append_choices( options.retry_prompt, channel_id, choices, self.style, choice_opts @@ -110,7 +116,6 @@ async def on_recognize( state: Dict[str, object], options: PromptOptions, ) -> PromptRecognizerResult: - # pylint: disable=undefined-variable if not turn_context: raise TypeError("ConfirmPrompt.on_prompt(): turn_context cannot be None.") @@ -119,13 +124,12 @@ async def on_recognize( # Recognize utterance message = turn_context.activity culture = self.determine_culture(turn_context.activity) - # TODO: Port ChoiceRecognizer - results = ChoiceRecognizer.recognize_boolean(message.text, culture) - if results.Count > 0: + results = recognize_boolean(message.text, culture) + if results: first = results[0] - if "value" in first.Resolution: + if "value" in first.resolution: result.succeeded = True - result.value = str(first.Resolution["value"]) + result.value = first.resolution["value"] else: # First check whether the prompt was sent to the user with numbers # if it was we should recognize numbers @@ -138,7 +142,7 @@ async def on_recognize( # This logic reflects the fact that IncludeNumbers is nullable and True is the default set in # Inline style - if opts.include_numbers.has_value or opts.include_numbers.value: + if opts.include_numbers is None or opts.include_numbers: # The text may be a number in which case we will interpret that as a choice. confirm_choices = ( self.confirm_choices @@ -146,7 +150,6 @@ async def on_recognize( else (defaults[0], defaults[1]) ) choices = {confirm_choices[0], confirm_choices[1]} - # TODO: Port ChoiceRecognizer second_attempt_results = ChoiceRecognizers.recognize_choices( message.text, choices ) diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt.py index e129ffab4..1e907ae1d 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt.py @@ -204,7 +204,7 @@ def default() -> Activity: msg = switcher.get(int(style.value), default)() # Update prompt with text, actions and attachments - if not prompt: + if prompt: # clone the prompt the set in the options (note ActivityEx has Properties so this is the safest mechanism) prompt = copy.copy(prompt) diff --git a/libraries/botbuilder-dialogs/setup.py b/libraries/botbuilder-dialogs/setup.py index cbc347851..89ae857f1 100644 --- a/libraries/botbuilder-dialogs/setup.py +++ b/libraries/botbuilder-dialogs/setup.py @@ -5,10 +5,12 @@ from setuptools import setup REQUIRES = [ - "recognizers-date-time>=1.0.0a1", - "recognizers-number-with-unit>=1.0.0a1", - "recognizers-number>=1.0.0a2", - "recognizers-text>=1.0.0a1", + "recognizers-text-date-time>=1.0.1a0", + "recognizers-text-number-with-unit>=1.0.1a0", + "recognizers-text-number>=1.0.1a0", + "recognizers-text>=1.0.1a0", + "recognizers-text-choice>=1.0.1a0", + "grapheme>=0.5.0", "botbuilder-schema>=4.4.0b1", "botframework-connector>=4.4.0b1", "botbuilder-core>=4.4.0b1", diff --git a/libraries/botbuilder-dialogs/tests/test_confirm_prompt.py b/libraries/botbuilder-dialogs/tests/test_confirm_prompt.py new file mode 100644 index 000000000..4b464936a --- /dev/null +++ b/libraries/botbuilder-dialogs/tests/test_confirm_prompt.py @@ -0,0 +1,276 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import aiounittest +from botbuilder.core import ( + ConversationState, + MemoryStorage, + TurnContext, + MessageFactory, +) +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.schema import Activity, ActivityTypes + + +class ConfirmPromptTest(aiounittest.AsyncTestCase): + def test_confirm_prompt_with_empty_id_should_fail(self): + empty_id = "" + + with self.assertRaises(TypeError): + ConfirmPrompt(empty_id) + + def test_confirm_prompt_with_none_id_should_fail(self): + none_id = None + + with self.assertRaises(TypeError): + ConfirmPrompt(none_id) + + async def test_confirm_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 confirm.") + ) + 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) + + # 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", default_locale="English") + dialogs.add(confirm_prompt) + + step1 = await adapter.send("hello") + step2 = await step1.assert_reply("Please confirm. (1) Yes or (2) No") + step3 = await step2.send("yes") + await step3.assert_reply("Confirmed") + + async def test_confirm_prompt_retry(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) + + # 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", default_locale="English") + dialogs.add(confirm_prompt) + + step1 = await adapter.send("hello") + 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("no") + await step5.assert_reply("Not confirmed") + + async def test_confirm_prompt_no_options(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: + await dialog_context.prompt("ConfirmPrompt", PromptOptions()) + 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) + + # 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", default_locale="English") + dialogs.add(confirm_prompt) + + step1 = await adapter.send("hello") + step2 = await step1.assert_reply(" (1) Yes or (2) No") + step3 = await step2.send("lala") + step4 = await step3.assert_reply(" (1) Yes or (2) No") + step5 = await step4.send("no") + await step5.assert_reply("Not confirmed") + + async def test_confirm_prompt_choice_options_numbers(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) + + # 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", default_locale="English") + confirm_prompt.choice_options = ChoiceFactoryOptions(include_numbers=True) + confirm_prompt.style = ListStyle.in_line + dialogs.add(confirm_prompt) + + step1 = await adapter.send("hello") + 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_confirm_prompt_choice_options_multiple_attempts(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) + + # 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", default_locale="English") + confirm_prompt.choice_options = ChoiceFactoryOptions(include_numbers=True) + confirm_prompt.style = ListStyle.in_line + dialogs.add(confirm_prompt) + + step1 = await adapter.send("hello") + 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("what") + step6 = await step5.assert_reply( + "Please confirm, say 'yes' or 'no' or something like that. (1) Yes or (2) No" + ) + step7 = await step6.send("2") + await step7.assert_reply("Not confirmed") + + async def test_confirm_prompt_options_no_numbers(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) + + # 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", default_locale="English") + confirm_prompt.choice_options = ChoiceFactoryOptions( + include_numbers=False, inline_separator="~" + ) + dialogs.add(confirm_prompt) + + step1 = await adapter.send("hello") + step2 = await step1.assert_reply("Please confirm. Yes or No") + step3 = await step2.send("2") + step4 = await step3.assert_reply( + "Please confirm, say 'yes' or 'no' or something like that. Yes or No" + ) + step5 = await step4.send("no") + await step5.assert_reply("Not confirmed") From 6349c7ad09ab6cda6655567dbd613a0bfffd5b7f Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Thu, 25 Jul 2019 16:16:31 -0700 Subject: [PATCH 2/2] added dependency library for recognizers-text --- libraries/botbuilder-dialogs/setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/botbuilder-dialogs/setup.py b/libraries/botbuilder-dialogs/setup.py index 89ae857f1..36d8739dd 100644 --- a/libraries/botbuilder-dialogs/setup.py +++ b/libraries/botbuilder-dialogs/setup.py @@ -11,6 +11,7 @@ "recognizers-text>=1.0.1a0", "recognizers-text-choice>=1.0.1a0", "grapheme>=0.5.0", + "emoji>=0.5.2", "botbuilder-schema>=4.4.0b1", "botframework-connector>=4.4.0b1", "botbuilder-core>=4.4.0b1",