From 9cd693ddfd55c7ddff4073101c329f080d5435e1 Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Fri, 13 Dec 2019 12:31:08 -0600 Subject: [PATCH] Removing samples and generator from Python repo --- generators/LICENSE.md | 21 - generators/README.md | 215 -------- .../app/templates/core/cookiecutter.json | 4 - .../core/{{cookiecutter.bot_name}}/.pylintrc | 498 ------------------ .../{{cookiecutter.bot_name}}/README-LUIS.md | 216 -------- .../core/{{cookiecutter.bot_name}}/README.md | 61 --- .../{{cookiecutter.bot_name}}/__init__.py | 2 - .../core/{{cookiecutter.bot_name}}/app.py | 110 ---- .../booking_details.py | 18 - .../bots/__init__.py | 7 - .../bots/dialog_and_welcome_bot.py | 42 -- .../bots/dialog_bot.py | 42 -- .../cards/welcomeCard.json | 46 -- .../cognitiveModels/FlightBooking.json | 339 ------------ .../core/{{cookiecutter.bot_name}}/config.py | 16 - .../dialogs/__init__.py | 9 - .../dialogs/booking_dialog.py | 137 ----- .../dialogs/cancel_and_help_dialog.py | 44 -- .../dialogs/date_resolver_dialog.py | 79 --- .../dialogs/main_dialog.py | 133 ----- .../flight_booking_recognizer.py | 32 -- .../helpers/__init__.py | 11 - .../helpers/dialog_helper.py | 19 - .../helpers/luis_helper.py | 104 ---- .../requirements.txt | 5 - .../app/templates/echo/cookiecutter.json | 4 - .../echo/{{cookiecutter.bot_name}}/.pylintrc | 497 ----------------- .../echo/{{cookiecutter.bot_name}}/README.md | 43 -- .../{{cookiecutter.bot_name}}/__init__.py | 0 .../echo/{{cookiecutter.bot_name}}/app.py | 86 --- .../echo/{{cookiecutter.bot_name}}/bot.py | 21 - .../echo/{{cookiecutter.bot_name}}/config.py | 12 - .../requirements.txt | 3 - .../app/templates/empty/cookiecutter.json | 4 - .../empty/{{cookiecutter.bot_name}}/.pylintrc | 497 ----------------- .../empty/{{cookiecutter.bot_name}}/README.md | 43 -- .../{{cookiecutter.bot_name}}/__init__.py | 0 .../empty/{{cookiecutter.bot_name}}/app.py | 72 --- .../empty/{{cookiecutter.bot_name}}/bot.py | 16 - .../empty/{{cookiecutter.bot_name}}/config.py | 12 - .../requirements.txt | 3 - samples/01.console-echo/README.md | 35 -- samples/01.console-echo/adapter/__init__.py | 6 - .../adapter/console_adapter.py | 180 ------- samples/01.console-echo/bot.py | 16 - samples/01.console-echo/main.py | 25 - samples/01.console-echo/requirements.txt | 4 - samples/02.echo-bot/README.md | 30 -- samples/02.echo-bot/app.py | 82 --- samples/02.echo-bot/bots/__init__.py | 6 - samples/02.echo-bot/bots/echo_bot.py | 17 - samples/02.echo-bot/config.py | 15 - .../template-with-preexisting-rg.json | 242 --------- samples/02.echo-bot/requirements.txt | 2 - samples/03.welcome-user/README.md | 36 -- samples/03.welcome-user/app.py | 95 ---- samples/03.welcome-user/bots/__init__.py | 6 - .../03.welcome-user/bots/welcome_user_bot.py | 143 ----- samples/03.welcome-user/config.py | 15 - .../03.welcome-user/data_models/__init__.py | 6 - .../data_models/welcome_user_state.py | 7 - samples/03.welcome-user/requirements.txt | 2 - samples/05.multi-turn-prompt/README.md | 50 -- samples/05.multi-turn-prompt/app.py | 106 ---- samples/05.multi-turn-prompt/bots/__init__.py | 6 - .../05.multi-turn-prompt/bots/dialog_bot.py | 51 -- samples/05.multi-turn-prompt/config.py | 15 - .../data_models/__init__.py | 6 - .../data_models/user_profile.py | 13 - .../05.multi-turn-prompt/dialogs/__init__.py | 6 - .../dialogs/user_profile_dialog.py | 167 ------ .../05.multi-turn-prompt/helpers/__init__.py | 6 - .../helpers/dialog_helper.py | 19 - samples/05.multi-turn-prompt/requirements.txt | 4 - samples/06.using-cards/README.md | 50 -- samples/06.using-cards/app.py | 108 ---- samples/06.using-cards/bots/__init__.py | 6 - samples/06.using-cards/bots/dialog_bot.py | 42 -- samples/06.using-cards/bots/rich_cards_bot.py | 28 - samples/06.using-cards/config.py | 15 - samples/06.using-cards/dialogs/__init__.py | 6 - samples/06.using-cards/dialogs/main_dialog.py | 298 ----------- .../dialogs/resources/__init__.py | 6 - .../resources/adaptive_card_example.py | 186 ------- samples/06.using-cards/helpers/__init__.py | 6 - .../06.using-cards/helpers/activity_helper.py | 45 -- .../06.using-cards/helpers/dialog_helper.py | 19 - samples/06.using-cards/requirements.txt | 3 - samples/08.suggested-actions/README.md | 28 - samples/08.suggested-actions/app.py | 90 ---- samples/08.suggested-actions/bots/__init__.py | 6 - .../bots/suggested_actions_bot.py | 81 --- samples/08.suggested-actions/config.py | 15 - samples/08.suggested-actions/requirements.txt | 2 - samples/11.qnamaker/README.md | 56 -- samples/11.qnamaker/app.py | 82 --- samples/11.qnamaker/bots/__init__.py | 6 - samples/11.qnamaker/bots/qna_bot.py | 37 -- .../cognitiveModels/smartLightFAQ.tsv | 15 - samples/11.qnamaker/config.py | 18 - .../template-with-preexisting-rg.json | 242 --------- samples/11.qnamaker/requirements.txt | 3 - samples/13.core-bot/README-LUIS.md | 216 -------- samples/13.core-bot/README.md | 61 --- .../13.core-bot/adapter_with_error_handler.py | 54 -- samples/13.core-bot/app.py | 79 --- samples/13.core-bot/booking_details.py | 18 - samples/13.core-bot/bots/__init__.py | 7 - .../bots/dialog_and_welcome_bot.py | 57 -- samples/13.core-bot/bots/dialog_bot.py | 41 -- samples/13.core-bot/cards/welcomeCard.json | 46 -- .../cognitiveModels/FlightBooking.json | 339 ------------ samples/13.core-bot/config.py | 19 - samples/13.core-bot/dialogs/__init__.py | 9 - samples/13.core-bot/dialogs/booking_dialog.py | 136 ----- .../dialogs/cancel_and_help_dialog.py | 47 -- .../dialogs/date_resolver_dialog.py | 80 --- samples/13.core-bot/dialogs/main_dialog.py | 132 ----- .../13.core-bot/flight_booking_recognizer.py | 32 -- samples/13.core-bot/helpers/__init__.py | 6 - .../13.core-bot/helpers/activity_helper.py | 37 -- samples/13.core-bot/helpers/dialog_helper.py | 19 - samples/13.core-bot/helpers/luis_helper.py | 102 ---- samples/13.core-bot/requirements.txt | 5 - samples/15.handling-attachments/README.md | 38 -- samples/15.handling-attachments/app.py | 89 ---- .../15.handling-attachments/bots/__init__.py | 6 - .../bots/attachments_bot.py | 218 -------- samples/15.handling-attachments/config.py | 15 - .../template-with-preexisting-rg.json | 242 --------- .../15.handling-attachments/requirements.txt | 3 - .../resources/architecture-resize.png | Bin 241516 -> 0 bytes samples/16.proactive-messages/README.md | 66 --- samples/16.proactive-messages/app.py | 123 ----- .../16.proactive-messages/bots/__init__.py | 6 - .../bots/proactive_bot.py | 45 -- samples/16.proactive-messages/config.py | 15 - .../16.proactive-messages/requirements.txt | 2 - samples/17.multilingual-bot/README.md | 58 -- samples/17.multilingual-bot/app.py | 105 ---- samples/17.multilingual-bot/bots/__init__.py | 6 - .../bots/multilingual_bot.py | 122 ----- .../cards/welcomeCard.json | 46 -- samples/17.multilingual-bot/config.py | 17 - .../template-with-preexisting-rg.json | 242 --------- samples/17.multilingual-bot/requirements.txt | 3 - .../translation/__init__.py | 7 - .../translation/microsoft_translator.py | 37 -- .../translation/translation_middleware.py | 88 ---- .../translation/translation_settings.py | 12 - samples/18.bot-authentication/README.md | 56 -- samples/18.bot-authentication/app.py | 104 ---- .../18.bot-authentication/bots/__init__.py | 7 - .../18.bot-authentication/bots/auth_bot.py | 44 -- .../18.bot-authentication/bots/dialog_bot.py | 41 -- samples/18.bot-authentication/config.py | 16 - .../template-with-preexisting-rg.json | 242 --------- .../18.bot-authentication/dialogs/__init__.py | 7 - .../dialogs/logout_dialog.py | 29 - .../dialogs/main_dialog.py | 94 ---- .../18.bot-authentication/helpers/__init__.py | 6 - .../helpers/dialog_helper.py | 19 - .../18.bot-authentication/requirements.txt | 2 - samples/19.custom-dialogs/README.md | 48 -- samples/19.custom-dialogs/app.py | 101 ---- samples/19.custom-dialogs/bots/__init__.py | 6 - samples/19.custom-dialogs/bots/dialog_bot.py | 34 -- samples/19.custom-dialogs/config.py | 15 - samples/19.custom-dialogs/dialogs/__init__.py | 7 - .../19.custom-dialogs/dialogs/root_dialog.py | 134 ----- .../19.custom-dialogs/dialogs/slot_details.py | 28 - .../dialogs/slot_filling_dialog.py | 100 ---- samples/19.custom-dialogs/helpers/__init__.py | 6 - .../helpers/dialog_helper.py | 19 - samples/19.custom-dialogs/requirements.txt | 2 - samples/21.corebot-app-insights/NOTICE.md | 8 - .../21.corebot-app-insights/README-LUIS.md | 216 -------- samples/21.corebot-app-insights/README.md | 65 --- samples/21.corebot-app-insights/app.py | 123 ----- .../booking_details.py | 14 - .../21.corebot-app-insights/bots/__init__.py | 8 - .../bots/dialog_and_welcome_bot.py | 63 --- .../bots/dialog_bot.py | 71 --- .../bots/resources/welcomeCard.json | 46 -- .../cognitiveModels/FlightBooking.json | 226 -------- samples/21.corebot-app-insights/config.py | 21 - .../dialogs/__init__.py | 9 - .../dialogs/booking_dialog.py | 132 ----- .../dialogs/cancel_and_help_dialog.py | 55 -- .../dialogs/date_resolver_dialog.py | 91 ---- .../dialogs/main_dialog.py | 115 ---- .../helpers/__init__.py | 7 - .../helpers/activity_helper.py | 38 -- .../helpers/dialog_helper.py | 22 - .../helpers/luis_helper.py | 71 --- .../21.corebot-app-insights/requirements.txt | 13 - samples/23.facebook-events/README.md | 36 -- samples/23.facebook-events/app.py | 89 ---- samples/23.facebook-events/bots/__init__.py | 6 - .../23.facebook-events/bots/facebook_bot.py | 129 ----- samples/23.facebook-events/config.py | 15 - .../template-with-preexisting-rg.json | 242 --------- samples/23.facebook-events/requirements.txt | 3 - samples/40.timex-resolution/README.md | 51 -- samples/40.timex-resolution/ambiguity.py | 78 --- samples/40.timex-resolution/constraints.py | 31 -- .../language_generation.py | 33 -- samples/40.timex-resolution/main.py | 23 - samples/40.timex-resolution/parsing.py | 45 -- samples/40.timex-resolution/ranges.py | 51 -- samples/40.timex-resolution/requirements.txt | 3 - samples/40.timex-resolution/resolution.py | 26 - samples/42.scaleout/README.md | 36 -- samples/42.scaleout/app.py | 96 ---- samples/42.scaleout/bots/__init__.py | 6 - samples/42.scaleout/bots/scaleout_bot.py | 45 -- samples/42.scaleout/config.py | 18 - .../template-with-preexisting-rg.json | 242 --------- samples/42.scaleout/dialogs/__init__.py | 6 - samples/42.scaleout/dialogs/root_dialog.py | 56 -- samples/42.scaleout/helpers/__init__.py | 6 - samples/42.scaleout/helpers/dialog_helper.py | 19 - samples/42.scaleout/host/__init__.py | 7 - samples/42.scaleout/host/dialog_host.py | 72 --- .../42.scaleout/host/dialog_host_adapter.py | 32 -- samples/42.scaleout/requirements.txt | 4 - samples/42.scaleout/store/__init__.py | 9 - samples/42.scaleout/store/blob_store.py | 51 -- samples/42.scaleout/store/memory_store.py | 29 - samples/42.scaleout/store/ref_accessor.py | 37 -- samples/42.scaleout/store/store.py | 32 -- samples/43.complex-dialog/README.md | 30 -- samples/43.complex-dialog/app.py | 106 ---- samples/43.complex-dialog/bots/__init__.py | 7 - .../bots/dialog_and_welcome_bot.py | 39 -- samples/43.complex-dialog/bots/dialog_bot.py | 41 -- samples/43.complex-dialog/config.py | 15 - .../43.complex-dialog/data_models/__init__.py | 6 - .../data_models/user_profile.py | 13 - samples/43.complex-dialog/dialogs/__init__.py | 8 - .../43.complex-dialog/dialogs/main_dialog.py | 50 -- .../dialogs/review_selection_dialog.py | 99 ---- .../dialogs/top_level_dialog.py | 95 ---- samples/43.complex-dialog/helpers/__init__.py | 6 - .../helpers/dialog_helper.py | 19 - samples/43.complex-dialog/requirements.txt | 2 - samples/44.prompt-users-for-input/README.md | 37 -- samples/44.prompt-users-for-input/app.py | 103 ---- .../bots/__init__.py | 6 - .../bots/custom_prompt_bot.py | 189 ------- samples/44.prompt-users-for-input/config.py | 15 - .../data_models/__init__.py | 7 - .../data_models/conversation_flow.py | 18 - .../data_models/user_profile.py | 9 - .../requirements.txt | 3 - samples/45.state-management/README.md | 36 -- samples/45.state-management/app.py | 100 ---- samples/45.state-management/bots/__init__.py | 6 - .../bots/state_management_bot.py | 97 ---- samples/45.state-management/config.py | 15 - .../data_models/__init__.py | 7 - .../data_models/conversation_data.py | 14 - .../data_models/user_profile.py | 7 - samples/45.state-management/requirements.txt | 2 - samples/47.inspection/README.md | 46 -- samples/47.inspection/app.py | 117 ---- samples/47.inspection/bots/__init__.py | 6 - samples/47.inspection/bots/echo_bot.py | 64 --- samples/47.inspection/config.py | 15 - samples/47.inspection/data_models/__init__.py | 6 - .../47.inspection/data_models/custom_state.py | 7 - samples/47.inspection/requirements.txt | 2 - samples/README.md | 14 - .../python_django/13.core-bot/README-LUIS.md | 216 -------- samples/python_django/13.core-bot/README.md | 61 --- .../13.core-bot/booking_details.py | 15 - .../13.core-bot/bots/__init__.py | 8 - .../python_django/13.core-bot/bots/bots.py | 54 -- .../bots/dialog_and_welcome_bot.py | 44 -- .../13.core-bot/bots/dialog_bot.py | 47 -- .../bots/resources/welcomeCard.json | 46 -- .../13.core-bot/bots/settings.py | 118 ----- .../python_django/13.core-bot/bots/urls.py | 15 - .../python_django/13.core-bot/bots/views.py | 53 -- .../python_django/13.core-bot/bots/wsgi.py | 19 - .../cognitiveModels/FlightBooking.json | 226 -------- samples/python_django/13.core-bot/config.py | 19 - samples/python_django/13.core-bot/db.sqlite3 | 0 .../13.core-bot/dialogs/__init__.py | 9 - .../13.core-bot/dialogs/booking_dialog.py | 119 ----- .../dialogs/cancel_and_help_dialog.py | 45 -- .../dialogs/date_resolver_dialog.py | 82 --- .../13.core-bot/dialogs/main_dialog.py | 83 --- .../13.core-bot/helpers/__init__.py | 7 - .../13.core-bot/helpers/activity_helper.py | 38 -- .../13.core-bot/helpers/dialog_helper.py | 22 - .../13.core-bot/helpers/luis_helper.py | 63 --- samples/python_django/13.core-bot/manage.py | 28 - .../13.core-bot/requirements.txt | 9 - 299 files changed, 16769 deletions(-) delete mode 100644 generators/LICENSE.md delete mode 100644 generators/README.md delete mode 100644 generators/app/templates/core/cookiecutter.json delete mode 100644 generators/app/templates/core/{{cookiecutter.bot_name}}/.pylintrc delete mode 100644 generators/app/templates/core/{{cookiecutter.bot_name}}/README-LUIS.md delete mode 100644 generators/app/templates/core/{{cookiecutter.bot_name}}/README.md delete mode 100644 generators/app/templates/core/{{cookiecutter.bot_name}}/__init__.py delete mode 100644 generators/app/templates/core/{{cookiecutter.bot_name}}/app.py delete mode 100644 generators/app/templates/core/{{cookiecutter.bot_name}}/booking_details.py delete mode 100644 generators/app/templates/core/{{cookiecutter.bot_name}}/bots/__init__.py delete mode 100644 generators/app/templates/core/{{cookiecutter.bot_name}}/bots/dialog_and_welcome_bot.py delete mode 100644 generators/app/templates/core/{{cookiecutter.bot_name}}/bots/dialog_bot.py delete mode 100644 generators/app/templates/core/{{cookiecutter.bot_name}}/cards/welcomeCard.json delete mode 100644 generators/app/templates/core/{{cookiecutter.bot_name}}/cognitiveModels/FlightBooking.json delete mode 100644 generators/app/templates/core/{{cookiecutter.bot_name}}/config.py delete mode 100644 generators/app/templates/core/{{cookiecutter.bot_name}}/dialogs/__init__.py delete mode 100644 generators/app/templates/core/{{cookiecutter.bot_name}}/dialogs/booking_dialog.py delete mode 100644 generators/app/templates/core/{{cookiecutter.bot_name}}/dialogs/cancel_and_help_dialog.py delete mode 100644 generators/app/templates/core/{{cookiecutter.bot_name}}/dialogs/date_resolver_dialog.py delete mode 100644 generators/app/templates/core/{{cookiecutter.bot_name}}/dialogs/main_dialog.py delete mode 100644 generators/app/templates/core/{{cookiecutter.bot_name}}/flight_booking_recognizer.py delete mode 100644 generators/app/templates/core/{{cookiecutter.bot_name}}/helpers/__init__.py delete mode 100644 generators/app/templates/core/{{cookiecutter.bot_name}}/helpers/dialog_helper.py delete mode 100644 generators/app/templates/core/{{cookiecutter.bot_name}}/helpers/luis_helper.py delete mode 100644 generators/app/templates/core/{{cookiecutter.bot_name}}/requirements.txt delete mode 100644 generators/app/templates/echo/cookiecutter.json delete mode 100644 generators/app/templates/echo/{{cookiecutter.bot_name}}/.pylintrc delete mode 100644 generators/app/templates/echo/{{cookiecutter.bot_name}}/README.md delete mode 100644 generators/app/templates/echo/{{cookiecutter.bot_name}}/__init__.py delete mode 100644 generators/app/templates/echo/{{cookiecutter.bot_name}}/app.py delete mode 100644 generators/app/templates/echo/{{cookiecutter.bot_name}}/bot.py delete mode 100644 generators/app/templates/echo/{{cookiecutter.bot_name}}/config.py delete mode 100644 generators/app/templates/echo/{{cookiecutter.bot_name}}/requirements.txt delete mode 100644 generators/app/templates/empty/cookiecutter.json delete mode 100644 generators/app/templates/empty/{{cookiecutter.bot_name}}/.pylintrc delete mode 100644 generators/app/templates/empty/{{cookiecutter.bot_name}}/README.md delete mode 100644 generators/app/templates/empty/{{cookiecutter.bot_name}}/__init__.py delete mode 100644 generators/app/templates/empty/{{cookiecutter.bot_name}}/app.py delete mode 100644 generators/app/templates/empty/{{cookiecutter.bot_name}}/bot.py delete mode 100644 generators/app/templates/empty/{{cookiecutter.bot_name}}/config.py delete mode 100644 generators/app/templates/empty/{{cookiecutter.bot_name}}/requirements.txt delete mode 100644 samples/01.console-echo/README.md delete mode 100644 samples/01.console-echo/adapter/__init__.py delete mode 100644 samples/01.console-echo/adapter/console_adapter.py delete mode 100644 samples/01.console-echo/bot.py delete mode 100644 samples/01.console-echo/main.py delete mode 100644 samples/01.console-echo/requirements.txt delete mode 100644 samples/02.echo-bot/README.md delete mode 100644 samples/02.echo-bot/app.py delete mode 100644 samples/02.echo-bot/bots/__init__.py delete mode 100644 samples/02.echo-bot/bots/echo_bot.py delete mode 100644 samples/02.echo-bot/config.py delete mode 100644 samples/02.echo-bot/deploymentTemplates/template-with-preexisting-rg.json delete mode 100644 samples/02.echo-bot/requirements.txt delete mode 100644 samples/03.welcome-user/README.md delete mode 100644 samples/03.welcome-user/app.py delete mode 100644 samples/03.welcome-user/bots/__init__.py delete mode 100644 samples/03.welcome-user/bots/welcome_user_bot.py delete mode 100644 samples/03.welcome-user/config.py delete mode 100644 samples/03.welcome-user/data_models/__init__.py delete mode 100644 samples/03.welcome-user/data_models/welcome_user_state.py delete mode 100644 samples/03.welcome-user/requirements.txt delete mode 100644 samples/05.multi-turn-prompt/README.md delete mode 100644 samples/05.multi-turn-prompt/app.py delete mode 100644 samples/05.multi-turn-prompt/bots/__init__.py delete mode 100644 samples/05.multi-turn-prompt/bots/dialog_bot.py delete mode 100644 samples/05.multi-turn-prompt/config.py delete mode 100644 samples/05.multi-turn-prompt/data_models/__init__.py delete mode 100644 samples/05.multi-turn-prompt/data_models/user_profile.py delete mode 100644 samples/05.multi-turn-prompt/dialogs/__init__.py delete mode 100644 samples/05.multi-turn-prompt/dialogs/user_profile_dialog.py delete mode 100644 samples/05.multi-turn-prompt/helpers/__init__.py delete mode 100644 samples/05.multi-turn-prompt/helpers/dialog_helper.py delete mode 100644 samples/05.multi-turn-prompt/requirements.txt delete mode 100644 samples/06.using-cards/README.md delete mode 100644 samples/06.using-cards/app.py delete mode 100644 samples/06.using-cards/bots/__init__.py delete mode 100644 samples/06.using-cards/bots/dialog_bot.py delete mode 100644 samples/06.using-cards/bots/rich_cards_bot.py delete mode 100644 samples/06.using-cards/config.py delete mode 100644 samples/06.using-cards/dialogs/__init__.py delete mode 100644 samples/06.using-cards/dialogs/main_dialog.py delete mode 100644 samples/06.using-cards/dialogs/resources/__init__.py delete mode 100644 samples/06.using-cards/dialogs/resources/adaptive_card_example.py delete mode 100644 samples/06.using-cards/helpers/__init__.py delete mode 100644 samples/06.using-cards/helpers/activity_helper.py delete mode 100644 samples/06.using-cards/helpers/dialog_helper.py delete mode 100644 samples/06.using-cards/requirements.txt delete mode 100644 samples/08.suggested-actions/README.md delete mode 100644 samples/08.suggested-actions/app.py delete mode 100644 samples/08.suggested-actions/bots/__init__.py delete mode 100644 samples/08.suggested-actions/bots/suggested_actions_bot.py delete mode 100644 samples/08.suggested-actions/config.py delete mode 100644 samples/08.suggested-actions/requirements.txt delete mode 100644 samples/11.qnamaker/README.md delete mode 100644 samples/11.qnamaker/app.py delete mode 100644 samples/11.qnamaker/bots/__init__.py delete mode 100644 samples/11.qnamaker/bots/qna_bot.py delete mode 100644 samples/11.qnamaker/cognitiveModels/smartLightFAQ.tsv delete mode 100644 samples/11.qnamaker/config.py delete mode 100644 samples/11.qnamaker/deploymentTemplates/template-with-preexisting-rg.json delete mode 100644 samples/11.qnamaker/requirements.txt delete mode 100644 samples/13.core-bot/README-LUIS.md delete mode 100644 samples/13.core-bot/README.md delete mode 100644 samples/13.core-bot/adapter_with_error_handler.py delete mode 100644 samples/13.core-bot/app.py delete mode 100644 samples/13.core-bot/booking_details.py delete mode 100644 samples/13.core-bot/bots/__init__.py delete mode 100644 samples/13.core-bot/bots/dialog_and_welcome_bot.py delete mode 100644 samples/13.core-bot/bots/dialog_bot.py delete mode 100644 samples/13.core-bot/cards/welcomeCard.json delete mode 100644 samples/13.core-bot/cognitiveModels/FlightBooking.json delete mode 100644 samples/13.core-bot/config.py delete mode 100644 samples/13.core-bot/dialogs/__init__.py delete mode 100644 samples/13.core-bot/dialogs/booking_dialog.py delete mode 100644 samples/13.core-bot/dialogs/cancel_and_help_dialog.py delete mode 100644 samples/13.core-bot/dialogs/date_resolver_dialog.py delete mode 100644 samples/13.core-bot/dialogs/main_dialog.py delete mode 100644 samples/13.core-bot/flight_booking_recognizer.py delete mode 100644 samples/13.core-bot/helpers/__init__.py delete mode 100644 samples/13.core-bot/helpers/activity_helper.py delete mode 100644 samples/13.core-bot/helpers/dialog_helper.py delete mode 100644 samples/13.core-bot/helpers/luis_helper.py delete mode 100644 samples/13.core-bot/requirements.txt delete mode 100644 samples/15.handling-attachments/README.md delete mode 100644 samples/15.handling-attachments/app.py delete mode 100644 samples/15.handling-attachments/bots/__init__.py delete mode 100644 samples/15.handling-attachments/bots/attachments_bot.py delete mode 100644 samples/15.handling-attachments/config.py delete mode 100644 samples/15.handling-attachments/deploymentTemplates/template-with-preexisting-rg.json delete mode 100644 samples/15.handling-attachments/requirements.txt delete mode 100644 samples/15.handling-attachments/resources/architecture-resize.png delete mode 100644 samples/16.proactive-messages/README.md delete mode 100644 samples/16.proactive-messages/app.py delete mode 100644 samples/16.proactive-messages/bots/__init__.py delete mode 100644 samples/16.proactive-messages/bots/proactive_bot.py delete mode 100644 samples/16.proactive-messages/config.py delete mode 100644 samples/16.proactive-messages/requirements.txt delete mode 100644 samples/17.multilingual-bot/README.md delete mode 100644 samples/17.multilingual-bot/app.py delete mode 100644 samples/17.multilingual-bot/bots/__init__.py delete mode 100644 samples/17.multilingual-bot/bots/multilingual_bot.py delete mode 100644 samples/17.multilingual-bot/cards/welcomeCard.json delete mode 100644 samples/17.multilingual-bot/config.py delete mode 100644 samples/17.multilingual-bot/deploymentTemplates/template-with-preexisting-rg.json delete mode 100644 samples/17.multilingual-bot/requirements.txt delete mode 100644 samples/17.multilingual-bot/translation/__init__.py delete mode 100644 samples/17.multilingual-bot/translation/microsoft_translator.py delete mode 100644 samples/17.multilingual-bot/translation/translation_middleware.py delete mode 100644 samples/17.multilingual-bot/translation/translation_settings.py delete mode 100644 samples/18.bot-authentication/README.md delete mode 100644 samples/18.bot-authentication/app.py delete mode 100644 samples/18.bot-authentication/bots/__init__.py delete mode 100644 samples/18.bot-authentication/bots/auth_bot.py delete mode 100644 samples/18.bot-authentication/bots/dialog_bot.py delete mode 100644 samples/18.bot-authentication/config.py delete mode 100644 samples/18.bot-authentication/deploymentTemplates/template-with-preexisting-rg.json delete mode 100644 samples/18.bot-authentication/dialogs/__init__.py delete mode 100644 samples/18.bot-authentication/dialogs/logout_dialog.py delete mode 100644 samples/18.bot-authentication/dialogs/main_dialog.py delete mode 100644 samples/18.bot-authentication/helpers/__init__.py delete mode 100644 samples/18.bot-authentication/helpers/dialog_helper.py delete mode 100644 samples/18.bot-authentication/requirements.txt delete mode 100644 samples/19.custom-dialogs/README.md delete mode 100644 samples/19.custom-dialogs/app.py delete mode 100644 samples/19.custom-dialogs/bots/__init__.py delete mode 100644 samples/19.custom-dialogs/bots/dialog_bot.py delete mode 100644 samples/19.custom-dialogs/config.py delete mode 100644 samples/19.custom-dialogs/dialogs/__init__.py delete mode 100644 samples/19.custom-dialogs/dialogs/root_dialog.py delete mode 100644 samples/19.custom-dialogs/dialogs/slot_details.py delete mode 100644 samples/19.custom-dialogs/dialogs/slot_filling_dialog.py delete mode 100644 samples/19.custom-dialogs/helpers/__init__.py delete mode 100644 samples/19.custom-dialogs/helpers/dialog_helper.py delete mode 100644 samples/19.custom-dialogs/requirements.txt delete mode 100644 samples/21.corebot-app-insights/NOTICE.md delete mode 100644 samples/21.corebot-app-insights/README-LUIS.md delete mode 100644 samples/21.corebot-app-insights/README.md delete mode 100644 samples/21.corebot-app-insights/app.py delete mode 100644 samples/21.corebot-app-insights/booking_details.py delete mode 100644 samples/21.corebot-app-insights/bots/__init__.py delete mode 100644 samples/21.corebot-app-insights/bots/dialog_and_welcome_bot.py delete mode 100644 samples/21.corebot-app-insights/bots/dialog_bot.py delete mode 100644 samples/21.corebot-app-insights/bots/resources/welcomeCard.json delete mode 100644 samples/21.corebot-app-insights/cognitiveModels/FlightBooking.json delete mode 100644 samples/21.corebot-app-insights/config.py delete mode 100644 samples/21.corebot-app-insights/dialogs/__init__.py delete mode 100644 samples/21.corebot-app-insights/dialogs/booking_dialog.py delete mode 100644 samples/21.corebot-app-insights/dialogs/cancel_and_help_dialog.py delete mode 100644 samples/21.corebot-app-insights/dialogs/date_resolver_dialog.py delete mode 100644 samples/21.corebot-app-insights/dialogs/main_dialog.py delete mode 100644 samples/21.corebot-app-insights/helpers/__init__.py delete mode 100644 samples/21.corebot-app-insights/helpers/activity_helper.py delete mode 100644 samples/21.corebot-app-insights/helpers/dialog_helper.py delete mode 100644 samples/21.corebot-app-insights/helpers/luis_helper.py delete mode 100644 samples/21.corebot-app-insights/requirements.txt delete mode 100644 samples/23.facebook-events/README.md delete mode 100644 samples/23.facebook-events/app.py delete mode 100644 samples/23.facebook-events/bots/__init__.py delete mode 100644 samples/23.facebook-events/bots/facebook_bot.py delete mode 100644 samples/23.facebook-events/config.py delete mode 100644 samples/23.facebook-events/deploymentTemplates/template-with-preexisting-rg.json delete mode 100644 samples/23.facebook-events/requirements.txt delete mode 100644 samples/40.timex-resolution/README.md delete mode 100644 samples/40.timex-resolution/ambiguity.py delete mode 100644 samples/40.timex-resolution/constraints.py delete mode 100644 samples/40.timex-resolution/language_generation.py delete mode 100644 samples/40.timex-resolution/main.py delete mode 100644 samples/40.timex-resolution/parsing.py delete mode 100644 samples/40.timex-resolution/ranges.py delete mode 100644 samples/40.timex-resolution/requirements.txt delete mode 100644 samples/40.timex-resolution/resolution.py delete mode 100644 samples/42.scaleout/README.md delete mode 100644 samples/42.scaleout/app.py delete mode 100644 samples/42.scaleout/bots/__init__.py delete mode 100644 samples/42.scaleout/bots/scaleout_bot.py delete mode 100644 samples/42.scaleout/config.py delete mode 100644 samples/42.scaleout/deploymentTemplates/template-with-preexisting-rg.json delete mode 100644 samples/42.scaleout/dialogs/__init__.py delete mode 100644 samples/42.scaleout/dialogs/root_dialog.py delete mode 100644 samples/42.scaleout/helpers/__init__.py delete mode 100644 samples/42.scaleout/helpers/dialog_helper.py delete mode 100644 samples/42.scaleout/host/__init__.py delete mode 100644 samples/42.scaleout/host/dialog_host.py delete mode 100644 samples/42.scaleout/host/dialog_host_adapter.py delete mode 100644 samples/42.scaleout/requirements.txt delete mode 100644 samples/42.scaleout/store/__init__.py delete mode 100644 samples/42.scaleout/store/blob_store.py delete mode 100644 samples/42.scaleout/store/memory_store.py delete mode 100644 samples/42.scaleout/store/ref_accessor.py delete mode 100644 samples/42.scaleout/store/store.py delete mode 100644 samples/43.complex-dialog/README.md delete mode 100644 samples/43.complex-dialog/app.py delete mode 100644 samples/43.complex-dialog/bots/__init__.py delete mode 100644 samples/43.complex-dialog/bots/dialog_and_welcome_bot.py delete mode 100644 samples/43.complex-dialog/bots/dialog_bot.py delete mode 100644 samples/43.complex-dialog/config.py delete mode 100644 samples/43.complex-dialog/data_models/__init__.py delete mode 100644 samples/43.complex-dialog/data_models/user_profile.py delete mode 100644 samples/43.complex-dialog/dialogs/__init__.py delete mode 100644 samples/43.complex-dialog/dialogs/main_dialog.py delete mode 100644 samples/43.complex-dialog/dialogs/review_selection_dialog.py delete mode 100644 samples/43.complex-dialog/dialogs/top_level_dialog.py delete mode 100644 samples/43.complex-dialog/helpers/__init__.py delete mode 100644 samples/43.complex-dialog/helpers/dialog_helper.py delete mode 100644 samples/43.complex-dialog/requirements.txt delete mode 100644 samples/44.prompt-users-for-input/README.md delete mode 100644 samples/44.prompt-users-for-input/app.py delete mode 100644 samples/44.prompt-users-for-input/bots/__init__.py delete mode 100644 samples/44.prompt-users-for-input/bots/custom_prompt_bot.py delete mode 100644 samples/44.prompt-users-for-input/config.py delete mode 100644 samples/44.prompt-users-for-input/data_models/__init__.py delete mode 100644 samples/44.prompt-users-for-input/data_models/conversation_flow.py delete mode 100644 samples/44.prompt-users-for-input/data_models/user_profile.py delete mode 100644 samples/44.prompt-users-for-input/requirements.txt delete mode 100644 samples/45.state-management/README.md delete mode 100644 samples/45.state-management/app.py delete mode 100644 samples/45.state-management/bots/__init__.py delete mode 100644 samples/45.state-management/bots/state_management_bot.py delete mode 100644 samples/45.state-management/config.py delete mode 100644 samples/45.state-management/data_models/__init__.py delete mode 100644 samples/45.state-management/data_models/conversation_data.py delete mode 100644 samples/45.state-management/data_models/user_profile.py delete mode 100644 samples/45.state-management/requirements.txt delete mode 100644 samples/47.inspection/README.md delete mode 100644 samples/47.inspection/app.py delete mode 100644 samples/47.inspection/bots/__init__.py delete mode 100644 samples/47.inspection/bots/echo_bot.py delete mode 100644 samples/47.inspection/config.py delete mode 100644 samples/47.inspection/data_models/__init__.py delete mode 100644 samples/47.inspection/data_models/custom_state.py delete mode 100644 samples/47.inspection/requirements.txt delete mode 100644 samples/README.md delete mode 100644 samples/python_django/13.core-bot/README-LUIS.md delete mode 100644 samples/python_django/13.core-bot/README.md delete mode 100644 samples/python_django/13.core-bot/booking_details.py delete mode 100644 samples/python_django/13.core-bot/bots/__init__.py delete mode 100644 samples/python_django/13.core-bot/bots/bots.py delete mode 100644 samples/python_django/13.core-bot/bots/dialog_and_welcome_bot.py delete mode 100644 samples/python_django/13.core-bot/bots/dialog_bot.py delete mode 100644 samples/python_django/13.core-bot/bots/resources/welcomeCard.json delete mode 100644 samples/python_django/13.core-bot/bots/settings.py delete mode 100644 samples/python_django/13.core-bot/bots/urls.py delete mode 100644 samples/python_django/13.core-bot/bots/views.py delete mode 100644 samples/python_django/13.core-bot/bots/wsgi.py delete mode 100644 samples/python_django/13.core-bot/cognitiveModels/FlightBooking.json delete mode 100644 samples/python_django/13.core-bot/config.py delete mode 100644 samples/python_django/13.core-bot/db.sqlite3 delete mode 100644 samples/python_django/13.core-bot/dialogs/__init__.py delete mode 100644 samples/python_django/13.core-bot/dialogs/booking_dialog.py delete mode 100644 samples/python_django/13.core-bot/dialogs/cancel_and_help_dialog.py delete mode 100644 samples/python_django/13.core-bot/dialogs/date_resolver_dialog.py delete mode 100644 samples/python_django/13.core-bot/dialogs/main_dialog.py delete mode 100644 samples/python_django/13.core-bot/helpers/__init__.py delete mode 100644 samples/python_django/13.core-bot/helpers/activity_helper.py delete mode 100644 samples/python_django/13.core-bot/helpers/dialog_helper.py delete mode 100644 samples/python_django/13.core-bot/helpers/luis_helper.py delete mode 100644 samples/python_django/13.core-bot/manage.py delete mode 100644 samples/python_django/13.core-bot/requirements.txt diff --git a/generators/LICENSE.md b/generators/LICENSE.md deleted file mode 100644 index 506ab97e5..000000000 --- a/generators/LICENSE.md +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2018 Microsoft Corporation - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/generators/README.md b/generators/README.md deleted file mode 100644 index 761d8ee79..000000000 --- a/generators/README.md +++ /dev/null @@ -1,215 +0,0 @@ -# python-generator-botbuilder - -Cookiecutter generators for [Bot Framework v4](https://dev.botframework.com). Will let you quickly set up a conversational AI bot -using core AI capabilities. - -## About - -`python-generator-botbuilder` will help you build new conversational AI bots using the [Bot Framework v4](https://dev.botframework.com). - -## Templates - -The generator supports three different template options. The table below can help guide which template is right for you. - -| Template | Description | -| ---------- | --------- | -| Echo Bot | A good template if you want a little more than "Hello World!", but not much more. This template handles the very basics of sending messages to a bot, and having the bot process the messages by repeating them back to the user. This template produces a bot that simply "echoes" back to the user anything the user says to the bot. | -| Core Bot | Our most advanced template, the Core template provides 6 core features every bot is likely to have. This template covers the core features of a Conversational-AI bot using [LUIS](https://www.luis.ai). See the **Core Bot Features** table below for more details. | -| Empty Bot | A good template if you are familiar with Bot Framework v4, and simply want a basic skeleton project. Also a good option if you want to take sample code from the documentation and paste it into a minimal bot in order to learn. | - -### How to Choose a Template - -| Template | When This Template is a Good Choice | -| -------- | -------- | -| Echo Bot | You are new to Bot Framework v4 and want a working bot with minimal features. | -| Core Bot | You understand some of the core concepts of Bot Framework v4 and are beyond the concepts introduced in the Echo Bot template. You're familiar with or are ready to learn concepts such as language understanding using LUIS, managing multi-turn conversations with Dialogs, handling user initiated Dialog interruptions, and using Adaptive Cards to welcome your users. | -| Empty Bot | You are a seasoned Bot Framework v4 developer. You've built bots before, and want the minimum skeleton of a bot. | - -### Template Overview - -#### Echo Bot Template - -The Echo Bot template is slightly more than the a classic "Hello World!" example, but not by much. This template shows the basic structure of a bot, how a bot recieves messages from a user, and how a bot sends messages to a user. The bot will "echo" back to the user, what the user says to the bot. It is a good choice for first time, new to Bot Framework v4 developers. - -#### Core Bot Template - -The Core Bot template consists of set of core features most every bot is likely to have. Building off of the core message processing features found in the Echo Bot template, this template adds a number of more sophisticated features. The table below lists these features and provides links to additional documentation. - -| Core Bot Features | Description | -| ------------------ | ----------- | -| [Send and receive messages](https://docs.microsoft.com/azure/bot-service/bot-builder-howto-send-messages?view=azure-bot-service-4.0) | The primary way your bot will communicate with users, and likewise receive communication, is through message activities. Some messages may simply consist of plain text, while others may contain richer content such as cards or attachments. | -| [Proactive messaging](https://docs.microsoft.com/azure/bot-service/bot-builder-howto-proactive-message?view=azure-bot-service-4.0) using [Adaptive Cards](https://docs.microsoft.com/azure/bot-service/bot-builder-send-welcome-message?view=azure-bot-service-4.0?#using-adaptive-card-greeting) | The primary goal when creating any bot is to engage your user in a meaningful conversation. One of the best ways to achieve this goal is to ensure that from the moment a user first connects to your bot, they understand your bot’s main purpose and capabilities. We refer to this as "welcoming the user." The Core template uses an [Adaptive Card](http://adaptivecards.io) to implement this behavior. | -| [Language understanding using LUIS](https://docs.microsoft.com/azure/bot-service/bot-builder-howto-v4-luis?view=azure-bot-service-4.0) | The ability to understand what your user means conversationally and contextually can be a difficult task, but can provide your bot a more natural conversation feel. Language Understanding, called LUIS, enables you to do just that so that your bot can recognize the intent of user messages, allow for more natural language from your user, and better direct the conversation flow. | -| [Multi-turn conversation support using Dialogs](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0) | The ability to manage conversations is an important part of the bot/user interation. Bot Framework introduces the concept of a Dialog to handle this conversational pattern. Dialog objects process inbound Activities and generate outbound responses. The business logic of the bot runs either directly or indirectly within Dialog classes. | -| [Managing conversation state](https://docs.microsoft.com/azure/bot-service/bot-builder-howto-v4-state?view=azure-bot-service-4.0) | A key to good bot design is to track the context of a conversation, so that your bot remembers things like the answers to previous questions. | -| [How to handle user-initiated interruptions](https://docs.microsoft.com/azure/bot-service/bot-builder-howto-handle-user-interrupt?view=azure-bot-service-4.0) | While you may think that your users will follow your defined conversation flow step by step, chances are good that they will change their minds or ask a question in the middle of the process instead of answering the question. Handling interruptions means making sure your bot is prepared to handle situations like this. | -| [How to unit test a bot](https://aka.ms/cs-unit-test-docs) | Optionally, the Core Bot template can generate corresponding unit tests that shows how to use the testing framework introduced in Bot Framework version 4.5. Selecting this option provides a complete set of units tests for Core Bot. It shows how to write unit tests to test the various features of Core Bot. To add the Core Bot unit tests, run the generator and answer `yes` when prompted. See below for an example of how to do this from the command line. | - -#### Empty Bot Template - -The Empty Bot template is the minimal skeleton code for a bot. It provides a stub `on_turn` handler but does not perform any actions. If you are experienced writing bots with Bot Framework v4 and want the minimum scaffolding, the Empty template is for you. - -## Features by Template - -| Feature | Empty Bot | Echo Bot | Core Bot* | -| --------- | :-----: | :-----: | :-----: | -| Generate code in Python | X | X | X | -| Support local development and testing using the [Bot Framework Emulator v4](https://www.github.com/microsoft/botframework-emulator) | X | X | X | -| Core bot message processing | | X | X | -| Deploy your bot to Microsoft Azure | | Pending | Pending | -| Welcome new users using Adaptive Card technology | | | X | -| Support AI-based greetings using [LUIS](https://www.luis.ai) | | | X | -| Use Dialogs to manage more in-depth conversations | | | X | -| Manage conversation state | | | X | -| Handle user interruptions | | | X | -| Unit test a bot using Bot Framework Testing framework (optional) | | | X | - -*Core Bot template is a work in progress landing soon. -## Installation - -1. Install [cookiecutter](https://github.com/cookiecutter/cookiecutter) using [pip](https://pip.pypa.io/en/stable/) (we assume you have pre-installed [python 3](https://www.python.org/downloads/)). - - ```bash - pip install cookiecutter - ``` - -2. Verify that cookiecutter has been installed correctly by typing the following into your console: - - ```bash - cookiecutter --help - ``` - - -## Usage - -### Creating a New Bot Project - -To create an Echo Bot project: - -```bash -cookiecutter https://github.com/microsoft/botbuilder-python/releases/download/Templates/echo.zip -``` - -To create a Core Bot project: - -```bash -# Work in progress -``` - -To create an Empty Bot project: - -```bash -cookiecutter https://github.com/microsoft/botbuilder-python/releases/download/Templates/empty.zip -``` - -When the generator is launched, it will prompt for the information required to create a new bot. - -### Generator Command Line Options and Arguments - -Cookiecutter supports a set of pre-defined command line options, the complete list with descriptions is available [here](https://cookiecutter.readthedocs.io/en/0.9.1/advanced_usage.html#command-line-options). - -Each generator can recieve a series of named arguments to pre-seed the prompt default value. If the `--no-input` option flag is send, these named arguments will be the default values for the template. - -| Named argument | Description | -| ------------------- | ----------- | -| project_name | The name given to the bot project | -| bot_description | A brief bit of text that describes the purpose of the bot | -| add_tests | **PENDING** _A Core Bot Template Only Feature_. The generator will add unit tests to the Core Bot generated bot. This option is not available to other templates at this time. To learn more about the test framework released with Bot Framework v4.5, see [How to unit test bots](https://aka.ms/js-unit-test-docs). This option is intended to enable automated bot generation for testing purposes. | - -#### Example Using Named Arguments - -This example shows how to pass named arguments to the generator, setting the default bot name to test_project. - -```bash -# Run the generator defaulting the bot name to test_project -cookiecutter https://github.com/microsoft/botbuilder-python/releases/download/Templates/echo.zip project_name="test_project" -``` - -### Generating a Bot Using --no-input - -The generator can be run in `--no-input` mode, which can be used for automated bot creation. When run in `--no-input` mode, the generator can be configured using named arguments as documented above. If a named argument is ommitted a reasonable default will be used. - -#### Default Values - -| Named argument | Default Value | -| ------------------- | ----------- | -| bot_name | `my-chat-bot` | -| bot_description | "Demonstrate the core capabilities of the Microsoft Bot Framework" | -| add_tests | `False`| - -#### Examples Using --no-input - -This example shows how to run the generator in --no-input mode, setting all required options on the command line. - -```bash -# Run the generator, setting all command line options -cookiecutter https://github.com/microsoft/botbuilder-python/releases/download/Templates/echo.zip --no-input project_name="test_bot" bot_description="Test description" -``` - -This example shows how to run the generator in --no-input mode, using all the default command line options. The generator will create a bot project using all the default values specified in the **Default Options** table above. - -```bash -# Run the generator using all default options -cookiecutter https://github.com/microsoft/botbuilder-python/releases/download/Templates/echo.zip --no-input -``` - -This example shows how to run the generator in --no-input mode, with unit tests. - -```bash -# PENDING: Run the generator using all default options -``` - -## Running Your Bot - -### Running Your Bot Locally - -To run your bot locally, type the following in your console: - -```bash -# install dependencies -pip install -r requirements.txt -``` - -```bash -# run the bot -python app.py -``` - -Alternatively to the last command, you can set the file in an environment variable with `set FLASK_APP=app.py` in windows (`export FLASK_APP=app.py` in mac/linux) and then run `flask run --host=127.0.0.1 --port=3978` - -### Interacting With Your Bot Using the Emulator - -- Launch Bot Framework Emulator -- File -> Open Bot -- Enter a Bot URL of `http://localhost:3978/api/messages` - -Once the Emulator is connected, you can interact with and receive messages from your bot. - -#### Lint Compliant Code - -The code generated by the botbuilder generator is pylint compliant to our ruleset. To use pylint as your develop your bot: - -```bash -# Assuming you created a project with the bot_name value 'my_chat_bot' -pylint --rcfile=my_chat_bot/.pylintrc my_chat_bot -``` - -#### Testing Core Bots with Tests (Pending) - -Core Bot templates generated with unit tests can be tested using the following: - -```bash -# launch pytest -pytest -``` - -## Deploy Your Bot to Azure (PENDING) - -After creating the bot and testing it locally, you can deploy it to Azure to make it accessible from anywhere. -To learn how, see [Deploy your bot to Azure](https://aka.ms/azuredeployment) for a complete set of deployment instructions. - -If you are new to Microsoft Azure, please refer to [Getting started with Azure](https://azure.microsoft.com/get-started/) for guidance on how to get started on Azure. - -## Logging Issues and Providing Feedback - -Issues and feedback about the botbuilder generator can be submitted through the project's [GitHub Issues](https://github.com/Microsoft/botbuilder-samples/issues) page. diff --git a/generators/app/templates/core/cookiecutter.json b/generators/app/templates/core/cookiecutter.json deleted file mode 100644 index 4a14b6ade..000000000 --- a/generators/app/templates/core/cookiecutter.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "bot_name": "my_chat_bot", - "bot_description": "Demonstrate the core capabilities of the Microsoft Bot Framework" -} \ No newline at end of file diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/.pylintrc b/generators/app/templates/core/{{cookiecutter.bot_name}}/.pylintrc deleted file mode 100644 index 9c1c70f04..000000000 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/.pylintrc +++ /dev/null @@ -1,498 +0,0 @@ -[MASTER] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. -extension-pkg-whitelist= - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore= - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use. -jobs=1 - -# Control the amount of potential inferred values when inferring a single -# object. This can help the performance when dealing with large functions or -# complex, nested conditions. -limit-inference-results=100 - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# Specify a configuration file. -#rcfile= - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once). You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". -disable=missing-docstring, - too-few-public-methods, - bad-continuation, - no-self-use, - duplicate-code, - broad-except, - no-name-in-module - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member - - -[REPORTS] - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages. -reports=no - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=sys.exit - - -[LOGGING] - -# Format style used to check logging format string. `old` means using % -# formatting, while `new` is for `{}` formatting. -logging-format-style=old - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package.. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# Tells whether to warn about missing members when the owner of the attribute -# is inferred to be None. -ignore-none=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid defining new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expected to -# not be used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore. -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=120 - -# Maximum number of lines in a module. -max-module-lines=1000 - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[BASIC] - -# Naming style matching correct argument names. -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style. -#argument-rgx= - -# Naming style matching correct attribute names. -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style. -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma. -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Naming style matching correct class attribute names. -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. -#class-attribute-rgx= - -# Naming style matching correct class names. -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming- -# style. -#class-rgx= - -# Naming style matching correct constant names. -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style. -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names. -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style. -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma. -good-names=i, - j, - k, - ex, - Run, - _ - -# Include a hint for the correct naming format with invalid-name. -include-naming-hint=no - -# Naming style matching correct inline iteration names. -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style. -#inlinevar-rgx= - -# Naming style matching correct method names. -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style. -#method-rgx= - -# Naming style matching correct module names. -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style. -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -# These decorators are taken in consideration only for invalid-name. -property-classes=abc.abstractproperty - -# Naming style matching correct variable names. -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style. -#variable-rgx= - - -[STRING] - -# This flag controls whether the implicit-str-concat-in-sequence should -# generate a warning on implicit string concatenation in sequences defined over -# several lines. -check-str-concat-over-line-jumps=no - - -[IMPORTS] - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules=optparse,tkinter.tix - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled). -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled). -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled). -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=cls - - -[DESIGN] - -# Maximum number of arguments for function / method. -max-args=5 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Maximum number of boolean expressions in an if statement. -max-bool-expr=5 - -# Maximum number of branch for function / method body. -max-branches=12 - -# Maximum number of locals for function / method body. -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body. -max-returns=6 - -# Maximum number of statements in function / method body. -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/README-LUIS.md b/generators/app/templates/core/{{cookiecutter.bot_name}}/README-LUIS.md deleted file mode 100644 index b6b9b925f..000000000 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/README-LUIS.md +++ /dev/null @@ -1,216 +0,0 @@ -# Setting up LUIS via CLI: - -This README contains information on how to create and deploy a LUIS application. When the bot is ready to be deployed to production, we recommend creating a LUIS Endpoint Resource for usage with your LUIS App. - -> _For instructions on how to create a LUIS Application via the LUIS portal, see these Quickstart steps:_ -> 1. _[Quickstart: Create a new app in the LUIS portal][Quickstart-create]_ -> 2. _[Quickstart: Deploy an app in the LUIS portal][Quickstart-deploy]_ - - [Quickstart-create]: https://docs.microsoft.com/azure/cognitive-services/luis/get-started-portal-build-app - [Quickstart-deploy]:https://docs.microsoft.com/azure/cognitive-services/luis/get-started-portal-deploy-app - -## Table of Contents: - -- [Prerequisites](#Prerequisites) -- [Import a new LUIS Application using a local LUIS application](#Import-a-new-LUIS-Application-using-a-local-LUIS-application) -- [How to create a LUIS Endpoint resource in Azure and pair it with a LUIS Application](#How-to-create-a-LUIS-Endpoint-resource-in-Azure-and-pair-it-with-a-LUIS-Application) - -___ - -## [Prerequisites](#Table-of-Contents): - -#### Install Azure CLI >=2.0.61: - -Visit the following page to find the correct installer for your OS: -- https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest - -#### Install LUIS CLI >=2.4.0: - -Open a CLI of your choice and type the following: - -```bash -npm i -g luis-apis@^2.4.0 -``` - -#### LUIS portal account: - -You should already have a LUIS account with either https://luis.ai, https://eu.luis.ai, or https://au.luis.ai. To determine where to create a LUIS account, consider where you will deploy your LUIS applications, and then place them in [the corresponding region][LUIS-Authoring-Regions]. - -After you've created your account, you need your [Authoring Key][LUIS-AKey] and a LUIS application ID. - - [LUIS-Authoring-Regions]: https://docs.microsoft.com/azure/cognitive-services/luis/luis-reference-regions#luis-authoring-regions] - [LUIS-AKey]: https://docs.microsoft.com/azure/cognitive-services/luis/luis-concept-keys#authoring-key - -___ - -## [Import a new LUIS Application using a local LUIS application](#Table-of-Contents) - -### 1. Import the local LUIS application to luis.ai - -```bash -luis import application --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appName "FlightBooking" --in "./cognitiveModels/FlightBooking.json" -``` - -Outputs the following JSON: - -```json -{ - "id": "########-####-####-####-############", - "name": "FlightBooking", - "description": "A LUIS model that uses intent and entities.", - "culture": "en-us", - "usageScenario": "", - "domain": "", - "versionsCount": 1, - "createdDateTime": "2019-03-29T18:32:02Z", - "endpoints": {}, - "endpointHitsCount": 0, - "activeVersion": "0.1", - "ownerEmail": "bot@contoso.com", - "tokenizerVersion": "1.0.0" -} -``` - -For the next step, you'll need the `"id"` value for `--appId` and the `"activeVersion"` value for `--versionId`. - -### 2. Train the LUIS Application - -```bash -luis train version --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appId "LuisAppId" --versionId "LuisAppversion" --wait -``` - -### 3. Publish the LUIS Application - -```bash -luis publish version --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appId "LuisAppId" --versionId "LuisAppversion" --publishRegion "LuisAppPublishRegion" -``` - -> `--region` corresponds to the region you _author_ your application in. The regions available for this are "westus", "westeurope" and "australiaeast".
-> These regions correspond to the three available portals, https://luis.ai, https://eu.luis.ai, or https://au.luis.ai.
-> `--publishRegion` corresponds to the region of the endpoint you're publishing to, (e.g. "westus", "southeastasia", "westeurope", "brazilsouth").
-> See the [reference docs][Endpoint-API] for a list of available publish/endpoint regions. - - [Endpoint-API]: https://westus.dev.cognitive.microsoft.com/docs/services/5819c76f40a6350ce09de1ac/operations/5819c77140a63516d81aee78 - -Outputs the following: - -```json - { - "versionId": "0.1", - "isStaging": false, - "endpointUrl": "https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/########-####-####-####-############", - "region": "westus", - "assignedEndpointKey": null, - "endpointRegion": "westus", - "failedRegions": "", - "publishedDateTime": "2019-03-29T18:40:32Z", - "directVersionPublish": false -} -``` - -To see how to create an LUIS Cognitive Service Resource in Azure, please see [the next README][README-LUIS]. This Resource should be used when you want to move your bot to production. The instructions will show you how to create and pair the resource with a LUIS Application. - - [README-LUIS]: ./README-LUIS.md - -___ - -## [How to create a LUIS Endpoint resource in Azure and pair it with a LUIS Application](#Table-of-Contents) - -### 1. Create a new LUIS Cognitive Services resource on Azure via Azure CLI - -> _Note:_
-> _If you don't have a Resource Group in your Azure subscription, you can create one through the Azure portal or through using:_ -> ```bash -> az group create --subscription "AzureSubscriptionGuid" --location "westus" --name "ResourceGroupName" -> ``` -> _To see a list of valid locations, use `az account list-locations`_ - - -```bash -# Use Azure CLI to create the LUIS Key resource on Azure -az cognitiveservices account create --kind "luis" --name "NewLuisResourceName" --sku "S0" --location "westus" --subscription "AzureSubscriptionGuid" -g "ResourceGroupName" -``` - -The command will output a response similar to the JSON below: - -```json -{ - "endpoint": "https://westus.api.cognitive.microsoft.com/luis/v2.0", - "etag": "\"########-####-####-####-############\"", - "id": "/subscriptions/########-####-####-####-############/resourceGroups/ResourceGroupName/providers/Microsoft.CognitiveServices/accounts/NewLuisResourceName", - "internalId": "################################", - "kind": "luis", - "location": "westus", - "name": "NewLuisResourceName", - "provisioningState": "Succeeded", - "resourceGroup": "ResourceGroupName", - "sku": { - "name": "S0", - "tier": null - }, - "tags": null, - "type": "Microsoft.CognitiveServices/accounts" -} -``` - - - -Take the output from the previous command and create a JSON file in the following format: - -```json -{ - "azureSubscriptionId": "00000000-0000-0000-0000-000000000000", - "resourceGroup": "ResourceGroupName", - "accountName": "NewLuisResourceName" -} -``` - -### 2. Retrieve ARM access token via Azure CLI - -```bash -az account get-access-token --subscription "AzureSubscriptionGuid" -``` - -This will return an object that looks like this: - -```json -{ - "accessToken": "eyJ0eXAiOiJKVtokentokentokentokentokeng1dCI6Ik4tbEMwbi05REFMcXdodUhZbkhRNjNHZUNYYyIsItokenI6Ik4tbEMwbi05REFMcXdodUhZbkhRNjNHZUNYYyJ9.eyJhdWQiOiJodHRwczovL21hbmFnZW1lbnQuY29yZS53aW5kb3dzLm5ldC8iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNTUzODc3MTUwLCJuYmYiOjE1NTM4NzcxNTAsImV4cCI6MTU1Mzg4MTA1MCwiX2NsYWltX25hbWVzIjp7Imdyb3VwcyI6InNyYzEifSwiX2NsYWltX3NvdXJjZXMiOnsic3JjMSI6eyJlbmRwb2ludCI6Imh0dHBzOi8vZ3JhcGgud2luZG93cy5uZXQvNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3L3VzZXJzL2ZmZTQyM2RkLWJhM2YtNDg0Ny04NjgyLWExNTI5MDA4MjM4Ny9nZXRNZW1iZXJPYmplY3RzIn19LCJhY3IiOiIxIiwiYWlvIjoiQVZRQXEvOEtBQUFBeGVUc201NDlhVHg4RE1mMFlRVnhGZmxxOE9RSC9PODR3QktuSmRqV1FqTkkwbmxLYzB0bHJEZzMyMFZ5bWZGaVVBSFBvNUFFUTNHL0FZNDRjdk01T3M0SEt0OVJkcE5JZW9WU0dzd0kvSkk9IiwiYW1yIjpbIndpYSIsIm1mYSJdLCJhcHBpZCI6IjA0YjA3Nzk1LThkZGItNDYxYS1iYmVlLTAyZjllMWJmN2I0NiIsImFwcGlkYWNyIjoiMCIsImRldmljZWlkIjoiNDhmNDVjNjEtMTg3Zi00MjUxLTlmZWItMTllZGFkZmMwMmE3IiwiZmFtaWx5X25hbWUiOiJHdW0iLCJnaXZlbl9uYW1lIjoiU3RldmVuIiwiaXBhZGRyIjoiMTY3LjIyMC4yLjU1IiwibmFtZSI6IlN0ZXZlbiBHdW0iLCJvaWQiOiJmZmU0MjNkZC1iYTNmLTQ4NDctODY4Mi1hMTUyOTAwODIzODciLCJvbnByZW1fc2lkIjoiUy0xLTUtMjEtMjEyNzUyMTE4NC0xNjA0MDEyOTIwLTE4ODc5Mjc1MjctMjYwOTgyODUiLCJwdWlkIjoiMTAwMzdGRkVBMDQ4NjlBNyIsInJoIjoiSSIsInNjcCI6InVzZXJfaW1wZXJzb25hdGlvbiIsInN1YiI6Ik1rMGRNMWszN0U5ckJyMjhieUhZYjZLSU85LXVFQVVkZFVhNWpkSUd1Nk0iLCJ0aWQiOiI3MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDciLCJ1bmlxdWVfbmFtZSI6InN0Z3VtQG1pY3Jvc29mdC5jb20iLCJ1cG4iOiJzdGd1bUBtaWNyb3NvZnQuY29tIiwidXRpIjoiT2w2NGN0TXY4RVNEQzZZQWRqRUFtokenInZlciI6IjEuMCJ9.kFAsEilE0mlS1pcpqxf4rEnRKeYsehyk-gz-zJHUrE__oad3QjgDSBDPrR_ikLdweynxbj86pgG4QFaHURNCeE6SzrbaIrNKw-n9jrEtokenlosOxg_0l2g1LeEUOi5Q4gQREAU_zvSbl-RY6sAadpOgNHtGvz3Rc6FZRITfkckSLmsKAOFoh-aWC6tFKG8P52rtB0qVVRz9tovBeNqkMYL49s9ypduygbXNVwSQhm5JszeWDgrFuVFHBUP_iENCQYGQpEZf_KvjmX1Ur1F9Eh9nb4yI2gFlKncKNsQl-tokenK7-tokentokentokentokentokentokenatoken", - "expiresOn": "2200-12-31 23:59:59.999999", - "subscription": "AzureSubscriptionGuid", - "tenant": "tenant-guid", - "tokenType": "Bearer" -} -``` - -The value needed for the next step is the `"accessToken"`. - -### 3. Use `luis add appazureaccount` to pair your LUIS resource with a LUIS Application - -```bash -luis add appazureaccount --in "path/to/created/requestBody.json" --appId "LuisAppId" --authoringKey "LuisAuthoringKey" --armToken "accessToken" -``` - -If successful, it should yield a response like this: - -```json -{ - "code": "Success", - "message": "Operation Successful" -} -``` - -### 4. See the LUIS Cognitive Services' keys - -```bash -az cognitiveservices account keys list --name "NewLuisResourceName" --subscription "AzureSubscriptionGuid" -g "ResourceGroupName" -``` - -This will return an object that looks like this: - -```json -{ - "key1": "9a69####dc8f####8eb4####399f####", - "key2": "####f99e####4b1a####fb3b####6b9f" -} -``` \ No newline at end of file diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/README.md b/generators/app/templates/core/{{cookiecutter.bot_name}}/README.md deleted file mode 100644 index 35a5eb2f1..000000000 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# CoreBot - -Bot Framework v4 core bot sample. - -This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to: - -- Use [LUIS](https://www.luis.ai) to implement core AI capabilities -- Implement a multi-turn conversation using Dialogs -- Handle user interruptions for such things as `Help` or `Cancel` -- Prompt for and validate requests for information from the user - -## Prerequisites - -This sample **requires** prerequisites in order to run. - -### Overview - -This bot uses [LUIS](https://www.luis.ai), an AI based cognitive service, to implement language understanding. - -### Install Python 3.6 - - -### Create a LUIS Application to enable language understanding - -LUIS language model setup, training, and application configuration steps can be found [here](https://docs.microsoft.com/azure/bot-service/bot-builder-howto-v4-luis?view=azure-bot-service-4.0&tabs=cs). - -If you wish to create a LUIS application via the CLI, these steps can be found in the [README-LUIS.md](README-LUIS.md). - -## Running the sample -- Run `pip install -r requirements.txt` to install all dependencies -- Update LuisAppId, LuisAPIKey and LuisAPIHostName in `config.py` with the information retrieved from the [LUIS portal](https://www.luis.ai) -- Run `python app.py` -- Alternatively to the last command, you can set the file in an environment variable with `set FLASK_APP=app.py` in windows (`export FLASK_APP=app.py` in mac/linux) and then run `flask run --host=127.0.0.1 --port=3978` - - -## Testing the bot using Bot Framework Emulator - -[Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework Emulator version 4.3.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to the bot using Bot Framework Emulator - -- Launch Bot Framework Emulator -- Enter a Bot URL of `http://localhost:3978/api/messages` - - -## Further reading - -- [Bot Framework Documentation](https://docs.botframework.com) -- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) -- [Dialogs](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0) -- [Gathering Input Using Prompts](https://docs.microsoft.com/azure/bot-service/bot-builder-prompts?view=azure-bot-service-4.0&tabs=csharp) -- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) -- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) -- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) -- [.NET Core CLI tools](https://docs.microsoft.com/dotnet/core/tools/?tabs=netcore2x) -- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest) -- [Azure Portal](https://portal.azure.com) -- [Language Understanding using LUIS](https://docs.microsoft.com/azure/cognitive-services/luis/) -- [Channels and Bot Connector Service](https://docs.microsoft.com/azure/bot-service/bot-concepts?view=azure-bot-service-4.0) \ No newline at end of file diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/__init__.py b/generators/app/templates/core/{{cookiecutter.bot_name}}/__init__.py deleted file mode 100644 index 5b7f7a925..000000000 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/app.py b/generators/app/templates/core/{{cookiecutter.bot_name}}/app.py deleted file mode 100644 index d08cff888..000000000 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/app.py +++ /dev/null @@ -1,110 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -# pylint: disable=import-error - -""" -This sample shows how to create a bot that demonstrates the following: -- Use [LUIS](https://www.luis.ai) to implement core AI capabilities. -- Implement a multi-turn conversation using Dialogs. -- Handle user interruptions for such things as `Help` or `Cancel`. -- Prompt for and validate requests for information from the user. -""" - -import asyncio -import sys -from datetime import datetime -from types import MethodType - -from flask import Flask, request, Response -from botbuilder.core import ( - BotFrameworkAdapter, - BotFrameworkAdapterSettings, - ConversationState, - MemoryStorage, - UserState, - TurnContext -) -from botbuilder.schema import Activity, ActivityTypes - -from bots import DialogAndWelcomeBot -from dialogs import MainDialog, BookingDialog -from flight_booking_recognizer import FlightBookingRecognizer - -LOOP = asyncio.get_event_loop() -APP = Flask(__name__, instance_relative_config=True) -APP.config.from_object("config.DefaultConfig") - -SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"]) -ADAPTER = BotFrameworkAdapter(SETTINGS) - -# Create MemoryStorage, UserState and ConversationState -MEMORY = MemoryStorage() -USER_STATE = UserState(MEMORY) -CONVERSATION_STATE = ConversationState(MEMORY) -RECOGNIZER = FlightBookingRecognizer(APP.config) -BOOKING_DIALOG = BookingDialog() - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"]) -ADAPTER = BotFrameworkAdapter(SETTINGS) - -# Catch-all for errors. -# pylint: disable=unused-argument -async def on_error(self, context: TurnContext, error: Exception): - # This check writes out errors to console log .vs. app insights. - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - - # Send a message to the user - await context.send_activity("The bot encounted an error or bug.") - await context.send_activity("To continue to run this bot, please fix the bot source code.") - # Send a trace activity if we're talking to the Bot Framework Emulator - if context.activity.channel_id == 'emulator': - # Create a trace activity that contains the error object - trace_activity = Activity( - label="TurnError", - name="on_turn_error Trace", - timestamp=datetime.utcnow(), - type=ActivityTypes.trace, - value=f"{error}", - value_type="https://www.botframework.com/schemas/error" - ) - # Send a trace activity, which will be displayed in Bot Framework Emulator - await context.send_activity(trace_activity) - -ADAPTER.on_turn_error = MethodType(on_error, ADAPTER) - -DIALOG = MainDialog(RECOGNIZER, BOOKING_DIALOG) -BOT = DialogAndWelcomeBot(CONVERSATION_STATE, USER_STATE, DIALOG) - - -@APP.route("/api/messages", methods=["POST"]) -def messages(): - """Main bot message handler.""" - if "application/json" in request.headers["Content-Type"]: - body = request.json - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = ( - request.headers["Authorization"] if "Authorization" in request.headers else "" - ) - - try: - task = LOOP.create_task( - ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - ) - LOOP.run_until_complete(task) - return Response(status=201) - except Exception as exception: - raise exception - - -if __name__ == "__main__": - try: - APP.run(debug=False, port=APP.config["PORT"]) # nosec debug - except Exception as exception: - raise exception diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/booking_details.py b/generators/app/templates/core/{{cookiecutter.bot_name}}/booking_details.py deleted file mode 100644 index ca0710ff0..000000000 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/booking_details.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from typing import List - - -class BookingDetails: - def __init__( - self, - destination: str = None, - origin: str = None, - travel_date: str = None, - unsupported_airports: List[str] = None, - ): - self.destination = destination - self.origin = origin - self.travel_date = travel_date - self.unsupported_airports = unsupported_airports or [] diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/bots/__init__.py b/generators/app/templates/core/{{cookiecutter.bot_name}}/bots/__init__.py deleted file mode 100644 index 6925db302..000000000 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/bots/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .dialog_bot import DialogBot -from .dialog_and_welcome_bot import DialogAndWelcomeBot - -__all__ = ["DialogBot", "DialogAndWelcomeBot"] diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/bots/dialog_and_welcome_bot.py b/generators/app/templates/core/{{cookiecutter.bot_name}}/bots/dialog_and_welcome_bot.py deleted file mode 100644 index 17bb2db80..000000000 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/bots/dialog_and_welcome_bot.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import json -import os.path - -from typing import List -from botbuilder.core import MessageFactory, TurnContext -from botbuilder.schema import Attachment, ChannelAccount - -from helpers import DialogHelper -from .dialog_bot import DialogBot - - -class DialogAndWelcomeBot(DialogBot): - - async def on_members_added_activity( - self, members_added: List[ChannelAccount], turn_context: TurnContext - ): - for member in members_added: - # Greet anyone that was not the target (recipient) of this message. - # To learn more about Adaptive Cards, see https://aka.ms/msbot-adaptivecards for more details. - if member.id != turn_context.activity.recipient.id: - welcome_card = self.create_adaptive_card_attachment() - response = MessageFactory.attachment(welcome_card) - await turn_context.send_activity(response) - await DialogHelper.run_dialog( - self.dialog, - turn_context, - self.conversation_state.create_property("DialogState"), - ) - - # Load attachment from file. - def create_adaptive_card_attachment(self): - relative_path = os.path.abspath(os.path.dirname(__file__)) - path = os.path.join(relative_path, "../cards/welcomeCard.json") - with open(path) as card_file: - card = json.load(card_file) - - return Attachment( - content_type="application/vnd.microsoft.card.adaptive", content=card - ) diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/bots/dialog_bot.py b/generators/app/templates/core/{{cookiecutter.bot_name}}/bots/dialog_bot.py deleted file mode 100644 index 5f2c148aa..000000000 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/bots/dialog_bot.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import ActivityHandler, ConversationState, UserState, TurnContext -from botbuilder.dialogs import Dialog - -from helpers import DialogHelper - - -class DialogBot(ActivityHandler): - def __init__( - self, - conversation_state: ConversationState, - user_state: UserState, - dialog: Dialog, - ): - if conversation_state is None: - raise Exception( - "[DialogBot]: Missing parameter. conversation_state is required" - ) - if user_state is None: - raise Exception("[DialogBot]: Missing parameter. user_state is required") - if dialog is None: - raise Exception("[DialogBot]: Missing parameter. dialog is required") - - self.conversation_state = conversation_state - self.user_state = user_state - self.dialog = dialog - - async def on_turn(self, turn_context: TurnContext): - await super().on_turn(turn_context) - - # Save any state changes that might have occurred during the turn. - await self.conversation_state.save_changes(turn_context, False) - await self.user_state.save_changes(turn_context, False) - - async def on_message_activity(self, turn_context: TurnContext): - await DialogHelper.run_dialog( - self.dialog, - turn_context, - self.conversation_state.create_property("DialogState"), - ) diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/cards/welcomeCard.json b/generators/app/templates/core/{{cookiecutter.bot_name}}/cards/welcomeCard.json deleted file mode 100644 index cc10cda9f..000000000 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/cards/welcomeCard.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", - "type": "AdaptiveCard", - "version": "1.0", - "body": [ - { - "type": "Image", - "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU", - "size": "stretch" - }, - { - "type": "TextBlock", - "spacing": "medium", - "size": "default", - "weight": "bolder", - "text": "Welcome to Bot Framework!", - "wrap": true, - "maxLines": 0 - }, - { - "type": "TextBlock", - "size": "default", - "isSubtle": "true", - "text": "Now that you have successfully run your bot, follow the links in this Adaptive Card to expand your knowledge of Bot Framework.", - "wrap": true, - "maxLines": 0 - } - ], - "actions": [ - { - "type": "Action.OpenUrl", - "title": "Get an overview", - "url": "https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0" - }, - { - "type": "Action.OpenUrl", - "title": "Ask a question", - "url": "https://stackoverflow.com/questions/tagged/botframework" - }, - { - "type": "Action.OpenUrl", - "title": "Learn how to deploy", - "url": "https://docs.microsoft.com/azure/bot-service/bot-builder-howto-deploy-azure?view=azure-bot-service-4.0" - } - ] -} \ No newline at end of file diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/cognitiveModels/FlightBooking.json b/generators/app/templates/core/{{cookiecutter.bot_name}}/cognitiveModels/FlightBooking.json deleted file mode 100644 index f0e4b9770..000000000 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/cognitiveModels/FlightBooking.json +++ /dev/null @@ -1,339 +0,0 @@ -{ - "luis_schema_version": "3.2.0", - "versionId": "0.1", - "name": "FlightBooking", - "desc": "Luis Model for CoreBot", - "culture": "en-us", - "tokenizerVersion": "1.0.0", - "intents": [ - { - "name": "BookFlight" - }, - { - "name": "Cancel" - }, - { - "name": "GetWeather" - }, - { - "name": "None" - } - ], - "entities": [], - "composites": [ - { - "name": "From", - "children": [ - "Airport" - ], - "roles": [] - }, - { - "name": "To", - "children": [ - "Airport" - ], - "roles": [] - } - ], - "closedLists": [ - { - "name": "Airport", - "subLists": [ - { - "canonicalForm": "Paris", - "list": [ - "paris", - "cdg" - ] - }, - { - "canonicalForm": "London", - "list": [ - "london", - "lhr" - ] - }, - { - "canonicalForm": "Berlin", - "list": [ - "berlin", - "txl" - ] - }, - { - "canonicalForm": "New York", - "list": [ - "new york", - "jfk" - ] - }, - { - "canonicalForm": "Seattle", - "list": [ - "seattle", - "sea" - ] - } - ], - "roles": [] - } - ], - "patternAnyEntities": [], - "regex_entities": [], - "prebuiltEntities": [ - { - "name": "datetimeV2", - "roles": [] - } - ], - "model_features": [], - "regex_features": [], - "patterns": [], - "utterances": [ - { - "text": "book a flight", - "intent": "BookFlight", - "entities": [] - }, - { - "text": "book a flight from new york", - "intent": "BookFlight", - "entities": [ - { - "entity": "From", - "startPos": 19, - "endPos": 26 - } - ] - }, - { - "text": "book a flight from seattle", - "intent": "BookFlight", - "entities": [ - { - "entity": "From", - "startPos": 19, - "endPos": 25 - } - ] - }, - { - "text": "book a hotel in new york", - "intent": "None", - "entities": [] - }, - { - "text": "book a restaurant", - "intent": "None", - "entities": [] - }, - { - "text": "book flight from london to paris on feb 14th", - "intent": "BookFlight", - "entities": [ - { - "entity": "From", - "startPos": 17, - "endPos": 22 - }, - { - "entity": "To", - "startPos": 27, - "endPos": 31 - } - ] - }, - { - "text": "book flight to berlin on feb 14th", - "intent": "BookFlight", - "entities": [ - { - "entity": "To", - "startPos": 15, - "endPos": 20 - } - ] - }, - { - "text": "book me a flight from london to paris", - "intent": "BookFlight", - "entities": [ - { - "entity": "From", - "startPos": 22, - "endPos": 27 - }, - { - "entity": "To", - "startPos": 32, - "endPos": 36 - } - ] - }, - { - "text": "bye", - "intent": "Cancel", - "entities": [] - }, - { - "text": "cancel booking", - "intent": "Cancel", - "entities": [] - }, - { - "text": "exit", - "intent": "Cancel", - "entities": [] - }, - { - "text": "find an airport near me", - "intent": "None", - "entities": [] - }, - { - "text": "flight to paris", - "intent": "BookFlight", - "entities": [ - { - "entity": "To", - "startPos": 10, - "endPos": 14 - } - ] - }, - { - "text": "flight to paris from london on feb 14th", - "intent": "BookFlight", - "entities": [ - { - "entity": "To", - "startPos": 10, - "endPos": 14 - }, - { - "entity": "From", - "startPos": 21, - "endPos": 26 - } - ] - }, - { - "text": "fly from berlin to paris on may 5th", - "intent": "BookFlight", - "entities": [ - { - "entity": "From", - "startPos": 9, - "endPos": 14 - }, - { - "entity": "To", - "startPos": 19, - "endPos": 23 - } - ] - }, - { - "text": "go to paris", - "intent": "BookFlight", - "entities": [ - { - "entity": "To", - "startPos": 6, - "endPos": 10 - } - ] - }, - { - "text": "going from paris to berlin", - "intent": "BookFlight", - "entities": [ - { - "entity": "From", - "startPos": 11, - "endPos": 15 - }, - { - "entity": "To", - "startPos": 20, - "endPos": 25 - } - ] - }, - { - "text": "i'd like to rent a car", - "intent": "None", - "entities": [] - }, - { - "text": "ignore", - "intent": "Cancel", - "entities": [] - }, - { - "text": "travel from new york to paris", - "intent": "BookFlight", - "entities": [ - { - "entity": "From", - "startPos": 12, - "endPos": 19 - }, - { - "entity": "To", - "startPos": 24, - "endPos": 28 - } - ] - }, - { - "text": "travel to new york", - "intent": "BookFlight", - "entities": [ - { - "entity": "To", - "startPos": 10, - "endPos": 17 - } - ] - }, - { - "text": "travel to paris", - "intent": "BookFlight", - "entities": [ - { - "entity": "To", - "startPos": 10, - "endPos": 14 - } - ] - }, - { - "text": "what's the forecast for this friday?", - "intent": "GetWeather", - "entities": [] - }, - { - "text": "what's the weather like for tomorrow", - "intent": "GetWeather", - "entities": [] - }, - { - "text": "what's the weather like in new york", - "intent": "GetWeather", - "entities": [] - }, - { - "text": "what's the weather like?", - "intent": "GetWeather", - "entities": [] - }, - { - "text": "winter is coming", - "intent": "None", - "entities": [] - } - ], - "settings": [] -} \ No newline at end of file diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/config.py b/generators/app/templates/core/{{cookiecutter.bot_name}}/config.py deleted file mode 100644 index 8df9f92c8..000000000 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/config.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - -class DefaultConfig: - """ Bot Configuration """ - - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") - LUIS_APP_ID = os.environ.get("LuisAppId", "") - LUIS_API_KEY = os.environ.get("LuisAPIKey", "") - # LUIS endpoint host name, ie "westus.api.cognitive.microsoft.com" - LUIS_API_HOST_NAME = os.environ.get("LuisAPIHostName", "") diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/dialogs/__init__.py b/generators/app/templates/core/{{cookiecutter.bot_name}}/dialogs/__init__.py deleted file mode 100644 index 567539f96..000000000 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/dialogs/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .booking_dialog import BookingDialog -from .cancel_and_help_dialog import CancelAndHelpDialog -from .date_resolver_dialog import DateResolverDialog -from .main_dialog import MainDialog - -__all__ = ["BookingDialog", "CancelAndHelpDialog", "DateResolverDialog", "MainDialog"] diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/dialogs/booking_dialog.py b/generators/app/templates/core/{{cookiecutter.bot_name}}/dialogs/booking_dialog.py deleted file mode 100644 index c5912075d..000000000 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/dialogs/booking_dialog.py +++ /dev/null @@ -1,137 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.dialogs import WaterfallDialog, WaterfallStepContext, DialogTurnResult -from botbuilder.dialogs.prompts import ConfirmPrompt, TextPrompt, PromptOptions -from botbuilder.core import MessageFactory -from botbuilder.schema import InputHints - -from datatypes_date_time.timex import Timex - -from .cancel_and_help_dialog import CancelAndHelpDialog -from .date_resolver_dialog import DateResolverDialog - - -class BookingDialog(CancelAndHelpDialog): - def __init__(self, dialog_id: str = None): - super(BookingDialog, self).__init__(dialog_id or BookingDialog.__name__) - - self.add_dialog(TextPrompt(TextPrompt.__name__)) - self.add_dialog(ConfirmPrompt(ConfirmPrompt.__name__)) - self.add_dialog(DateResolverDialog(DateResolverDialog.__name__)) - self.add_dialog( - WaterfallDialog( - WaterfallDialog.__name__, - [ - self.destination_step, - self.origin_step, - self.travel_date_step, - self.confirm_step, - self.final_step, - ], - ) - ) - - self.initial_dialog_id = WaterfallDialog.__name__ - - async def destination_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - """ - If a destination city has not been provided, prompt for one. - :param step_context: - :return DialogTurnResult: - """ - booking_details = step_context.options - - if booking_details.destination is None: - message_text = "Where would you like to travel to?" - prompt_message = MessageFactory.text( - message_text, message_text, InputHints.expecting_input - ) - return await step_context.prompt( - TextPrompt.__name__, PromptOptions(prompt=prompt_message) - ) - return await step_context.next(booking_details.destination) - - async def origin_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - """ - If an origin city has not been provided, prompt for one. - :param step_context: - :return DialogTurnResult: - """ - booking_details = step_context.options - - # Capture the response to the previous step's prompt - booking_details.destination = step_context.result - if booking_details.origin is None: - message_text = "From what city will you be travelling?" - prompt_message = MessageFactory.text( - message_text, message_text, InputHints.expecting_input - ) - return await step_context.prompt( - TextPrompt.__name__, PromptOptions(prompt=prompt_message) - ) - return await step_context.next(booking_details.origin) - - async def travel_date_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - """ - If a travel date has not been provided, prompt for one. - This will use the DATE_RESOLVER_DIALOG. - :param step_context: - :return DialogTurnResult: - """ - booking_details = step_context.options - - # Capture the results of the previous step - booking_details.origin = step_context.result - if not booking_details.travel_date or self.is_ambiguous( - booking_details.travel_date - ): - return await step_context.begin_dialog( - DateResolverDialog.__name__, booking_details.travel_date - ) - return await step_context.next(booking_details.travel_date) - - async def confirm_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - """ - Confirm the information the user has provided. - :param step_context: - :return DialogTurnResult: - """ - booking_details = step_context.options - - # Capture the results of the previous step - booking_details.travel_date = step_context.result - message_text = ( - f"Please confirm, I have you traveling to: { booking_details.destination } from: " - f"{ booking_details.origin } on: { booking_details.travel_date}." - ) - prompt_message = MessageFactory.text( - message_text, message_text, InputHints.expecting_input - ) - - # Offer a YES/NO prompt. - return await step_context.prompt( - ConfirmPrompt.__name__, PromptOptions(prompt=prompt_message) - ) - - async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - """ - Complete the interaction and end the dialog. - :param step_context: - :return DialogTurnResult: - """ - if step_context.result: - booking_details = step_context.options - - return await step_context.end_dialog(booking_details) - return await step_context.end_dialog() - - def is_ambiguous(self, timex: str) -> bool: - timex_property = Timex(timex) - return "definite" not in timex_property.types diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/dialogs/cancel_and_help_dialog.py b/generators/app/templates/core/{{cookiecutter.bot_name}}/dialogs/cancel_and_help_dialog.py deleted file mode 100644 index f09a63b62..000000000 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/dialogs/cancel_and_help_dialog.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.dialogs import ( - ComponentDialog, - DialogContext, - DialogTurnResult, - DialogTurnStatus, -) -from botbuilder.schema import ActivityTypes, InputHints -from botbuilder.core import MessageFactory - - -class CancelAndHelpDialog(ComponentDialog): - async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult: - result = await self.interrupt(inner_dc) - if result is not None: - return result - - return await super(CancelAndHelpDialog, self).on_continue_dialog(inner_dc) - - async def interrupt(self, inner_dc: DialogContext) -> DialogTurnResult: - if inner_dc.context.activity.type == ActivityTypes.message: - text = inner_dc.context.activity.text.lower() - - help_message_text = "Show Help..." - help_message = MessageFactory.text( - help_message_text, help_message_text, InputHints.expecting_input - ) - - if text in ("help", "?"): - await inner_dc.context.send_activity(help_message) - return DialogTurnResult(DialogTurnStatus.Waiting) - - cancel_message_text = "Cancelling" - cancel_message = MessageFactory.text( - cancel_message_text, cancel_message_text, InputHints.ignoring_input - ) - - if text in ("cancel", "quit"): - await inner_dc.context.send_activity(cancel_message) - return await inner_dc.cancel_all_dialogs() - - return None diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/dialogs/date_resolver_dialog.py b/generators/app/templates/core/{{cookiecutter.bot_name}}/dialogs/date_resolver_dialog.py deleted file mode 100644 index 985dbf389..000000000 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/dialogs/date_resolver_dialog.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import MessageFactory -from botbuilder.dialogs import WaterfallDialog, DialogTurnResult, WaterfallStepContext -from botbuilder.dialogs.prompts import ( - DateTimePrompt, - PromptValidatorContext, - PromptOptions, - DateTimeResolution, -) -from botbuilder.schema import InputHints -from datatypes_date_time.timex import Timex - -from .cancel_and_help_dialog import CancelAndHelpDialog - - -class DateResolverDialog(CancelAndHelpDialog): - def __init__(self, dialog_id: str = None): - super(DateResolverDialog, self).__init__( - dialog_id or DateResolverDialog.__name__ - ) - - self.add_dialog( - DateTimePrompt( - DateTimePrompt.__name__, DateResolverDialog.datetime_prompt_validator - ) - ) - self.add_dialog( - WaterfallDialog( - WaterfallDialog.__name__ + "2", [self.initial_step, self.final_step] - ) - ) - - self.initial_dialog_id = WaterfallDialog.__name__ + "2" - - async def initial_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - timex = step_context.options - - prompt_msg_text = "On what date would you like to travel?" - prompt_msg = MessageFactory.text( - prompt_msg_text, prompt_msg_text, InputHints.expecting_input - ) - - reprompt_msg_text = "I'm sorry, for best results, please enter your travel date " \ - "including the month, day and year." - reprompt_msg = MessageFactory.text( - reprompt_msg_text, reprompt_msg_text, InputHints.expecting_input - ) - - if timex is None: - # We were not given any date at all so prompt the user. - return await step_context.prompt( - DateTimePrompt.__name__, - PromptOptions(prompt=prompt_msg, retry_prompt=reprompt_msg), - ) - # We have a Date we just need to check it is unambiguous. - if "definite" not in Timex(timex).types: - # This is essentially a "reprompt" of the data we were given up front. - return await step_context.prompt( - DateTimePrompt.__name__, PromptOptions(prompt=reprompt_msg) - ) - - return await step_context.next(DateTimeResolution(timex=timex)) - - async def final_step(self, step_context: WaterfallStepContext): - timex = step_context.result[0].timex - return await step_context.end_dialog(timex) - - @staticmethod - async def datetime_prompt_validator(prompt_context: PromptValidatorContext) -> bool: - if prompt_context.recognized.succeeded: - timex = prompt_context.recognized.value[0].timex.split("T")[0] - - return "definite" in Timex(timex).types - - return False diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/dialogs/main_dialog.py b/generators/app/templates/core/{{cookiecutter.bot_name}}/dialogs/main_dialog.py deleted file mode 100644 index 91566728d..000000000 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/dialogs/main_dialog.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.dialogs import ( - ComponentDialog, - WaterfallDialog, - WaterfallStepContext, - DialogTurnResult, -) -from botbuilder.dialogs.prompts import TextPrompt, PromptOptions -from botbuilder.core import MessageFactory, TurnContext -from botbuilder.schema import InputHints - -from booking_details import BookingDetails -from flight_booking_recognizer import FlightBookingRecognizer - -from helpers import LuisHelper, Intent -from .booking_dialog import BookingDialog - - -class MainDialog(ComponentDialog): - def __init__( - self, luis_recognizer: FlightBookingRecognizer, booking_dialog: BookingDialog - ): - super(MainDialog, self).__init__(MainDialog.__name__) - - self._luis_recognizer = luis_recognizer - self._booking_dialog_id = booking_dialog.id - - self.add_dialog(TextPrompt(TextPrompt.__name__)) - self.add_dialog(booking_dialog) - self.add_dialog( - WaterfallDialog( - "WFDialog", [self.intro_step, self.act_step, self.final_step] - ) - ) - - self.initial_dialog_id = "WFDialog" - - async def intro_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - if not self._luis_recognizer.is_configured: - await step_context.context.send_activity( - MessageFactory.text( - "NOTE: LUIS is not configured. To enable all capabilities, add 'LuisAppId', 'LuisAPIKey' and " - "'LuisAPIHostName' to the appsettings.json file.", - input_hint=InputHints.ignoring_input, - ) - ) - - return await step_context.next(None) - message_text = ( - str(step_context.options) - if step_context.options - else "What can I help you with today?" - ) - prompt_message = MessageFactory.text( - message_text, message_text, InputHints.expecting_input - ) - - return await step_context.prompt( - TextPrompt.__name__, PromptOptions(prompt=prompt_message) - ) - - async def act_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - if not self._luis_recognizer.is_configured: - # LUIS is not configured, we just run the BookingDialog path with an empty BookingDetailsInstance. - return await step_context.begin_dialog( - self._booking_dialog_id, BookingDetails() - ) - - # Call LUIS and gather any potential booking details. (Note the TurnContext has the response to the prompt.) - intent, luis_result = await LuisHelper.execute_luis_query( - self._luis_recognizer, step_context.context - ) - - if intent == Intent.BOOK_FLIGHT.value and luis_result: - # Show a warning for Origin and Destination if we can't resolve them. - await MainDialog._show_warning_for_unsupported_cities( - step_context.context, luis_result - ) - - # Run the BookingDialog giving it whatever details we have from the LUIS call. - return await step_context.begin_dialog(self._booking_dialog_id, luis_result) - - if intent == Intent.GET_WEATHER.value: - get_weather_text = "TODO: get weather flow here" - get_weather_message = MessageFactory.text( - get_weather_text, get_weather_text, InputHints.ignoring_input - ) - await step_context.context.send_activity(get_weather_message) - - else: - didnt_understand_text = ( - "Sorry, I didn't get that. Please try asking in a different way" - ) - didnt_understand_message = MessageFactory.text( - didnt_understand_text, didnt_understand_text, InputHints.ignoring_input - ) - await step_context.context.send_activity(didnt_understand_message) - - return await step_context.next(None) - - async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - # If the child dialog ("BookingDialog") was cancelled or the user failed to confirm, - # the Result here will be null. - if step_context.result is not None: - result = step_context.result - - # Now we have all the booking details call the booking service. - - # If the call to the booking service was successful tell the user. - # time_property = Timex(result.travel_date) - # travel_date_msg = time_property.to_natural_language(datetime.now()) - msg_txt = f"I have you booked to {result.destination} from {result.origin} on {result.travel_date}" - message = MessageFactory.text(msg_txt, msg_txt, InputHints.ignoring_input) - await step_context.context.send_activity(message) - - prompt_message = "What else can I do for you?" - return await step_context.replace_dialog(self.id, prompt_message) - - @staticmethod - async def _show_warning_for_unsupported_cities( - context: TurnContext, luis_result: BookingDetails - ) -> None: - if luis_result.unsupported_airports: - message_text = ( - f"Sorry but the following airports are not supported:" - f" {', '.join(luis_result.unsupported_airports)}" - ) - message = MessageFactory.text( - message_text, message_text, InputHints.ignoring_input - ) - await context.send_activity(message) diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/flight_booking_recognizer.py b/generators/app/templates/core/{{cookiecutter.bot_name}}/flight_booking_recognizer.py deleted file mode 100644 index 7476103c7..000000000 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/flight_booking_recognizer.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.ai.luis import LuisApplication, LuisRecognizer -from botbuilder.core import Recognizer, RecognizerResult, TurnContext - - -class FlightBookingRecognizer(Recognizer): - def __init__(self, configuration: dict): - self._recognizer = None - - luis_is_configured = ( - configuration["LUIS_APP_ID"] - and configuration["LUIS_API_KEY"] - and configuration["LUIS_API_HOST_NAME"] - ) - if luis_is_configured: - luis_application = LuisApplication( - configuration["LUIS_APP_ID"], - configuration["LUIS_API_KEY"], - "https://" + configuration["LUIS_API_HOST_NAME"], - ) - - self._recognizer = LuisRecognizer(luis_application) - - @property - def is_configured(self) -> bool: - # Returns true if luis is configured in the appsettings.json and initialized. - return self._recognizer is not None - - async def recognize(self, turn_context: TurnContext) -> RecognizerResult: - return await self._recognizer.recognize(turn_context) diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/helpers/__init__.py b/generators/app/templates/core/{{cookiecutter.bot_name}}/helpers/__init__.py deleted file mode 100644 index 787a8ed1a..000000000 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/helpers/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .luis_helper import Intent, LuisHelper -from .dialog_helper import DialogHelper - -__all__ = [ - "DialogHelper", - "LuisHelper", - "Intent" -] diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/helpers/dialog_helper.py b/generators/app/templates/core/{{cookiecutter.bot_name}}/helpers/dialog_helper.py deleted file mode 100644 index 6b2646b0b..000000000 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/helpers/dialog_helper.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import StatePropertyAccessor, TurnContext -from botbuilder.dialogs import Dialog, DialogSet, DialogTurnStatus - - -class DialogHelper: - @staticmethod - async def run_dialog( - dialog: Dialog, turn_context: TurnContext, accessor: StatePropertyAccessor - ): - dialog_set = DialogSet(accessor) - dialog_set.add(dialog) - - dialog_context = await dialog_set.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dialog_context.begin_dialog(dialog.id) diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/helpers/luis_helper.py b/generators/app/templates/core/{{cookiecutter.bot_name}}/helpers/luis_helper.py deleted file mode 100644 index 30331a0d5..000000000 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/helpers/luis_helper.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from enum import Enum -from typing import Dict -from botbuilder.ai.luis import LuisRecognizer -from botbuilder.core import IntentScore, TopIntent, TurnContext - -from booking_details import BookingDetails - - -class Intent(Enum): - BOOK_FLIGHT = "BookFlight" - CANCEL = "Cancel" - GET_WEATHER = "GetWeather" - NONE_INTENT = "NoneIntent" - - -def top_intent(intents: Dict[Intent, dict]) -> TopIntent: - max_intent = Intent.NONE_INTENT - max_value = 0.0 - - for intent, value in intents: - intent_score = IntentScore(value) - if intent_score.score > max_value: - max_intent, max_value = intent, intent_score.score - - return TopIntent(max_intent, max_value) - - -class LuisHelper: - @staticmethod - async def execute_luis_query( - luis_recognizer: LuisRecognizer, turn_context: TurnContext - ) -> (Intent, object): - """ - Returns an object with pre-formatted LUIS results for the bot's dialogs to consume. - """ - result = None - intent = None - - try: - recognizer_result = await luis_recognizer.recognize(turn_context) - - intent = ( - sorted( - recognizer_result.intents, - key=recognizer_result.intents.get, - reverse=True, - )[:1][0] - if recognizer_result.intents - else None - ) - - if intent == Intent.BOOK_FLIGHT.value: - result = BookingDetails() - - # We need to get the result from the LUIS JSON which at every level - # returns an array. - to_entities = recognizer_result.entities.get("$instance", {}).get( - "To", [] - ) - if len(to_entities) > 0: - if recognizer_result.entities.get("To", [{"$instance": {}}])[0][ - "$instance" - ]: - result.destination = to_entities[0]["text"].capitalize() - else: - result.unsupported_airports.append( - to_entities[0]["text"].capitalize() - ) - - from_entities = recognizer_result.entities.get("$instance", {}).get( - "From", [] - ) - if len(from_entities) > 0: - if recognizer_result.entities.get("From", [{"$instance": {}}])[0][ - "$instance" - ]: - result.origin = from_entities[0]["text"].capitalize() - else: - result.unsupported_airports.append( - from_entities[0]["text"].capitalize() - ) - - # This value will be a TIMEX. And we are only interested in a Date so - # grab the first result and drop the Time part. - # TIMEX is a format that represents DateTime expressions that include - # some ambiguity. e.g. missing a Year. - date_entities = recognizer_result.entities.get("datetime", []) - if date_entities: - timex = date_entities[0]["timex"] - - if timex: - datetime = timex[0].split("T")[0] - - result.travel_date = datetime - - else: - result.travel_date = None - - except Exception as err: - print(err) - - return intent, result diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/requirements.txt b/generators/app/templates/core/{{cookiecutter.bot_name}}/requirements.txt deleted file mode 100644 index c11eb2923..000000000 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -botbuilder-dialogs>=4.4.0.b1 -botbuilder-ai>=4.4.0.b1 -datatypes-date-time>=1.0.0.a2 -flask>=1.0.3 - diff --git a/generators/app/templates/echo/cookiecutter.json b/generators/app/templates/echo/cookiecutter.json deleted file mode 100644 index 4a14b6ade..000000000 --- a/generators/app/templates/echo/cookiecutter.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "bot_name": "my_chat_bot", - "bot_description": "Demonstrate the core capabilities of the Microsoft Bot Framework" -} \ No newline at end of file diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/.pylintrc b/generators/app/templates/echo/{{cookiecutter.bot_name}}/.pylintrc deleted file mode 100644 index 1baee5edb..000000000 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/.pylintrc +++ /dev/null @@ -1,497 +0,0 @@ -[MASTER] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. -extension-pkg-whitelist= - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore= - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use. -jobs=1 - -# Control the amount of potential inferred values when inferring a single -# object. This can help the performance when dealing with large functions or -# complex, nested conditions. -limit-inference-results=100 - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# Specify a configuration file. -#rcfile= - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once). You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". -disable=missing-docstring, - too-few-public-methods, - bad-continuation, - no-self-use, - duplicate-code, - broad-except - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member - - -[REPORTS] - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages. -reports=no - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=sys.exit - - -[LOGGING] - -# Format style used to check logging format string. `old` means using % -# formatting, while `new` is for `{}` formatting. -logging-format-style=old - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package.. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# Tells whether to warn about missing members when the owner of the attribute -# is inferred to be None. -ignore-none=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid defining new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expected to -# not be used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore. -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=120 - -# Maximum number of lines in a module. -max-module-lines=1000 - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[BASIC] - -# Naming style matching correct argument names. -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style. -#argument-rgx= - -# Naming style matching correct attribute names. -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style. -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma. -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Naming style matching correct class attribute names. -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. -#class-attribute-rgx= - -# Naming style matching correct class names. -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming- -# style. -#class-rgx= - -# Naming style matching correct constant names. -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style. -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names. -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style. -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma. -good-names=i, - j, - k, - ex, - Run, - _ - -# Include a hint for the correct naming format with invalid-name. -include-naming-hint=no - -# Naming style matching correct inline iteration names. -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style. -#inlinevar-rgx= - -# Naming style matching correct method names. -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style. -#method-rgx= - -# Naming style matching correct module names. -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style. -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -# These decorators are taken in consideration only for invalid-name. -property-classes=abc.abstractproperty - -# Naming style matching correct variable names. -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style. -#variable-rgx= - - -[STRING] - -# This flag controls whether the implicit-str-concat-in-sequence should -# generate a warning on implicit string concatenation in sequences defined over -# several lines. -check-str-concat-over-line-jumps=no - - -[IMPORTS] - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules=optparse,tkinter.tix - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled). -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled). -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled). -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=cls - - -[DESIGN] - -# Maximum number of arguments for function / method. -max-args=5 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Maximum number of boolean expressions in an if statement. -max-bool-expr=5 - -# Maximum number of branch for function / method body. -max-branches=12 - -# Maximum number of locals for function / method body. -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body. -max-returns=6 - -# Maximum number of statements in function / method body. -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/README.md b/generators/app/templates/echo/{{cookiecutter.bot_name}}/README.md deleted file mode 100644 index 5eeee191f..000000000 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# {{cookiecutter.bot_name}} - -{{cookiecutter.bot_description}} - -This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back. - -## Prerequisites - -This sample **requires** prerequisites in order to run. - -### Install Python 3.6 - -## Running the sample -- Run `pip install -r requirements.txt` to install all dependencies -- Run `python app.py` -- Alternatively to the last command, you can set the file in an environment variable with `set FLASK_APP=app.py` in windows (`export FLASK_APP=app.py` in mac/linux) and then run `flask run --host=127.0.0.1 --port=3978` - - -## Testing the bot using Bot Framework Emulator - -[Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework Emulator version 4.3.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to the bot using Bot Framework Emulator - -- Launch Bot Framework Emulator -- Enter a Bot URL of `http://localhost:3978/api/messages` - - -## Further reading - -- [Bot Framework Documentation](https://docs.botframework.com) -- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) -- [Dialogs](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0) -- [Gathering Input Using Prompts](https://docs.microsoft.com/azure/bot-service/bot-builder-prompts?view=azure-bot-service-4.0&tabs=csharp) -- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) -- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) -- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) -- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest) -- [Azure Portal](https://portal.azure.com) -- [Language Understanding using LUIS](https://docs.microsoft.com/azure/cognitive-services/luis/) -- [Channels and Bot Connector Service](https://docs.microsoft.com/azure/bot-service/bot-concepts?view=azure-bot-service-4.0) \ No newline at end of file diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/__init__.py b/generators/app/templates/echo/{{cookiecutter.bot_name}}/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/app.py b/generators/app/templates/echo/{{cookiecutter.bot_name}}/app.py deleted file mode 100644 index f7fa35cac..000000000 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/app.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import asyncio -import sys -from datetime import datetime -from types import MethodType - -from flask import Flask, request, Response -from botbuilder.core import ( - BotFrameworkAdapter, - BotFrameworkAdapterSettings, - TurnContext, -) -from botbuilder.schema import Activity, ActivityTypes -from bot import MyBot - -# Create the loop and Flask app -LOOP = asyncio.get_event_loop() -APP = Flask(__name__, instance_relative_config=True) -APP.config.from_object("config.DefaultConfig") - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"]) -ADAPTER = BotFrameworkAdapter(SETTINGS) - -# Catch-all for errors. -# pylint: disable=unused-argument -async def on_error(self, context: TurnContext, error: Exception): - # This check writes out errors to console log .vs. app insights. - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - - # Send a message to the user - await context.send_activity("The bot encounted an error or bug.") - await context.send_activity("To continue to run this bot, please fix the bot source code.") - # Send a trace activity if we're talking to the Bot Framework Emulator - if context.activity.channel_id == 'emulator': - # Create a trace activity that contains the error object - trace_activity = Activity( - label="TurnError", - name="on_turn_error Trace", - timestamp=datetime.utcnow(), - type=ActivityTypes.trace, - value=f"{error}", - value_type="https://www.botframework.com/schemas/error" - ) - # Send a trace activity, which will be displayed in Bot Framework Emulator - await context.send_activity(trace_activity) - -ADAPTER.on_turn_error = MethodType(on_error, ADAPTER) - -# Create the main dialog -BOT = MyBot() - -# Listen for incoming requests on /api/messages. -@APP.route("/api/messages", methods=["POST"]) -def messages(): - # Main bot message handler. - if "application/json" in request.headers["Content-Type"]: - body = request.json - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = ( - request.headers["Authorization"] if "Authorization" in request.headers else "" - ) - - try: - task = LOOP.create_task( - ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - ) - LOOP.run_until_complete(task) - return Response(status=201) - except Exception as exception: - raise exception - - -if __name__ == "__main__": - try: - APP.run(debug=False, port=APP.config["PORT"]) # nosec debug - except Exception as exception: - raise exception diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/bot.py b/generators/app/templates/echo/{{cookiecutter.bot_name}}/bot.py deleted file mode 100644 index c1ea90861..000000000 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/bot.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import ActivityHandler, TurnContext -from botbuilder.schema import ChannelAccount - - -class MyBot(ActivityHandler): - # See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types. - - async def on_message_activity(self, turn_context: TurnContext): - await turn_context.send_activity(f"You said '{ turn_context.activity.text }'") - - async def on_members_added_activity( - self, - members_added: ChannelAccount, - turn_context: TurnContext - ): - for member_added in members_added: - if member_added.id != turn_context.activity.recipient.id: - await turn_context.send_activity("Hello and welcome!") diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/config.py b/generators/app/templates/echo/{{cookiecutter.bot_name}}/config.py deleted file mode 100644 index 7163a79aa..000000000 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/config.py +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - -class DefaultConfig: - """ Bot Configuration """ - - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/requirements.txt b/generators/app/templates/echo/{{cookiecutter.bot_name}}/requirements.txt deleted file mode 100644 index 2e5ecf3fc..000000000 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -botbuilder-core>=4.5.0.b4 -flask>=1.0.3 - diff --git a/generators/app/templates/empty/cookiecutter.json b/generators/app/templates/empty/cookiecutter.json deleted file mode 100644 index 4a14b6ade..000000000 --- a/generators/app/templates/empty/cookiecutter.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "bot_name": "my_chat_bot", - "bot_description": "Demonstrate the core capabilities of the Microsoft Bot Framework" -} \ No newline at end of file diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/.pylintrc b/generators/app/templates/empty/{{cookiecutter.bot_name}}/.pylintrc deleted file mode 100644 index 1baee5edb..000000000 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/.pylintrc +++ /dev/null @@ -1,497 +0,0 @@ -[MASTER] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. -extension-pkg-whitelist= - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore= - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use. -jobs=1 - -# Control the amount of potential inferred values when inferring a single -# object. This can help the performance when dealing with large functions or -# complex, nested conditions. -limit-inference-results=100 - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# Specify a configuration file. -#rcfile= - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once). You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". -disable=missing-docstring, - too-few-public-methods, - bad-continuation, - no-self-use, - duplicate-code, - broad-except - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member - - -[REPORTS] - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages. -reports=no - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=sys.exit - - -[LOGGING] - -# Format style used to check logging format string. `old` means using % -# formatting, while `new` is for `{}` formatting. -logging-format-style=old - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package.. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# Tells whether to warn about missing members when the owner of the attribute -# is inferred to be None. -ignore-none=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid defining new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expected to -# not be used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore. -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=120 - -# Maximum number of lines in a module. -max-module-lines=1000 - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[BASIC] - -# Naming style matching correct argument names. -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style. -#argument-rgx= - -# Naming style matching correct attribute names. -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style. -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma. -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Naming style matching correct class attribute names. -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. -#class-attribute-rgx= - -# Naming style matching correct class names. -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming- -# style. -#class-rgx= - -# Naming style matching correct constant names. -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style. -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names. -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style. -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma. -good-names=i, - j, - k, - ex, - Run, - _ - -# Include a hint for the correct naming format with invalid-name. -include-naming-hint=no - -# Naming style matching correct inline iteration names. -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style. -#inlinevar-rgx= - -# Naming style matching correct method names. -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style. -#method-rgx= - -# Naming style matching correct module names. -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style. -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -# These decorators are taken in consideration only for invalid-name. -property-classes=abc.abstractproperty - -# Naming style matching correct variable names. -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style. -#variable-rgx= - - -[STRING] - -# This flag controls whether the implicit-str-concat-in-sequence should -# generate a warning on implicit string concatenation in sequences defined over -# several lines. -check-str-concat-over-line-jumps=no - - -[IMPORTS] - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules=optparse,tkinter.tix - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled). -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled). -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled). -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=cls - - -[DESIGN] - -# Maximum number of arguments for function / method. -max-args=5 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Maximum number of boolean expressions in an if statement. -max-bool-expr=5 - -# Maximum number of branch for function / method body. -max-branches=12 - -# Maximum number of locals for function / method body. -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body. -max-returns=6 - -# Maximum number of statements in function / method body. -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/README.md b/generators/app/templates/empty/{{cookiecutter.bot_name}}/README.md deleted file mode 100644 index 5eeee191f..000000000 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# {{cookiecutter.bot_name}} - -{{cookiecutter.bot_description}} - -This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back. - -## Prerequisites - -This sample **requires** prerequisites in order to run. - -### Install Python 3.6 - -## Running the sample -- Run `pip install -r requirements.txt` to install all dependencies -- Run `python app.py` -- Alternatively to the last command, you can set the file in an environment variable with `set FLASK_APP=app.py` in windows (`export FLASK_APP=app.py` in mac/linux) and then run `flask run --host=127.0.0.1 --port=3978` - - -## Testing the bot using Bot Framework Emulator - -[Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework Emulator version 4.3.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to the bot using Bot Framework Emulator - -- Launch Bot Framework Emulator -- Enter a Bot URL of `http://localhost:3978/api/messages` - - -## Further reading - -- [Bot Framework Documentation](https://docs.botframework.com) -- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) -- [Dialogs](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0) -- [Gathering Input Using Prompts](https://docs.microsoft.com/azure/bot-service/bot-builder-prompts?view=azure-bot-service-4.0&tabs=csharp) -- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) -- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) -- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) -- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest) -- [Azure Portal](https://portal.azure.com) -- [Language Understanding using LUIS](https://docs.microsoft.com/azure/cognitive-services/luis/) -- [Channels and Bot Connector Service](https://docs.microsoft.com/azure/bot-service/bot-concepts?view=azure-bot-service-4.0) \ No newline at end of file diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/__init__.py b/generators/app/templates/empty/{{cookiecutter.bot_name}}/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/app.py b/generators/app/templates/empty/{{cookiecutter.bot_name}}/app.py deleted file mode 100644 index 4ab9d480f..000000000 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/app.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import asyncio -import sys -from types import MethodType - -from flask import Flask, request, Response -from botbuilder.core import ( - BotFrameworkAdapter, - BotFrameworkAdapterSettings, - TurnContext, -) -from botbuilder.schema import Activity -from bot import MyBot - -# Create the loop and Flask app -LOOP = asyncio.get_event_loop() -APP = Flask(__name__, instance_relative_config=True) -APP.config.from_object("config.DefaultConfig") - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"]) -ADAPTER = BotFrameworkAdapter(SETTINGS) - -# Catch-all for errors. -# pylint: disable=unused-argument -async def on_error(self, context: TurnContext, error: Exception): - # This check writes out errors to console log .vs. app insights. - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - - # Send a message to the user - await context.send_activity("The bot encounted an error or bug.") - await context.send_activity("To continue to run this bot, please fix the bot source code.") - -ADAPTER.on_turn_error = MethodType(on_error, ADAPTER) - -# Create the main dialog -BOT = MyBot() - -# Listen for incoming requests on /api/messages. -@APP.route("/api/messages", methods=["POST"]) -def messages(): - # Main bot message handler. - if "application/json" in request.headers["Content-Type"]: - body = request.json - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = ( - request.headers["Authorization"] if "Authorization" in request.headers else "" - ) - - try: - task = LOOP.create_task( - ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - ) - LOOP.run_until_complete(task) - return Response(status=201) - except Exception as exception: - raise exception - - -if __name__ == "__main__": - try: - APP.run(debug=False, port=APP.config["PORT"]) # nosec debug - except Exception as exception: - raise exception diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/bot.py b/generators/app/templates/empty/{{cookiecutter.bot_name}}/bot.py deleted file mode 100644 index f0c2122cf..000000000 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/bot.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import ActivityHandler, TurnContext -from botbuilder.schema import ChannelAccount - - -class MyBot(ActivityHandler): - async def on_members_added_activity( - self, - members_added: ChannelAccount, - turn_context: TurnContext - ): - for member_added in members_added: - if member_added.id != turn_context.activity.recipient.id: - await turn_context.send_activity("Hello world!") diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/config.py b/generators/app/templates/empty/{{cookiecutter.bot_name}}/config.py deleted file mode 100644 index 7163a79aa..000000000 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/config.py +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - -class DefaultConfig: - """ Bot Configuration """ - - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/requirements.txt b/generators/app/templates/empty/{{cookiecutter.bot_name}}/requirements.txt deleted file mode 100644 index 2e5ecf3fc..000000000 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -botbuilder-core>=4.5.0.b4 -flask>=1.0.3 - diff --git a/samples/01.console-echo/README.md b/samples/01.console-echo/README.md deleted file mode 100644 index 996e0909b..000000000 --- a/samples/01.console-echo/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# Console EchoBot -Bot Framework v4 console echo sample. - -This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that you can talk to from the console window. - -This sample shows a simple echo bot and demonstrates the bot working as a console app using a sample console adapter. - -## To try this sample -- Clone the repository -```bash -git clone https://github.com/Microsoft/botbuilder-python.git -``` - - -### Visual studio code -- open `botbuilder-python\samples\01.console-echo` folder -- Bring up a terminal, navigate to `botbuilder-python\samples\01.console-echo` folder -- type 'python main.py' - - -# Adapters -[Adapters](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0#the-bot-adapter) provide an abstraction for your bot to work with a variety of environments. - -A bot is directed by it's adapter, which can be thought of as the conductor for your bot. The adapter is responsible for directing incoming and outgoing communication, authentication, and so on. The adapter differs based on it's environment (the adapter internally works differently locally versus on Azure) but in each instance it achieves the same goal. - -In most situations we don't work with the adapter directly, such as when creating a bot from a template, but it's good to know it's there and what it does. -The bot adapter encapsulates authentication processes and sends activities to and receives activities from the Bot Connector Service. When your bot receives an activity, the adapter wraps up everything about that activity, creates a [context object](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0#turn-context), passes it to your bot's application logic, and sends responses generated by your bot back to the user's channel. - - -# Further reading - -- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) -- [Bot basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) -- [Channels and Bot Connector service](https://docs.microsoft.com/azure/bot-service/bot-concepts?view=azure-bot-service-4.0) -- [Activity processing](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) \ No newline at end of file diff --git a/samples/01.console-echo/adapter/__init__.py b/samples/01.console-echo/adapter/__init__.py deleted file mode 100644 index 56d4bd2ee..000000000 --- a/samples/01.console-echo/adapter/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .console_adapter import ConsoleAdapter - -__all__ = ["ConsoleAdapter"] diff --git a/samples/01.console-echo/adapter/console_adapter.py b/samples/01.console-echo/adapter/console_adapter.py deleted file mode 100644 index 16824436f..000000000 --- a/samples/01.console-echo/adapter/console_adapter.py +++ /dev/null @@ -1,180 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import datetime -import asyncio -import warnings -from typing import List, Callable - -from botbuilder.schema import ( - Activity, - ActivityTypes, - ChannelAccount, - ConversationAccount, - ResourceResponse, - ConversationReference, -) -from botbuilder.core.turn_context import TurnContext -from botbuilder.core.bot_adapter import BotAdapter - - -class ConsoleAdapter(BotAdapter): - """ - Lets a user communicate with a bot from a console window. - - :Example: - import asyncio - from botbuilder.core import ConsoleAdapter - - async def logic(context): - await context.send_activity('Hello World!') - - adapter = ConsoleAdapter() - loop = asyncio.get_event_loop() - if __name__ == "__main__": - try: - loop.run_until_complete(adapter.process_activity(logic)) - except KeyboardInterrupt: - pass - finally: - loop.stop() - loop.close() - """ - - def __init__(self, reference: ConversationReference = None): - super(ConsoleAdapter, self).__init__() - - self.reference = ConversationReference( - channel_id="console", - user=ChannelAccount(id="user", name="User1"), - bot=ChannelAccount(id="bot", name="Bot"), - conversation=ConversationAccount(id="convo1", name="", is_group=False), - service_url="", - ) - - # Warn users to pass in an instance of a ConversationReference, otherwise the parameter will be ignored. - if reference is not None and not isinstance(reference, ConversationReference): - warnings.warn( - "ConsoleAdapter: `reference` argument is not an instance of ConversationReference and will " - "be ignored." - ) - else: - self.reference.channel_id = getattr( - reference, "channel_id", self.reference.channel_id - ) - self.reference.user = getattr(reference, "user", self.reference.user) - self.reference.bot = getattr(reference, "bot", self.reference.bot) - self.reference.conversation = getattr( - reference, "conversation", self.reference.conversation - ) - self.reference.service_url = getattr( - reference, "service_url", self.reference.service_url - ) - # The only attribute on self.reference without an initial value is activity_id, so if reference does not - # have a value for activity_id, default self.reference.activity_id to None - self.reference.activity_id = getattr(reference, "activity_id", None) - - self._next_id = 0 - - async def process_activity(self, logic: Callable): - """ - Begins listening to console input. - :param logic: - :return: - """ - while True: - msg = input() - if msg is None: - pass - else: - self._next_id += 1 - activity = Activity( - text=msg, - channel_id="console", - from_property=ChannelAccount(id="user", name="User1"), - recipient=ChannelAccount(id="bot", name="Bot"), - conversation=ConversationAccount(id="Convo1"), - type=ActivityTypes.message, - timestamp=datetime.datetime.now(), - id=str(self._next_id), - ) - - activity = TurnContext.apply_conversation_reference( - activity, self.reference, True - ) - context = TurnContext(self, activity) - await self.run_pipeline(context, logic) - - async def send_activities(self, context: TurnContext, activities: List[Activity]) -> List[ResourceResponse]: - """ - Logs a series of activities to the console. - :param context: - :param activities: - :return: - """ - if context is None: - raise TypeError( - "ConsoleAdapter.send_activities(): `context` argument cannot be None." - ) - if not isinstance(activities, list): - raise TypeError( - "ConsoleAdapter.send_activities(): `activities` argument must be a list." - ) - if len(activities) == 0: - raise ValueError( - "ConsoleAdapter.send_activities(): `activities` argument cannot have a length of 0." - ) - - async def next_activity(i: int): - responses = [] - - if i < len(activities): - responses.append(ResourceResponse()) - activity = activities[i] - - if activity.type == "delay": - await asyncio.sleep(activity.delay) - await next_activity(i + 1) - elif activity.type == ActivityTypes.message: - if ( - activity.attachments is not None - and len(activity.attachments) > 0 - ): - append = ( - "(1 attachment)" - if len(activity.attachments) == 1 - else f"({len(activity.attachments)} attachments)" - ) - print(f"{activity.text} {append}") - else: - print(activity.text) - await next_activity(i + 1) - else: - print(f"[{activity.type}]") - await next_activity(i + 1) - else: - return responses - - await next_activity(0) - - async def delete_activity( - self, context: TurnContext, reference: ConversationReference - ): - """ - Not supported for the ConsoleAdapter. Calling this method or `TurnContext.delete_activity()` - will result an error being returned. - :param context: - :param reference: - :return: - """ - raise NotImplementedError("ConsoleAdapter.delete_activity(): not supported.") - - async def update_activity(self, context: TurnContext, activity: Activity): - """ - Not supported for the ConsoleAdapter. Calling this method or `TurnContext.update_activity()` - will result an error being returned. - :param context: - :param activity: - :return: - """ - raise NotImplementedError("ConsoleAdapter.update_activity(): not supported.") diff --git a/samples/01.console-echo/bot.py b/samples/01.console-echo/bot.py deleted file mode 100644 index 226f0d963..000000000 --- a/samples/01.console-echo/bot.py +++ /dev/null @@ -1,16 +0,0 @@ -from sys import exit - - -class EchoBot: - async def on_turn(self, context): - # Check to see if this activity is an incoming message. - # (It could theoretically be another type of activity.) - if context.activity.type == "message" and context.activity.text: - # Check to see if the user sent a simple "quit" message. - if context.activity.text.lower() == "quit": - # Send a reply. - await context.send_activity("Bye!") - exit(0) - else: - # Echo the message text back to the user. - await context.send_activity(f"I heard you say {context.activity.text}") diff --git a/samples/01.console-echo/main.py b/samples/01.console-echo/main.py deleted file mode 100644 index 73801d1b8..000000000 --- a/samples/01.console-echo/main.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import asyncio - -from adapter import ConsoleAdapter -from bot import EchoBot - -# Create adapter -ADAPTER = ConsoleAdapter() -BOT = EchoBot() - -LOOP = asyncio.get_event_loop() - -if __name__ == "__main__": - try: - # Greet user - print("Hi... I'm an echobot. Whatever you say I'll echo back.") - - LOOP.run_until_complete(ADAPTER.process_activity(BOT.on_turn)) - except KeyboardInterrupt: - pass - finally: - LOOP.stop() - LOOP.close() diff --git a/samples/01.console-echo/requirements.txt b/samples/01.console-echo/requirements.txt deleted file mode 100644 index 7e1c1616d..000000000 --- a/samples/01.console-echo/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -asyncio>=3.4.3 -botbuilder-core>=4.4.0.b1 -botbuilder-schema>=4.4.0.b1 -botframework-connector>=4.4.0.b1 \ No newline at end of file diff --git a/samples/02.echo-bot/README.md b/samples/02.echo-bot/README.md deleted file mode 100644 index 40e84f525..000000000 --- a/samples/02.echo-bot/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# EchoBot - -Bot Framework v4 echo bot sample. - -This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back. - -## Running the sample -- Clone the repository -```bash -git clone https://github.com/Microsoft/botbuilder-python.git -``` -- Activate your desired virtual environment -- Bring up a terminal, navigate to `botbuilder-python\samples\02.echo-bot` folder -- In the terminal, type `pip install -r requirements.txt` -- In the terminal, type `python app.py` - -## Testing the bot using Bot Framework Emulator -[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to bot using Bot Framework Emulator -- Launch Bot Framework Emulator -- Paste this URL in the emulator window - http://localhost:3978/api/messages - -## Further reading - -- [Bot Framework Documentation](https://docs.botframework.com) -- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) -- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) diff --git a/samples/02.echo-bot/app.py b/samples/02.echo-bot/app.py deleted file mode 100644 index 5cc960eb8..000000000 --- a/samples/02.echo-bot/app.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import asyncio -import sys -from datetime import datetime - -from flask import Flask, request, Response -from botbuilder.core import BotFrameworkAdapterSettings, TurnContext, BotFrameworkAdapter -from botbuilder.schema import Activity, ActivityTypes - -from bots import EchoBot - -# Create the loop and Flask app -LOOP = asyncio.get_event_loop() -app = Flask(__name__, instance_relative_config=True) -app.config.from_object("config.DefaultConfig") - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings(app.config["APP_ID"], app.config["APP_PASSWORD"]) -ADAPTER = BotFrameworkAdapter(SETTINGS) - - -# Catch-all for errors. -async def on_error(context: TurnContext, error: Exception): - # This check writes out errors to console log .vs. app insights. - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - - # Send a message to the user - await context.send_activity("The bot encountered an error or bug.") - await context.send_activity("To continue to run this bot, please fix the bot source code.") - # Send a trace activity if we're talking to the Bot Framework Emulator - if context.activity.channel_id == 'emulator': - # Create a trace activity that contains the error object - trace_activity = Activity( - label="TurnError", - name="on_turn_error Trace", - timestamp=datetime.utcnow(), - type=ActivityTypes.trace, - value=f"{error}", - value_type="https://www.botframework.com/schemas/error" - ) - # Send a trace activity, which will be displayed in Bot Framework Emulator - await context.send_activity(trace_activity) - -ADAPTER.on_turn_error = on_error - -# Create the Bot -BOT = EchoBot() - -# Listen for incoming requests on /api/messages -@app.route("/api/messages", methods=["POST"]) -def messages(): - # Main bot message handler. - if "application/json" in request.headers["Content-Type"]: - body = request.json - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = ( - request.headers["Authorization"] if "Authorization" in request.headers else "" - ) - - try: - task = LOOP.create_task( - ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - ) - LOOP.run_until_complete(task) - return Response(status=201) - except Exception as exception: - raise exception - - -if __name__ == "__main__": - try: - app.run(debug=False, port=app.config["PORT"]) # nosec debug - except Exception as exception: - raise exception diff --git a/samples/02.echo-bot/bots/__init__.py b/samples/02.echo-bot/bots/__init__.py deleted file mode 100644 index f95fbbbad..000000000 --- a/samples/02.echo-bot/bots/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .echo_bot import EchoBot - -__all__ = ["EchoBot"] diff --git a/samples/02.echo-bot/bots/echo_bot.py b/samples/02.echo-bot/bots/echo_bot.py deleted file mode 100644 index 985c0694c..000000000 --- a/samples/02.echo-bot/bots/echo_bot.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import ActivityHandler, MessageFactory, TurnContext -from botbuilder.schema import ChannelAccount - - -class EchoBot(ActivityHandler): - async def on_members_added_activity( - self, members_added: [ChannelAccount], turn_context: TurnContext - ): - for member in members_added: - if member.id != turn_context.activity.recipient.id: - await turn_context.send_activity("Hello and welcome!") - - async def on_message_activity(self, turn_context: TurnContext): - return await turn_context.send_activity(MessageFactory.text(f"Echo: {turn_context.activity.text}")) diff --git a/samples/02.echo-bot/config.py b/samples/02.echo-bot/config.py deleted file mode 100644 index e007d0fa9..000000000 --- a/samples/02.echo-bot/config.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - -""" Bot Configuration """ - - -class DefaultConfig: - """ Bot Configuration """ - - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") diff --git a/samples/02.echo-bot/deploymentTemplates/template-with-preexisting-rg.json b/samples/02.echo-bot/deploymentTemplates/template-with-preexisting-rg.json deleted file mode 100644 index bff8c096d..000000000 --- a/samples/02.echo-bot/deploymentTemplates/template-with-preexisting-rg.json +++ /dev/null @@ -1,242 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "appId": { - "type": "string", - "metadata": { - "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." - } - }, - "appSecret": { - "type": "string", - "metadata": { - "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." - } - }, - "botId": { - "type": "string", - "metadata": { - "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." - } - }, - "botSku": { - "defaultValue": "F0", - "type": "string", - "metadata": { - "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." - } - }, - "newAppServicePlanName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "The name of the new App Service Plan." - } - }, - "newAppServicePlanSku": { - "type": "object", - "defaultValue": { - "name": "S1", - "tier": "Standard", - "size": "S1", - "family": "S", - "capacity": 1 - }, - "metadata": { - "description": "The SKU of the App Service Plan. Defaults to Standard values." - } - }, - "appServicePlanLocation": { - "type": "string", - "metadata": { - "description": "The location of the App Service Plan." - } - }, - "existingAppServicePlan": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Name of the existing App Service Plan used to create the Web App for the bot." - } - }, - "newWebAppName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." - } - } - }, - "variables": { - "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", - "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", - "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", - "publishingUsername": "[concat('$', parameters('newWebAppName'))]", - "resourcesLocation": "[parameters('appServicePlanLocation')]", - "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", - "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", - "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" - }, - "resources": [ - { - "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", - "type": "Microsoft.Web/serverfarms", - "apiVersion": "2016-09-01", - "name": "[variables('servicePlanName')]", - "location": "[variables('resourcesLocation')]", - "sku": "[parameters('newAppServicePlanSku')]", - "kind": "linux", - "properties": { - "name": "[variables('servicePlanName')]", - "perSiteScaling": false, - "reserved": true, - "targetWorkerCount": 0, - "targetWorkerSizeId": 0 - } - }, - { - "comments": "Create a Web App using a Linux App Service Plan", - "type": "Microsoft.Web/sites", - "apiVersion": "2016-08-01", - "name": "[variables('webAppName')]", - "location": "[variables('resourcesLocation')]", - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]" - ], - "kind": "app,linux", - "properties": { - "enabled": true, - "hostNameSslStates": [ - { - "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", - "sslState": "Disabled", - "hostType": "Standard" - }, - { - "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", - "sslState": "Disabled", - "hostType": "Repository" - } - ], - "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]", - "reserved": true, - "scmSiteAlsoStopped": false, - "clientAffinityEnabled": false, - "clientCertEnabled": false, - "hostNamesDisabled": false, - "containerSize": 0, - "dailyMemoryTimeQuota": 0, - "httpsOnly": false, - "siteConfig": { - "appSettings": [ - { - "name": "MicrosoftAppId", - "value": "[parameters('appId')]" - }, - { - "name": "MicrosoftAppPassword", - "value": "[parameters('appSecret')]" - }, - { - "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", - "value": "true" - } - ], - "cors": { - "allowedOrigins": [ - "https://botservice.hosting.portal.azure.net", - "https://hosting.onecloud.azure-test.net/" - ] - } - } - } - }, - { - "type": "Microsoft.Web/sites/config", - "apiVersion": "2016-08-01", - "name": "[concat(variables('webAppName'), '/web')]", - "location": "[variables('resourcesLocation')]", - "dependsOn": [ - "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" - ], - "properties": { - "numberOfWorkers": 1, - "defaultDocuments": [ - "Default.htm", - "Default.html", - "Default.asp", - "index.htm", - "index.html", - "iisstart.htm", - "default.aspx", - "index.php", - "hostingstart.html" - ], - "netFrameworkVersion": "v4.0", - "phpVersion": "", - "pythonVersion": "", - "nodeVersion": "", - "linuxFxVersion": "PYTHON|3.7", - "requestTracingEnabled": false, - "remoteDebuggingEnabled": false, - "remoteDebuggingVersion": "VS2017", - "httpLoggingEnabled": true, - "logsDirectorySizeLimit": 35, - "detailedErrorLoggingEnabled": false, - "publishingUsername": "[variables('publishingUsername')]", - "scmType": "None", - "use32BitWorkerProcess": true, - "webSocketsEnabled": false, - "alwaysOn": false, - "appCommandLine": "", - "managedPipelineMode": "Integrated", - "virtualApplications": [ - { - "virtualPath": "/", - "physicalPath": "site\\wwwroot", - "preloadEnabled": false, - "virtualDirectories": null - } - ], - "winAuthAdminState": 0, - "winAuthTenantState": 0, - "customAppPoolIdentityAdminState": false, - "customAppPoolIdentityTenantState": false, - "loadBalancing": "LeastRequests", - "routingRules": [], - "experiments": { - "rampUpRules": [] - }, - "autoHealEnabled": false, - "vnetName": "", - "minTlsVersion": "1.2", - "ftpsState": "AllAllowed", - "reservedInstanceCount": 0 - } - }, - { - "apiVersion": "2017-12-01", - "type": "Microsoft.BotService/botServices", - "name": "[parameters('botId')]", - "location": "global", - "kind": "bot", - "sku": { - "name": "[parameters('botSku')]" - }, - "properties": { - "name": "[parameters('botId')]", - "displayName": "[parameters('botId')]", - "endpoint": "[variables('botEndpoint')]", - "msaAppId": "[parameters('appId')]", - "developerAppInsightsApplicationId": null, - "developerAppInsightKey": null, - "publishingCredentials": null, - "storageResourceId": null - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" - ] - } - ] -} \ No newline at end of file diff --git a/samples/02.echo-bot/requirements.txt b/samples/02.echo-bot/requirements.txt deleted file mode 100644 index 7e54b62ec..000000000 --- a/samples/02.echo-bot/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -botbuilder-core>=4.4.0b1 -flask>=1.0.3 diff --git a/samples/03.welcome-user/README.md b/samples/03.welcome-user/README.md deleted file mode 100644 index ac6c37553..000000000 --- a/samples/03.welcome-user/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# welcome users - - -Bot Framework v4 welcome users bot sample - -This bot has been created using [Bot Framework](https://dev.botframework.com), is shows how to welcome users when they join the conversation. - -## Running the sample -- Clone the repository -```bash -git clone https://github.com/Microsoft/botbuilder-python.git -``` -- Activate your desired virtual environment -- Bring up a terminal, navigate to `botbuilder-python\samples\03.welcome-user` folder -- In the terminal, type `pip install -r requirements.txt` -- In the terminal, type `python app.py` - -## Testing the bot using Bot Framework Emulator -[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to bot using Bot Framework Emulator -- Launch Bot Framework Emulator -- Paste this URL in the emulator window - http://localhost:3978/api/messages - - -## Welcoming Users - -The primary goal when creating any bot is to engage your user in a meaningful conversation. One of the best ways to achieve this goal is to ensure that from the moment a user first connects, they understand your bot’s main purpose and capabilities, the reason your bot was created. See [Send welcome message to users](https://aka.ms/botframework-welcome-instructions) for additional information on how a bot can welcome users to a conversation. - -## Further reading - -- [Bot Framework Documentation](https://docs.botframework.com) -- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) -- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) diff --git a/samples/03.welcome-user/app.py b/samples/03.welcome-user/app.py deleted file mode 100644 index 7941afeb1..000000000 --- a/samples/03.welcome-user/app.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import asyncio -import sys -from datetime import datetime - -from flask import Flask, request, Response -from botbuilder.core import ( - BotFrameworkAdapter, - BotFrameworkAdapterSettings, - MemoryStorage, - TurnContext, - UserState, -) -from botbuilder.schema import Activity, ActivityTypes - -from bots import WelcomeUserBot - -# Create the loop and Flask app -LOOP = asyncio.get_event_loop() -APP = Flask(__name__, instance_relative_config=True) -APP.config.from_object("config.DefaultConfig") - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"]) -ADAPTER = BotFrameworkAdapter(SETTINGS) - - -# Catch-all for errors. -async def on_error(context: TurnContext, error: Exception): - # This check writes out errors to console log .vs. app insights. - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - - # Send a message to the user - await context.send_activity("The bot encountered an error or bug.") - await context.send_activity( - "To continue to run this bot, please fix the bot source code." - ) - # Send a trace activity if we're talking to the Bot Framework Emulator - if context.activity.channel_id == "emulator": - # Create a trace activity that contains the error object - trace_activity = Activity( - label="TurnError", - name="on_turn_error Trace", - timestamp=datetime.utcnow(), - type=ActivityTypes.trace, - value=f"{error}", - value_type="https://www.botframework.com/schemas/error", - ) - # Send a trace activity, which will be displayed in Bot Framework Emulator - await context.send_activity(trace_activity) - - -ADAPTER.on_turn_error = on_error - -# Create MemoryStorage, UserState -MEMORY = MemoryStorage() -USER_STATE = UserState(MEMORY) - -# Create the Bot -BOT = WelcomeUserBot(USER_STATE) - -# Listen for incoming requests on /api/messages. -@APP.route("/api/messages", methods=["POST"]) -def messages(): - # Main bot message handler. - if "application/json" in request.headers["Content-Type"]: - body = request.json - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = ( - request.headers["Authorization"] if "Authorization" in request.headers else "" - ) - - try: - task = LOOP.create_task( - ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - ) - LOOP.run_until_complete(task) - return Response(status=201) - except Exception as exception: - raise exception - - -if __name__ == "__main__": - try: - APP.run(debug=False, port=APP.config["PORT"]) # nosec debug - except Exception as exception: - raise exception diff --git a/samples/03.welcome-user/bots/__init__.py b/samples/03.welcome-user/bots/__init__.py deleted file mode 100644 index 4f3e70d59..000000000 --- a/samples/03.welcome-user/bots/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .welcome_user_bot import WelcomeUserBot - -__all__ = ["WelcomeUserBot"] diff --git a/samples/03.welcome-user/bots/welcome_user_bot.py b/samples/03.welcome-user/bots/welcome_user_bot.py deleted file mode 100644 index 9aa584732..000000000 --- a/samples/03.welcome-user/bots/welcome_user_bot.py +++ /dev/null @@ -1,143 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import ( - ActivityHandler, - TurnContext, - UserState, - CardFactory, - MessageFactory, -) -from botbuilder.schema import ( - ChannelAccount, - HeroCard, - CardImage, - CardAction, - ActionTypes, -) - -from data_models import WelcomeUserState - - -class WelcomeUserBot(ActivityHandler): - def __init__(self, user_state: UserState): - if user_state is None: - raise TypeError( - "[WelcomeUserBot]: Missing parameter. user_state is required but None was given" - ) - - self.user_state = user_state - - self.user_state_accessor = self.user_state.create_property("WelcomeUserState") - - self.WELCOME_MESSAGE = """This is a simple Welcome Bot sample. This bot will introduce you - to welcoming and greeting users. You can say 'intro' to see the - introduction card. If you are running this bot in the Bot Framework - Emulator, press the 'Restart Conversation' button to simulate user joining - a bot or a channel""" - - async def on_turn(self, turn_context: TurnContext): - await super().on_turn(turn_context) - - # save changes to WelcomeUserState after each turn - await self.user_state.save_changes(turn_context) - - async def on_members_added_activity( - self, members_added: [ChannelAccount], turn_context: TurnContext - ): - """ - Greet when users are added to the conversation. - Note that all channels do not send the conversation update activity. - If you find that this bot works in the emulator, but does not in - another channel the reason is most likely that the channel does not - send this activity. - """ - for member in members_added: - if member.id != turn_context.activity.recipient.id: - await turn_context.send_activity( - f"Hi there { member.name }. " + self.WELCOME_MESSAGE - ) - - await turn_context.send_activity( - """You are seeing this message because the bot received at least one - 'ConversationUpdate' event, indicating you (and possibly others) - joined the conversation. If you are using the emulator, pressing - the 'Start Over' button to trigger this event again. The specifics - of the 'ConversationUpdate' event depends on the channel. You can - read more information at: https://aka.ms/about-botframework-welcome-user""" - ) - - await turn_context.send_activity( - """It is a good pattern to use this event to send general greeting - to user, explaining what your bot can do. In this example, the bot - handles 'hello', 'hi', 'help' and 'intro'. Try it now, type 'hi'""" - ) - - async def on_message_activity(self, turn_context: TurnContext): - """ - Respond to messages sent from the user. - """ - # Get the state properties from the turn context. - welcome_user_state = await self.user_state_accessor.get( - turn_context, WelcomeUserState - ) - - if not welcome_user_state.did_welcome_user: - welcome_user_state.did_welcome_user = True - - await turn_context.send_activity( - "You are seeing this message because this was your first message ever to this bot." - ) - - name = turn_context.activity.from_property.name - await turn_context.send_activity( - f"It is a good practice to welcome the user and provide personal greeting. For example: Welcome {name}" - ) - - else: - # This example hardcodes specific utterances. You should use LUIS or QnA for more advance language - # understanding. - text = turn_context.activity.text.lower() - if text in ("hello", "hi"): - await turn_context.send_activity(f"You said { text }") - elif text in ("intro", "help"): - await self.__send_intro_card(turn_context) - else: - await turn_context.send_activity(self.WELCOME_MESSAGE) - - async def __send_intro_card(self, turn_context: TurnContext): - card = HeroCard( - title="Welcome to Bot Framework!", - text="Welcome to Welcome Users bot sample! This Introduction card " - "is a great way to introduce your Bot to the user and suggest " - "some things to get them started. We use this opportunity to " - "recommend a few next steps for learning more creating and deploying bots.", - images=[CardImage(url="https://aka.ms/bf-welcome-card-image")], - buttons=[ - CardAction( - type=ActionTypes.open_url, - title="Get an overview", - text="Get an overview", - display_text="Get an overview", - value="https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0", - ), - CardAction( - type=ActionTypes.open_url, - title="Ask a question", - text="Ask a question", - display_text="Ask a question", - value="https://stackoverflow.com/questions/tagged/botframework", - ), - CardAction( - type=ActionTypes.open_url, - title="Learn how to deploy", - text="Learn how to deploy", - display_text="Learn how to deploy", - value="https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-deploy-azure?view=azure-bot-service-4.0", - ), - ], - ) - - return await turn_context.send_activity( - MessageFactory.attachment(CardFactory.hero_card(card)) - ) diff --git a/samples/03.welcome-user/config.py b/samples/03.welcome-user/config.py deleted file mode 100644 index e007d0fa9..000000000 --- a/samples/03.welcome-user/config.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - -""" Bot Configuration """ - - -class DefaultConfig: - """ Bot Configuration """ - - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") diff --git a/samples/03.welcome-user/data_models/__init__.py b/samples/03.welcome-user/data_models/__init__.py deleted file mode 100644 index a7cd0686a..000000000 --- a/samples/03.welcome-user/data_models/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .welcome_user_state import WelcomeUserState - -__all__ = ["WelcomeUserState"] diff --git a/samples/03.welcome-user/data_models/welcome_user_state.py b/samples/03.welcome-user/data_models/welcome_user_state.py deleted file mode 100644 index 7470d4378..000000000 --- a/samples/03.welcome-user/data_models/welcome_user_state.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -class WelcomeUserState: - def __init__(self, did_welcome: bool = False): - self.did_welcome_user = did_welcome diff --git a/samples/03.welcome-user/requirements.txt b/samples/03.welcome-user/requirements.txt deleted file mode 100644 index 7e54b62ec..000000000 --- a/samples/03.welcome-user/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -botbuilder-core>=4.4.0b1 -flask>=1.0.3 diff --git a/samples/05.multi-turn-prompt/README.md b/samples/05.multi-turn-prompt/README.md deleted file mode 100644 index 405a70f2a..000000000 --- a/samples/05.multi-turn-prompt/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# multi-turn prompt - -Bot Framework v4 welcome users bot sample - -This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to use the prompts classes included in `botbuilder-dialogs`. This bot will ask for the user's name and age, then store the responses. It demonstrates a multi-turn dialog flow using a text prompt, a number prompt, and state accessors to store and retrieve values. - -## Running the sample -- Clone the repository -```bash -git clone https://github.com/Microsoft/botbuilder-python.git -``` -- Run `pip install -r requirements.txt` to install all dependencies -- Run `python app.py` -- Alternatively to the last command, you can set the file in an environment variable with `set FLASK_APP=app.py` in windows (`export FLASK_APP=app.py` in mac/linux) and then run `flask run --host=127.0.0.1 --port=3978` - - -### Visual studio code -- Activate your desired virtual environment -- Open `botbuilder-python\samples\45.state-management` folder -- Bring up a terminal, navigate to `botbuilder-python\samples\05.multi-turn-prompt` folder -- In the terminal, type `pip install -r requirements.txt` -- In the terminal, type `python app.py` - -## Testing the bot using Bot Framework Emulator -[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to bot using Bot Framework Emulator -- Launch Bot Framework Emulator -- Paste this URL in the emulator window - http://localhost:3978/api/messages - - -## Prompts - -A conversation between a bot and a user often involves asking (prompting) the user for information, parsing the user's response, -and then acting on that information. This sample demonstrates how to prompt users for information using the different prompt types -included in the [botbuilder-dialogs](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0) library -and supported by the SDK. - -The `botbuilder-dialogs` library includes a variety of pre-built prompt classes, including text, number, and datetime types. This -sample demonstrates using a text prompt to collect the user's name, then using a number prompt to collect an age. - -# Further reading - -- [Bot Framework Documentation](https://docs.botframework.com) -- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) -- [Dialogs](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0) -- [Gathering Input Using Prompts](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-prompts?view=azure-bot-service-4.0&tabs=csharp) -- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) diff --git a/samples/05.multi-turn-prompt/app.py b/samples/05.multi-turn-prompt/app.py deleted file mode 100644 index fd68f6667..000000000 --- a/samples/05.multi-turn-prompt/app.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import asyncio -import sys -from datetime import datetime - -from flask import Flask, request, Response -from botbuilder.core import ( - BotFrameworkAdapter, - BotFrameworkAdapterSettings, - ConversationState, - MemoryStorage, - TurnContext, - UserState, -) -from botbuilder.schema import Activity, ActivityTypes - -from dialogs import UserProfileDialog -from bots import DialogBot - -# Create the loop and Flask app -LOOP = asyncio.get_event_loop() -APP = Flask(__name__, instance_relative_config=True) -APP.config.from_object("config.DefaultConfig") - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"]) -ADAPTER = BotFrameworkAdapter(SETTINGS) - - -# Catch-all for errors. -async def on_error(context: TurnContext, error: Exception): - # This check writes out errors to console log - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error]: { error }", file=sys.stderr) - - # Send a message to the user - await context.send_activity("The bot encountered an error or bug.") - await context.send_activity( - "To continue to run this bot, please fix the bot source code." - ) - # Send a trace activity if we're talking to the Bot Framework Emulator - if context.activity.channel_id == "emulator": - # Create a trace activity that contains the error object - trace_activity = Activity( - label="TurnError", - name="on_turn_error Trace", - timestamp=datetime.utcnow(), - type=ActivityTypes.trace, - value=f"{error}", - value_type="https://www.botframework.com/schemas/error", - ) - - # Send a trace activity, which will be displayed in Bot Framework Emulator - await context.send_activity(trace_activity) - - # Clear out state - await CONVERSATION_STATE.delete(context) - - -# Set the error handler on the Adapter. -# In this case, we want an unbound method, so MethodType is not needed. -ADAPTER.on_turn_error = on_error - -# Create MemoryStorage, UserState and ConversationState -MEMORY = MemoryStorage() -CONVERSATION_STATE = ConversationState(MEMORY) -USER_STATE = UserState(MEMORY) - -# create main dialog and bot -DIALOG = UserProfileDialog(USER_STATE) -BOT = DialogBot(CONVERSATION_STATE, USER_STATE, DIALOG) - - -# Listen for incoming requests on /api/messages. -@APP.route("/api/messages", methods=["POST"]) -def messages(): - # Main bot message handler.s - if "application/json" in request.headers["Content-Type"]: - body = request.json - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = ( - request.headers["Authorization"] if "Authorization" in request.headers else "" - ) - - try: - task = LOOP.create_task( - ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - ) - LOOP.run_until_complete(task) - return Response(status=201) - except Exception as exception: - raise exception - - -if __name__ == "__main__": - try: - APP.run(debug=False, port=APP.config["PORT"]) # nosec debug - except Exception as exception: - raise exception diff --git a/samples/05.multi-turn-prompt/bots/__init__.py b/samples/05.multi-turn-prompt/bots/__init__.py deleted file mode 100644 index 306aca22c..000000000 --- a/samples/05.multi-turn-prompt/bots/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .dialog_bot import DialogBot - -__all__ = ["DialogBot"] diff --git a/samples/05.multi-turn-prompt/bots/dialog_bot.py b/samples/05.multi-turn-prompt/bots/dialog_bot.py deleted file mode 100644 index c66d73755..000000000 --- a/samples/05.multi-turn-prompt/bots/dialog_bot.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import ActivityHandler, ConversationState, TurnContext, UserState -from botbuilder.dialogs import Dialog -from helpers.dialog_helper import DialogHelper - - -class DialogBot(ActivityHandler): - """ - This Bot implementation can run any type of Dialog. The use of type parameterization is to allows multiple - different bots to be run at different endpoints within the same project. This can be achieved by defining distinct - Controller types each with dependency on distinct Bot types. The ConversationState is used by the Dialog system. The - UserState isn't, however, it might have been used in a Dialog implementation, and the requirement is that all - BotState objects are saved at the end of a turn. - """ - - def __init__( - self, - conversation_state: ConversationState, - user_state: UserState, - dialog: Dialog, - ): - if conversation_state is None: - raise TypeError( - "[DialogBot]: Missing parameter. conversation_state is required but None was given" - ) - if user_state is None: - raise TypeError( - "[DialogBot]: Missing parameter. user_state is required but None was given" - ) - if dialog is None: - raise Exception("[DialogBot]: Missing parameter. dialog is required") - - self.conversation_state = conversation_state - self.user_state = user_state - self.dialog = dialog - - async def on_turn(self, turn_context: TurnContext): - await super().on_turn(turn_context) - - # Save any state changes that might have ocurred during the turn. - await self.conversation_state.save_changes(turn_context) - await self.user_state.save_changes(turn_context) - - async def on_message_activity(self, turn_context: TurnContext): - await DialogHelper.run_dialog( - self.dialog, - turn_context, - self.conversation_state.create_property("DialogState"), - ) diff --git a/samples/05.multi-turn-prompt/config.py b/samples/05.multi-turn-prompt/config.py deleted file mode 100644 index e007d0fa9..000000000 --- a/samples/05.multi-turn-prompt/config.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - -""" Bot Configuration """ - - -class DefaultConfig: - """ Bot Configuration """ - - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") diff --git a/samples/05.multi-turn-prompt/data_models/__init__.py b/samples/05.multi-turn-prompt/data_models/__init__.py deleted file mode 100644 index 35a5934d4..000000000 --- a/samples/05.multi-turn-prompt/data_models/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .user_profile import UserProfile - -__all__ = ["UserProfile"] diff --git a/samples/05.multi-turn-prompt/data_models/user_profile.py b/samples/05.multi-turn-prompt/data_models/user_profile.py deleted file mode 100644 index efdc77eeb..000000000 --- a/samples/05.multi-turn-prompt/data_models/user_profile.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -""" - This is our application state. Just a regular serializable Python class. -""" - - -class UserProfile: - def __init__(self, name: str = None, transport: str = None, age: int = 0): - self.name = name - self.transport = transport - self.age = age diff --git a/samples/05.multi-turn-prompt/dialogs/__init__.py b/samples/05.multi-turn-prompt/dialogs/__init__.py deleted file mode 100644 index 2de723d58..000000000 --- a/samples/05.multi-turn-prompt/dialogs/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .user_profile_dialog import UserProfileDialog - -__all__ = ["UserProfileDialog"] diff --git a/samples/05.multi-turn-prompt/dialogs/user_profile_dialog.py b/samples/05.multi-turn-prompt/dialogs/user_profile_dialog.py deleted file mode 100644 index dad1f6d18..000000000 --- a/samples/05.multi-turn-prompt/dialogs/user_profile_dialog.py +++ /dev/null @@ -1,167 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.dialogs import ( - ComponentDialog, - WaterfallDialog, - WaterfallStepContext, - DialogTurnResult, -) -from botbuilder.dialogs.prompts import ( - TextPrompt, - NumberPrompt, - ChoicePrompt, - ConfirmPrompt, - PromptOptions, - PromptValidatorContext, -) -from botbuilder.dialogs.choices import Choice -from botbuilder.core import MessageFactory, UserState - -from data_models import UserProfile - - -class UserProfileDialog(ComponentDialog): - def __init__(self, user_state: UserState): - super(UserProfileDialog, self).__init__(UserProfileDialog.__name__) - - self.user_profile_accessor = user_state.create_property("UserProfile") - - self.add_dialog( - WaterfallDialog( - WaterfallDialog.__name__, - [ - self.transport_step, - self.name_step, - self.name_confirm_step, - self.age_step, - self.confirm_step, - self.summary_step, - ], - ) - ) - self.add_dialog(TextPrompt(TextPrompt.__name__)) - self.add_dialog( - NumberPrompt(NumberPrompt.__name__, UserProfileDialog.age_prompt_validator) - ) - self.add_dialog(ChoicePrompt(ChoicePrompt.__name__)) - self.add_dialog(ConfirmPrompt(ConfirmPrompt.__name__)) - - self.initial_dialog_id = WaterfallDialog.__name__ - - async def transport_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - # WaterfallStep always finishes with the end of the Waterfall or with another dialog; - # here it is a Prompt Dialog. Running a prompt here means the next WaterfallStep will - # be run when the users response is received. - return await step_context.prompt( - ChoicePrompt.__name__, - PromptOptions( - prompt=MessageFactory.text("Please enter your mode of transport."), - choices=[Choice("Car"), Choice("Bus"), Choice("Bicycle")], - ), - ) - - async def name_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - step_context.values["transport"] = step_context.result.value - - return await step_context.prompt( - TextPrompt.__name__, - PromptOptions(prompt=MessageFactory.text("Please enter your name.")), - ) - - async def name_confirm_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - step_context.values["name"] = step_context.result - - # We can send messages to the user at any point in the WaterfallStep. - await step_context.context.send_activity( - MessageFactory.text(f"Thanks {step_context.result}") - ) - - # WaterfallStep always finishes with the end of the Waterfall or - # with another dialog; here it is a Prompt Dialog. - return await step_context.prompt( - ConfirmPrompt.__name__, - PromptOptions( - prompt=MessageFactory.text("Would you like to give your age?") - ), - ) - - async def age_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - if step_context.result: - # User said "yes" so we will be prompting for the age. - # WaterfallStep always finishes with the end of the Waterfall or with another dialog, - # here it is a Prompt Dialog. - return await step_context.prompt( - NumberPrompt.__name__, - PromptOptions( - prompt=MessageFactory.text("Please enter your age."), - retry_prompt=MessageFactory.text( - "The value entered must be greater than 0 and less than 150." - ), - ), - ) - - # User said "no" so we will skip the next step. Give -1 as the age. - return await step_context.next(-1) - - async def confirm_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - age = step_context.result - step_context.values["age"] = step_context.result - - msg = ( - "No age given." - if step_context.result == -1 - else f"I have your age as {age}." - ) - - # We can send messages to the user at any point in the WaterfallStep. - await step_context.context.send_activity(MessageFactory.text(msg)) - - # WaterfallStep always finishes with the end of the Waterfall or - # with another dialog; here it is a Prompt Dialog. - return await step_context.prompt( - ConfirmPrompt.__name__, - PromptOptions(prompt=MessageFactory.text("Is this ok?")), - ) - - async def summary_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - if step_context.result: - # Get the current profile object from user state. Changes to it - # will saved during Bot.on_turn. - user_profile = await self.user_profile_accessor.get( - step_context.context, UserProfile - ) - - user_profile.transport = step_context.values["transport"] - user_profile.name = step_context.values["name"] - user_profile.age = step_context.values["age"] - - msg = f"I have your mode of transport as {user_profile.transport} and your name as {user_profile.name}." - if user_profile.age != -1: - msg += f" And age as {user_profile.age}." - - await step_context.context.send_activity(MessageFactory.text(msg)) - else: - await step_context.context.send_activity( - MessageFactory.text("Thanks. Your profile will not be kept.") - ) - - # WaterfallStep always finishes with the end of the Waterfall or with another - # dialog, here it is the end. - return await step_context.end_dialog() - - @staticmethod - async def age_prompt_validator(prompt_context: PromptValidatorContext) -> bool: - # This condition is our validation rule. You can also change the value at this point. - return ( - prompt_context.recognized.succeeded - and 0 < prompt_context.recognized.value < 150 - ) diff --git a/samples/05.multi-turn-prompt/helpers/__init__.py b/samples/05.multi-turn-prompt/helpers/__init__.py deleted file mode 100644 index a824eb8f4..000000000 --- a/samples/05.multi-turn-prompt/helpers/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from . import dialog_helper - -__all__ = ["dialog_helper"] diff --git a/samples/05.multi-turn-prompt/helpers/dialog_helper.py b/samples/05.multi-turn-prompt/helpers/dialog_helper.py deleted file mode 100644 index 6b2646b0b..000000000 --- a/samples/05.multi-turn-prompt/helpers/dialog_helper.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import StatePropertyAccessor, TurnContext -from botbuilder.dialogs import Dialog, DialogSet, DialogTurnStatus - - -class DialogHelper: - @staticmethod - async def run_dialog( - dialog: Dialog, turn_context: TurnContext, accessor: StatePropertyAccessor - ): - dialog_set = DialogSet(accessor) - dialog_set.add(dialog) - - dialog_context = await dialog_set.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dialog_context.begin_dialog(dialog.id) diff --git a/samples/05.multi-turn-prompt/requirements.txt b/samples/05.multi-turn-prompt/requirements.txt deleted file mode 100644 index 676447d22..000000000 --- a/samples/05.multi-turn-prompt/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -botbuilder-dialogs>=4.4.0.b1 -botbuilder-ai>=4.4.0.b1 -flask>=1.0.3 - diff --git a/samples/06.using-cards/README.md b/samples/06.using-cards/README.md deleted file mode 100644 index 7a0b31b06..000000000 --- a/samples/06.using-cards/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# Using Cards Bot - -Bot Framework v4 using cards bot sample - -This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a bot that uses rich cards to enhance your bot design. - -## PREREQUISITES -- Python 3.7 or above - -## Running the sample -- Clone the repository -```bash -git clone https://github.com/Microsoft/botbuilder-python.git -``` -- Run `pip install -r requirements.txt` to install all dependencies -- Run `python app.py` -- Alternatively to the last command, you can set the file in an environment variable with `set FLASK_APP=app.py` in windows (`export FLASK_APP=app.py` in mac/linux) and then run `flask run --host=127.0.0.1 --port=3978` - -### Visual studio code -- Activate your desired virtual environment -- Open `botbuilder-python\samples\06.using-cards` folder -- Bring up a terminal, navigate to `botbuilder-python\samples\06.using-cards` folder -- In the terminal, type `pip install -r requirements.txt` -- In the terminal, type `python app.py` - - -## Testing the bot using Bot Framework Emulator -[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to bot using Bot Framework Emulator -- Launch Bot Framework Emulator -- Paste this URL in the emulator window - http://localhost:3978/api/messages - -# Adding media to messages -A message exchange between user and bot can contain media attachments, such as cards, images, video, audio, and files. - -There are several different card types supported by Bot Framework including: -- [Adaptive card](http://adaptivecards.io) -- [Hero card](https://docs.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-api-reference?view=azure-bot-service-4.0#herocard-object) -- [Thumbnail card](https://docs.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-api-reference?view=azure-bot-service-4.0#thumbnailcard-object) -- [More...](https://docs.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-add-rich-cards?view=azure-bot-service-4.0) - -# Further reading - -- [Azure Bot Service Introduction](https://docs.microsoft.com/en-us/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) -- [Bot State](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-storage-concept?view=azure-bot-service-4.0) -- [Add media to messages](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-add-media-attachments?view=azure-bot-service-4.0&tabs=csharp) -- [Rich card types](https://docs.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-add-rich-cards?view=azure-bot-service-4.0) diff --git a/samples/06.using-cards/app.py b/samples/06.using-cards/app.py deleted file mode 100644 index 257474898..000000000 --- a/samples/06.using-cards/app.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -""" -This sample shows how to use different types of rich cards. -""" -import asyncio -import sys -from datetime import datetime - -from flask import Flask, request, Response -from botbuilder.core import ( - BotFrameworkAdapter, - BotFrameworkAdapterSettings, - ConversationState, - MemoryStorage, - TurnContext, - UserState, -) -from botbuilder.schema import Activity, ActivityTypes - -from dialogs import MainDialog -from bots import RichCardsBot - -# Create the loop and Flask app -LOOP = asyncio.get_event_loop() -APP = Flask(__name__, instance_relative_config=True) -APP.config.from_object("config.DefaultConfig") - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"]) -ADAPTER = BotFrameworkAdapter(SETTINGS) - - -# Catch-all for errors. -async def on_error(context: TurnContext, error: Exception): - # This check writes out errors to console log .vs. app insights. - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - - # Send a message to the user - await context.send_activity("The bot encountered an error or bug.") - await context.send_activity( - "To continue to run this bot, please fix the bot source code." - ) - # Send a trace activity if we're talking to the Bot Framework Emulator - if context.activity.channel_id == "emulator": - # Create a trace activity that contains the error object - trace_activity = Activity( - label="TurnError", - name="on_turn_error Trace", - timestamp=datetime.utcnow(), - type=ActivityTypes.trace, - value=f"{error}", - value_type="https://www.botframework.com/schemas/error", - ) - # Send a trace activity, which will be displayed in Bot Framework Emulator - await context.send_activity(trace_activity) - - # Clear out state - await CONVERSATION_STATE.delete(context) - - -# Set the error handler on the Adapter. -# In this case, we want an unbound method, so MethodType is not needed. -ADAPTER.on_turn_error = on_error - -# Create MemoryStorage, UserState and ConversationState -MEMORY = MemoryStorage() -USER_STATE = UserState(MEMORY) -CONVERSATION_STATE = ConversationState(MEMORY) - -# Create dialog and Bot -DIALOG = MainDialog() -BOT = RichCardsBot(CONVERSATION_STATE, USER_STATE, DIALOG) - - -# Listen for incoming requests on /api/messages. -@APP.route("/api/messages", methods=["POST"]) -def messages(): - # Main bot message handler.s - if "application/json" in request.headers["Content-Type"]: - body = request.json - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = ( - request.headers["Authorization"] if "Authorization" in request.headers else "" - ) - - try: - task = LOOP.create_task( - ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - ) - LOOP.run_until_complete(task) - return Response(status=201) - except Exception as exception: - raise exception - - -if __name__ == "__main__": - try: - APP.run(debug=False, port=APP.config["PORT"]) # nosec debug - except Exception as exception: - raise exception diff --git a/samples/06.using-cards/bots/__init__.py b/samples/06.using-cards/bots/__init__.py deleted file mode 100644 index 393acb3e7..000000000 --- a/samples/06.using-cards/bots/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .rich_cards_bot import RichCardsBot - -__all__ = ["RichCardsBot"] diff --git a/samples/06.using-cards/bots/dialog_bot.py b/samples/06.using-cards/bots/dialog_bot.py deleted file mode 100644 index ff4473e85..000000000 --- a/samples/06.using-cards/bots/dialog_bot.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from helpers.dialog_helper import DialogHelper -from botbuilder.core import ActivityHandler, ConversationState, UserState, TurnContext -from botbuilder.dialogs import Dialog - - -class DialogBot(ActivityHandler): - def __init__( - self, - conversation_state: ConversationState, - user_state: UserState, - dialog: Dialog, - ): - if conversation_state is None: - raise Exception( - "[DialogBot]: Missing parameter. conversation_state is required" - ) - if user_state is None: - raise Exception("[DialogBot]: Missing parameter. user_state is required") - if dialog is None: - raise Exception("[DialogBot]: Missing parameter. dialog is required") - - self.conversation_state = conversation_state - self.user_state = user_state - self.dialog = dialog - self.dialog_state = self.conversation_state.create_property("DialogState") - - async def on_turn(self, turn_context: TurnContext): - await super().on_turn(turn_context) - - # Save any state changes that might have occured during the turn. - await self.conversation_state.save_changes(turn_context, False) - await self.user_state.save_changes(turn_context, False) - - async def on_message_activity(self, turn_context: TurnContext): - await DialogHelper.run_dialog( - self.dialog, - turn_context, - self.conversation_state.create_property("DialogState"), - ) diff --git a/samples/06.using-cards/bots/rich_cards_bot.py b/samples/06.using-cards/bots/rich_cards_bot.py deleted file mode 100644 index 54da137db..000000000 --- a/samples/06.using-cards/bots/rich_cards_bot.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import MessageFactory, TurnContext -from botbuilder.schema import ChannelAccount -from .dialog_bot import DialogBot - - -class RichCardsBot(DialogBot): - """ - RichCardsBot prompts a user to select a Rich Card and then returns the card - that matches the user's selection. - """ - - def __init__(self, conversation_state, user_state, dialog): - super().__init__(conversation_state, user_state, dialog) - - async def on_members_added_activity( - self, members_added: ChannelAccount, turn_context: TurnContext - ): - for member in members_added: - if member.id != turn_context.activity.recipient.id: - reply = MessageFactory.text( - "Welcome to CardBot. " - + "This bot will show you different types of Rich Cards. " - + "Please type anything to get started." - ) - await turn_context.send_activity(reply) diff --git a/samples/06.using-cards/config.py b/samples/06.using-cards/config.py deleted file mode 100644 index e007d0fa9..000000000 --- a/samples/06.using-cards/config.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - -""" Bot Configuration """ - - -class DefaultConfig: - """ Bot Configuration """ - - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") diff --git a/samples/06.using-cards/dialogs/__init__.py b/samples/06.using-cards/dialogs/__init__.py deleted file mode 100644 index 74d870b7c..000000000 --- a/samples/06.using-cards/dialogs/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .main_dialog import MainDialog - -__all__ = ["MainDialog"] diff --git a/samples/06.using-cards/dialogs/main_dialog.py b/samples/06.using-cards/dialogs/main_dialog.py deleted file mode 100644 index 9490933e7..000000000 --- a/samples/06.using-cards/dialogs/main_dialog.py +++ /dev/null @@ -1,298 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import CardFactory, MessageFactory -from botbuilder.dialogs import ( - ComponentDialog, - WaterfallDialog, - WaterfallStepContext, -) -from botbuilder.dialogs.prompts import TextPrompt, PromptOptions -from botbuilder.schema import ( - ActionTypes, - Attachment, - AnimationCard, - AudioCard, - HeroCard, - VideoCard, - ReceiptCard, - SigninCard, - ThumbnailCard, - MediaUrl, - CardAction, - CardImage, - ThumbnailUrl, - Fact, - ReceiptItem, -) - -from helpers.activity_helper import create_activity_reply -from .resources.adaptive_card_example import ADAPTIVE_CARD_CONTENT - -MAIN_WATERFALL_DIALOG = "mainWaterfallDialog" - - -class MainDialog(ComponentDialog): - def __init__(self): - super().__init__("MainDialog") - - # Define the main dialog and its related components. - self.add_dialog(TextPrompt("TextPrompt")) - self.add_dialog( - WaterfallDialog( - MAIN_WATERFALL_DIALOG, [self.choice_card_step, self.show_card_step] - ) - ) - - # The initial child Dialog to run. - self.initial_dialog_id = MAIN_WATERFALL_DIALOG - - async def choice_card_step(self, step_context: WaterfallStepContext): - """ - 1. Prompts the user if the user is not in the middle of a dialog. - 2. Re-prompts the user when an invalid input is received. - """ - menu_text = ( - "Which card would you like to see?\n" - "(1) Adaptive Card\n" - "(2) Animation Card\n" - "(3) Audio Card\n" - "(4) Hero Card\n" - "(5) Receipt Card\n" - "(6) Signin Card\n" - "(7) Thumbnail Card\n" - "(8) Video Card\n" - "(9) All Cards" - ) - - # Prompt the user with the configured PromptOptions. - return await step_context.prompt( - "TextPrompt", PromptOptions(prompt=MessageFactory.text(menu_text)) - ) - - async def show_card_step(self, step_context: WaterfallStepContext): - """ - Send a Rich Card response to the user based on their choice. - self method is only called when a valid prompt response is parsed from the user's - response to the ChoicePrompt. - """ - response = step_context.result.lower().strip() - choice_dict = { - "1": [self.create_adaptive_card], - "adaptive card": [self.create_adaptive_card], - "2": [self.create_animation_card], - "animation card": [self.create_animation_card], - "3": [self.create_audio_card], - "audio card": [self.create_audio_card], - "4": [self.create_hero_card], - "hero card": [self.create_hero_card], - "5": [self.create_receipt_card], - "receipt card": [self.create_receipt_card], - "6": [self.create_signin_card], - "signin card": [self.create_signin_card], - "7": [self.create_thumbnail_card], - "thumbnail card": [self.create_thumbnail_card], - "8": [self.create_video_card], - "video card": [self.create_video_card], - "9": [ - self.create_adaptive_card, - self.create_animation_card, - self.create_audio_card, - self.create_hero_card, - self.create_receipt_card, - self.create_signin_card, - self.create_thumbnail_card, - self.create_video_card, - ], - "all cards": [ - self.create_adaptive_card, - self.create_animation_card, - self.create_audio_card, - self.create_hero_card, - self.create_receipt_card, - self.create_signin_card, - self.create_thumbnail_card, - self.create_video_card, - ], - } - - # Get the functions that will generate the card(s) for our response - # If the stripped response from the user is not found in our choice_dict, default to None - choice = choice_dict.get(response, None) - # If the user's choice was not found, respond saying the bot didn't understand the user's response. - if not choice: - not_found = create_activity_reply( - step_context.context.activity, "Sorry, I didn't understand that. :(" - ) - await step_context.context.send_activity(not_found) - else: - for func in choice: - card = func() - response = create_activity_reply( - step_context.context.activity, "", "", [card] - ) - await step_context.context.send_activity(response) - - # Give the user instructions about what to do next - await step_context.context.send_activity("Type anything to see another card.") - - return await step_context.end_dialog() - - # ====================================== - # Helper functions used to create cards. - # ====================================== - - # Methods to generate cards - def create_adaptive_card(self) -> Attachment: - return CardFactory.adaptive_card(ADAPTIVE_CARD_CONTENT) - - def create_animation_card(self) -> Attachment: - card = AnimationCard( - media=[MediaUrl(url="http://i.giphy.com/Ki55RUbOV5njy.gif")], - title="Microsoft Bot Framework", - subtitle="Animation Card", - ) - return CardFactory.animation_card(card) - - def create_audio_card(self) -> Attachment: - card = AudioCard( - media=[MediaUrl(url="http://www.wavlist.com/movies/004/father.wav")], - title="I am your father", - subtitle="Star Wars: Episode V - The Empire Strikes Back", - text="The Empire Strikes Back (also known as Star Wars: Episode V – The Empire Strikes " - "Back) is a 1980 American epic space opera film directed by Irvin Kershner. Leigh " - "Brackett and Lawrence Kasdan wrote the screenplay, with George Lucas writing the " - "film's story and serving as executive producer. The second installment in the " - "original Star Wars trilogy, it was produced by Gary Kurtz for Lucasfilm Ltd. and " - "stars Mark Hamill, Harrison Ford, Carrie Fisher, Billy Dee Williams, Anthony " - "Daniels, David Prowse, Kenny Baker, Peter Mayhew and Frank Oz.", - image=ThumbnailUrl( - url="https://upload.wikimedia.org/wikipedia/en/3/3c/SW_-_Empire_Strikes_Back.jpg" - ), - buttons=[ - CardAction( - type=ActionTypes.open_url, - title="Read more", - value="https://en.wikipedia.org/wiki/The_Empire_Strikes_Back", - ) - ], - ) - return CardFactory.audio_card(card) - - def create_hero_card(self) -> Attachment: - card = HeroCard( - title="", - images=[ - CardImage( - url="https://sec.ch9.ms/ch9/7ff5/e07cfef0-aa3b-40bb-9baa-7c9ef8ff7ff5/buildreactionbotframework_960.jpg" - ) - ], - buttons=[ - CardAction( - type=ActionTypes.open_url, - title="Get Started", - value="https://docs.microsoft.com/en-us/azure/bot-service/", - ) - ], - ) - return CardFactory.hero_card(card) - - def create_video_card(self) -> Attachment: - card = VideoCard( - title="Big Buck Bunny", - subtitle="by the Blender Institute", - text="Big Buck Bunny (code-named Peach) is a short computer-animated comedy film by the Blender " - "Institute, part of the Blender Foundation. Like the foundation's previous film Elephants " - "Dream, the film was made using Blender, a free software application for animation made by " - "the same foundation. It was released as an open-source film under Creative Commons License " - "Attribution 3.0.", - media=[ - MediaUrl( - url="http://download.blender.org/peach/bigbuckbunny_movies/" - "BigBuckBunny_320x180.mp4" - ) - ], - buttons=[ - CardAction( - type=ActionTypes.open_url, - title="Learn More", - value="https://peach.blender.org/", - ) - ], - ) - return CardFactory.video_card(card) - - def create_receipt_card(self) -> Attachment: - card = ReceiptCard( - title="John Doe", - facts=[ - Fact(key="Order Number", value="1234"), - Fact(key="Payment Method", value="VISA 5555-****"), - ], - items=[ - ReceiptItem( - title="Data Transfer", - price="$38.45", - quantity="368", - image=CardImage( - url="https://github.com/amido/azure-vector-icons/raw/master/" - "renders/traffic-manager.png" - ), - ), - ReceiptItem( - title="App Service", - price="$45.00", - quantity="720", - image=CardImage( - url="https://github.com/amido/azure-vector-icons/raw/master/" - "renders/cloud-service.png" - ), - ), - ], - tax="$7.50", - total="90.95", - buttons=[ - CardAction( - type=ActionTypes.open_url, - title="More Information", - value="https://azure.microsoft.com/en-us/pricing/details/bot-service/", - ) - ], - ) - return CardFactory.receipt_card(card) - - def create_signin_card(self) -> Attachment: - card = SigninCard( - text="BotFramework Sign-in Card", - buttons=[ - CardAction( - type=ActionTypes.signin, - title="Sign-in", - value="https://login.microsoftonline.com", - ) - ], - ) - return CardFactory.signin_card(card) - - def create_thumbnail_card(self) -> Attachment: - card = ThumbnailCard( - title="BotFramework Thumbnail Card", - subtitle="Your bots — wherever your users are talking", - text="Build and connect intelligent bots to interact with your users naturally wherever" - " they are, from text/sms to Skype, Slack, Office 365 mail and other popular services.", - images=[ - CardImage( - url="https://sec.ch9.ms/ch9/7ff5/" - "e07cfef0-aa3b-40bb-9baa-7c9ef8ff7ff5/" - "buildreactionbotframework_960.jpg" - ) - ], - buttons=[ - CardAction( - type=ActionTypes.open_url, - title="Get Started", - value="https://docs.microsoft.com/en-us/azure/bot-service/", - ) - ], - ) - return CardFactory.thumbnail_card(card) diff --git a/samples/06.using-cards/dialogs/resources/__init__.py b/samples/06.using-cards/dialogs/resources/__init__.py deleted file mode 100644 index 7569a0e37..000000000 --- a/samples/06.using-cards/dialogs/resources/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from . import adaptive_card_example - -__all__ = ["adaptive_card_example"] diff --git a/samples/06.using-cards/dialogs/resources/adaptive_card_example.py b/samples/06.using-cards/dialogs/resources/adaptive_card_example.py deleted file mode 100644 index 49cf269b8..000000000 --- a/samples/06.using-cards/dialogs/resources/adaptive_card_example.py +++ /dev/null @@ -1,186 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Example content for an AdaptiveCard.""" - -ADAPTIVE_CARD_CONTENT = { - "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", - "version": "1.0", - "type": "AdaptiveCard", - "speak": "Your flight is confirmed for you and 3 other passengers from San Francisco to Amsterdam on Friday, October 10 8:30 AM", - "body": [ - { - "type": "TextBlock", - "text": "Passengers", - "weight": "bolder", - "isSubtle": False, - }, - {"type": "TextBlock", "text": "Sarah Hum", "separator": True}, - {"type": "TextBlock", "text": "Jeremy Goldberg", "spacing": "none"}, - {"type": "TextBlock", "text": "Evan Litvak", "spacing": "none"}, - { - "type": "TextBlock", - "text": "2 Stops", - "weight": "bolder", - "spacing": "medium", - }, - { - "type": "TextBlock", - "text": "Fri, October 10 8:30 AM", - "weight": "bolder", - "spacing": "none", - }, - { - "type": "ColumnSet", - "separator": True, - "columns": [ - { - "type": "Column", - "width": 1, - "items": [ - { - "type": "TextBlock", - "text": "San Francisco", - "isSubtle": True, - }, - { - "type": "TextBlock", - "size": "extraLarge", - "color": "accent", - "text": "SFO", - "spacing": "none", - }, - ], - }, - { - "type": "Column", - "width": "auto", - "items": [ - {"type": "TextBlock", "text": " "}, - { - "type": "Image", - "url": "http://messagecardplayground.azurewebsites.net/assets/airplane.png", - "size": "small", - "spacing": "none", - }, - ], - }, - { - "type": "Column", - "width": 1, - "items": [ - { - "type": "TextBlock", - "horizontalAlignment": "right", - "text": "Amsterdam", - "isSubtle": True, - }, - { - "type": "TextBlock", - "horizontalAlignment": "right", - "size": "extraLarge", - "color": "accent", - "text": "AMS", - "spacing": "none", - }, - ], - }, - ], - }, - { - "type": "TextBlock", - "text": "Non-Stop", - "weight": "bolder", - "spacing": "medium", - }, - { - "type": "TextBlock", - "text": "Fri, October 18 9:50 PM", - "weight": "bolder", - "spacing": "none", - }, - { - "type": "ColumnSet", - "separator": True, - "columns": [ - { - "type": "Column", - "width": 1, - "items": [ - {"type": "TextBlock", "text": "Amsterdam", "isSubtle": True}, - { - "type": "TextBlock", - "size": "extraLarge", - "color": "accent", - "text": "AMS", - "spacing": "none", - }, - ], - }, - { - "type": "Column", - "width": "auto", - "items": [ - {"type": "TextBlock", "text": " "}, - { - "type": "Image", - "url": "http://messagecardplayground.azurewebsites.net/assets/airplane.png", - "size": "small", - "spacing": "none", - }, - ], - }, - { - "type": "Column", - "width": 1, - "items": [ - { - "type": "TextBlock", - "horizontalAlignment": "right", - "text": "San Francisco", - "isSubtle": True, - }, - { - "type": "TextBlock", - "horizontalAlignment": "right", - "size": "extraLarge", - "color": "accent", - "text": "SFO", - "spacing": "none", - }, - ], - }, - ], - }, - { - "type": "ColumnSet", - "spacing": "medium", - "columns": [ - { - "type": "Column", - "width": "1", - "items": [ - { - "type": "TextBlock", - "text": "Total", - "size": "medium", - "isSubtle": True, - } - ], - }, - { - "type": "Column", - "width": 1, - "items": [ - { - "type": "TextBlock", - "horizontalAlignment": "right", - "text": "$4,032.54", - "size": "medium", - "weight": "bolder", - } - ], - }, - ], - }, - ], -} diff --git a/samples/06.using-cards/helpers/__init__.py b/samples/06.using-cards/helpers/__init__.py deleted file mode 100644 index 135279f61..000000000 --- a/samples/06.using-cards/helpers/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from . import activity_helper, dialog_helper - -__all__ = ["activity_helper", "dialog_helper"] diff --git a/samples/06.using-cards/helpers/activity_helper.py b/samples/06.using-cards/helpers/activity_helper.py deleted file mode 100644 index 354317c3e..000000000 --- a/samples/06.using-cards/helpers/activity_helper.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from datetime import datetime -from botbuilder.schema import ( - Activity, - ActivityTypes, - ChannelAccount, - ConversationAccount, -) - - -def create_activity_reply( - activity: Activity, - text: str = None, - locale: str = None, - attachments=None, -): - if attachments is None: - attachments = [] - attachments_aux = attachments.copy() - - return Activity( - type=ActivityTypes.message, - timestamp=datetime.utcnow(), - from_property=ChannelAccount( - id=getattr(activity.recipient, "id", None), - name=getattr(activity.recipient, "name", None), - ), - recipient=ChannelAccount( - id=activity.from_property.id, name=activity.from_property.name - ), - reply_to_id=activity.id, - service_url=activity.service_url, - channel_id=activity.channel_id, - conversation=ConversationAccount( - is_group=activity.conversation.is_group, - id=activity.conversation.id, - name=activity.conversation.name, - ), - text=text or "", - locale=locale or "", - attachments=attachments_aux, - entities=[], - ) diff --git a/samples/06.using-cards/helpers/dialog_helper.py b/samples/06.using-cards/helpers/dialog_helper.py deleted file mode 100644 index 6b2646b0b..000000000 --- a/samples/06.using-cards/helpers/dialog_helper.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import StatePropertyAccessor, TurnContext -from botbuilder.dialogs import Dialog, DialogSet, DialogTurnStatus - - -class DialogHelper: - @staticmethod - async def run_dialog( - dialog: Dialog, turn_context: TurnContext, accessor: StatePropertyAccessor - ): - dialog_set = DialogSet(accessor) - dialog_set.add(dialog) - - dialog_context = await dialog_set.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dialog_context.begin_dialog(dialog.id) diff --git a/samples/06.using-cards/requirements.txt b/samples/06.using-cards/requirements.txt deleted file mode 100644 index e44abb535..000000000 --- a/samples/06.using-cards/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -botbuilder-core>=4.4.0b1 -botbuilder-dialogs>=4.4.0b1 -flask>=1.0.3 diff --git a/samples/08.suggested-actions/README.md b/samples/08.suggested-actions/README.md deleted file mode 100644 index 4e0e76ebb..000000000 --- a/samples/08.suggested-actions/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# suggested actions - -Bot Framework v4 using adaptive cards bot sample - -This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to use suggested actions. Suggested actions enable your bot to present buttons that the user can tap to provide input. - -## Running the sample -- Clone the repository -```bash -git clone https://github.com/Microsoft/botbuilder-python.git -``` -- Bring up a terminal, navigate to `botbuilder-python\samples\08.suggested-actions` folder -- In the terminal, type `pip install -r requirements.txt` -- In the terminal, type `python app.py` - -## Testing the bot using Bot Framework Emulator -[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to bot using Bot Framework Emulator -- Launch Bot Framework Emulator -- File -> Open Bot -- Paste this URL in the emulator window - http://localhost:3978/api/messages - -## Suggested actions - -Suggested actions enable your bot to present buttons that the user can tap to provide input. Suggested actions appear close to the composer and enhance user experience. diff --git a/samples/08.suggested-actions/app.py b/samples/08.suggested-actions/app.py deleted file mode 100644 index 1504563d3..000000000 --- a/samples/08.suggested-actions/app.py +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import asyncio -import sys -from datetime import datetime - -from flask import Flask, request, Response -from botbuilder.core import ( - BotFrameworkAdapterSettings, - BotFrameworkAdapter, - TurnContext, -) -from botbuilder.schema import Activity, ActivityTypes - -from bots import SuggestActionsBot - -# Create the loop and Flask app -LOOP = asyncio.get_event_loop() -APP = Flask(__name__, instance_relative_config=True) -APP.config.from_object("config.DefaultConfig") - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"]) -ADAPTER = BotFrameworkAdapter(SETTINGS) - - -# Catch-all for errors. -async def on_error(context: TurnContext, error: Exception): - # This check writes out errors to console log .vs. app insights. - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - - # Send a message to the user - await context.send_activity("The bot encountered an error or bug.") - await context.send_activity( - "To continue to run this bot, please fix the bot source code." - ) - # Send a trace activity if we're talking to the Bot Framework Emulator - if context.activity.channel_id == "emulator": - # Create a trace activity that contains the error object - trace_activity = Activity( - label="TurnError", - name="on_turn_error Trace", - timestamp=datetime.utcnow(), - type=ActivityTypes.trace, - value=f"{error}", - value_type="https://www.botframework.com/schemas/error", - ) - # Send a trace activity, which will be displayed in Bot Framework Emulator - await context.send_activity(trace_activity) - - -ADAPTER.on_turn_error = on_error - -# Create Bot -BOT = SuggestActionsBot() - - -# Listen for incoming requests on /api/messages. -@APP.route("/api/messages", methods=["POST"]) -def messages(): - # Main bot message handler. - if "application/json" in request.headers["Content-Type"]: - body = request.json - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = ( - request.headers["Authorization"] if "Authorization" in request.headers else "" - ) - - try: - task = LOOP.create_task( - ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - ) - LOOP.run_until_complete(task) - return Response(status=201) - except Exception as exception: - raise exception - - -if __name__ == "__main__": - try: - APP.run(debug=False, port=APP.config["PORT"]) # nosec debug - except Exception as exception: - raise exception diff --git a/samples/08.suggested-actions/bots/__init__.py b/samples/08.suggested-actions/bots/__init__.py deleted file mode 100644 index cbf771a32..000000000 --- a/samples/08.suggested-actions/bots/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .suggested_actions_bot import SuggestActionsBot - -__all__ = ["SuggestActionsBot"] diff --git a/samples/08.suggested-actions/bots/suggested_actions_bot.py b/samples/08.suggested-actions/bots/suggested_actions_bot.py deleted file mode 100644 index 3daa70d5e..000000000 --- a/samples/08.suggested-actions/bots/suggested_actions_bot.py +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import ActivityHandler, MessageFactory, TurnContext -from botbuilder.schema import ChannelAccount, CardAction, ActionTypes, SuggestedActions - - -class SuggestActionsBot(ActivityHandler): - """ - This bot will respond to the user's input with suggested actions. - Suggested actions enable your bot to present buttons that the user - can tap to provide input. - """ - - async def on_members_added_activity( - self, members_added: [ChannelAccount], turn_context: TurnContext - ): - """ - Send a welcome message to the user and tell them what actions they may perform to use this bot - """ - - return await self._send_welcome_message(turn_context) - - async def on_message_activity(self, turn_context: TurnContext): - """ - Respond to the users choice and display the suggested actions again. - """ - - text = turn_context.activity.text.lower() - response_text = self._process_input(text) - - await turn_context.send_activity(MessageFactory.text(response_text)) - - return await self._send_suggested_actions(turn_context) - - async def _send_welcome_message(self, turn_context: TurnContext): - for member in turn_context.activity.members_added: - if member.id != turn_context.activity.recipient.id: - await turn_context.send_activity( - MessageFactory.text( - f"Welcome to SuggestedActionsBot {member.name}." - f" This bot will introduce you to suggestedActions." - f" Please answer the question: " - ) - ) - - await self._send_suggested_actions(turn_context) - - def _process_input(self, text: str): - color_text = "is the best color, I agree." - - if text == "red": - return f"Red {color_text}" - - if text == "yellow": - return f"Yellow {color_text}" - - if text == "blue": - return f"Blue {color_text}" - - return "Please select a color from the suggested action choices" - - async def _send_suggested_actions(self, turn_context: TurnContext): - """ - Creates and sends an activity with suggested actions to the user. When the user - clicks one of the buttons the text value from the "CardAction" will be displayed - in the channel just as if the user entered the text. There are multiple - "ActionTypes" that may be used for different situations. - """ - - reply = MessageFactory.text("What is your favorite color?") - - reply.suggested_actions = SuggestedActions( - actions=[ - CardAction(title="Red", type=ActionTypes.im_back, value="Red"), - CardAction(title="Yellow", type=ActionTypes.im_back, value="Yellow"), - CardAction(title="Blue", type=ActionTypes.im_back, value="Blue"), - ] - ) - - return await turn_context.send_activity(reply) diff --git a/samples/08.suggested-actions/config.py b/samples/08.suggested-actions/config.py deleted file mode 100644 index e007d0fa9..000000000 --- a/samples/08.suggested-actions/config.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - -""" Bot Configuration """ - - -class DefaultConfig: - """ Bot Configuration """ - - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") diff --git a/samples/08.suggested-actions/requirements.txt b/samples/08.suggested-actions/requirements.txt deleted file mode 100644 index 7e54b62ec..000000000 --- a/samples/08.suggested-actions/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -botbuilder-core>=4.4.0b1 -flask>=1.0.3 diff --git a/samples/11.qnamaker/README.md b/samples/11.qnamaker/README.md deleted file mode 100644 index 27edff425..000000000 --- a/samples/11.qnamaker/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# QnA Maker - -Bot Framework v4 QnA Maker bot sample - -This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a bot that uses the [QnA Maker Cognitive AI](https://www.qnamaker.ai) service. - -The [QnA Maker Service](https://www.qnamaker.ai) enables you to build, train and publish a simple question and answer bot based on FAQ URLs, structured documents or editorial content in minutes. In this sample, we demonstrate how to use the QnA Maker service to answer questions based on a FAQ text file used as input. - -## Prerequisites - -This samples **requires** prerequisites in order to run. - -### Overview - -This bot uses [QnA Maker Service](https://www.qnamaker.ai), an AI based cognitive service, to implement simple Question and Answer conversational patterns. - -### Create a QnAMaker Application to enable QnA Knowledge Bases - -QnA knowledge base setup and application configuration steps can be found [here](https://aka.ms/qna-instructions). - -## Running the sample -- Clone the repository -```bash -git clone https://github.com/Microsoft/botbuilder-python.git -``` -- Activate your desired virtual environment -- Bring up a terminal, navigate to `botbuilder-python\samples\11.qnamaker` folder -- In the terminal, type `pip install -r requirements.txt` -- Update `QNA_KNOWLEDGEBASE_ID`, `QNA_ENDPOINT_KEY`, and `QNA_ENDPOINT_HOST` in `config.py` -- In the terminal, type `python app.py` - -## Testing the bot using Bot Framework Emulator -[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to bot using Bot Framework Emulator -- Launch Bot Framework Emulator -- File -> Open Bot -- Paste this URL in the emulator window - http://localhost:3978/api/messages - -## QnA Maker service - -QnA Maker enables you to power a question and answer service from your semi-structured content. - -One of the basic requirements in writing your own bot is to seed it with questions and answers. In many cases, the questions and answers already exist in content like FAQ URLs/documents, product manuals, etc. With QnA Maker, users can query your application in a natural, conversational manner. QnA Maker uses machine learning to extract relevant question-answer pairs from your content. It also uses powerful matching and ranking algorithms to provide the best possible match between the user query and the questions. - -## Further reading - -- [Bot Framework Documentation](https://docs.botframework.com) -- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) -- [QnA Maker Documentation](https://docs.microsoft.com/en-us/azure/cognitive-services/qnamaker/overview/overview) -- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) -- [QnA Maker CLI](https://github.com/Microsoft/botbuilder-tools/tree/master/packages/QnAMaker) -- [Channels and Bot Connector Service](https://docs.microsoft.com/en-us/azure/bot-service/bot-concepts?view=azure-bot-service-4.0) -- [Azure Portal](https://portal.azure.com) diff --git a/samples/11.qnamaker/app.py b/samples/11.qnamaker/app.py deleted file mode 100644 index 1f8c6f97f..000000000 --- a/samples/11.qnamaker/app.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import asyncio -import sys -from datetime import datetime - -from flask import Flask, request, Response -from botbuilder.core import BotFrameworkAdapterSettings, TurnContext, BotFrameworkAdapter -from botbuilder.schema import Activity, ActivityTypes - -from bots import QnABot - -# Create the loop and Flask app -LOOP = asyncio.get_event_loop() -app = Flask(__name__, instance_relative_config=True) -app.config.from_object("config.DefaultConfig") - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings(app.config["APP_ID"], app.config["APP_PASSWORD"]) -ADAPTER = BotFrameworkAdapter(SETTINGS) - - -# Catch-all for errors. -async def on_error(context: TurnContext, error: Exception): - # This check writes out errors to console log .vs. app insights. - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - - # Send a message to the user - await context.send_activity("The bot encountered an error or bug.") - await context.send_activity("To continue to run this bot, please fix the bot source code.") - # Send a trace activity if we're talking to the Bot Framework Emulator - if context.activity.channel_id == 'emulator': - # Create a trace activity that contains the error object - trace_activity = Activity( - label="TurnError", - name="on_turn_error Trace", - timestamp=datetime.utcnow(), - type=ActivityTypes.trace, - value=f"{error}", - value_type="https://www.botframework.com/schemas/error" - ) - # Send a trace activity, which will be displayed in Bot Framework Emulator - await context.send_activity(trace_activity) - -ADAPTER.on_turn_error = on_error - -# Create the Bot -BOT = QnABot(app.config) - -# Listen for incoming requests on /api/messages -@app.route("/api/messages", methods=["POST"]) -def messages(): - # Main bot message handler. - if "application/json" in request.headers["Content-Type"]: - body = request.json - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = ( - request.headers["Authorization"] if "Authorization" in request.headers else "" - ) - - try: - task = LOOP.create_task( - ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - ) - LOOP.run_until_complete(task) - return Response(status=201) - except Exception as exception: - raise exception - - -if __name__ == "__main__": - try: - app.run(debug=False, port=app.config["PORT"]) # nosec debug - except Exception as exception: - raise exception diff --git a/samples/11.qnamaker/bots/__init__.py b/samples/11.qnamaker/bots/__init__.py deleted file mode 100644 index 457940100..000000000 --- a/samples/11.qnamaker/bots/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .qna_bot import QnABot - -__all__ = ["QnABot"] diff --git a/samples/11.qnamaker/bots/qna_bot.py b/samples/11.qnamaker/bots/qna_bot.py deleted file mode 100644 index 8ff589e07..000000000 --- a/samples/11.qnamaker/bots/qna_bot.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from flask import Config - -from botbuilder.ai.qna import QnAMaker, QnAMakerEndpoint -from botbuilder.core import ActivityHandler, MessageFactory, TurnContext -from botbuilder.schema import ChannelAccount - - -class QnABot(ActivityHandler): - def __init__(self, config: Config): - self.qna_maker = QnAMaker( - QnAMakerEndpoint( - knowledge_base_id=config["QNA_KNOWLEDGEBASE_ID"], - endpoint_key=config["QNA_ENDPOINT_KEY"], - host=config["QNA_ENDPOINT_HOST"], - ) - ) - - async def on_members_added_activity( - self, members_added: [ChannelAccount], turn_context: TurnContext - ): - for member in members_added: - if member.id != turn_context.activity.recipient.id: - await turn_context.send_activity( - "Welcome to the QnA Maker sample! Ask me a question and I will try " - "to answer it." - ) - - async def on_message_activity(self, turn_context: TurnContext): - # The actual call to the QnA Maker service. - response = await self.qna_maker.get_answers(turn_context) - if response and len(response) > 0: - await turn_context.send_activity(MessageFactory.text(response[0].answer)) - else: - await turn_context.send_activity("No QnA Maker answers were found.") diff --git a/samples/11.qnamaker/cognitiveModels/smartLightFAQ.tsv b/samples/11.qnamaker/cognitiveModels/smartLightFAQ.tsv deleted file mode 100644 index 754118909..000000000 --- a/samples/11.qnamaker/cognitiveModels/smartLightFAQ.tsv +++ /dev/null @@ -1,15 +0,0 @@ -Question Answer Source Keywords -Question Answer 96207418-4609-48df-9cbc-dd35a71e83f7-KB.tsv Source -My Contoso smart light won't turn on. Check the connection to the wall outlet to make sure it's plugged in properly. 96207418-4609-48df-9cbc-dd35a71e83f7-KB.tsv Editorial -Light won't turn on. Check the connection to the wall outlet to make sure it's plugged in properly. 96207418-4609-48df-9cbc-dd35a71e83f7-KB.tsv Editorial -My smart light app stopped responding. Restart the app. If the problem persists, contact support. 96207418-4609-48df-9cbc-dd35a71e83f7-KB.tsv Editorial -How do I contact support? Email us at service@contoso.com 96207418-4609-48df-9cbc-dd35a71e83f7-KB.tsv Editorial -I need help. Email us at service@contoso.com 96207418-4609-48df-9cbc-dd35a71e83f7-KB.tsv Editorial -I upgraded the app and it doesn't work anymore. When you upgrade, you need to disable Bluetooth, then re-enable it. After re-enable, re-pair your light with the app. 96207418-4609-48df-9cbc-dd35a71e83f7-KB.tsv Editorial -Light doesn't work after upgrade. When you upgrade, you need to disable Bluetooth, then re-enable it. After re-enable, re-pair your light with the app. 96207418-4609-48df-9cbc-dd35a71e83f7-KB.tsv Editorial -Question Answer 890b1efc-27ad-4dca-8dcb-b20d29d50e14-KB.tsv Source -Who should I contact for customer service? Please direct all customer service questions to (202) 555-0164 \n 890b1efc-27ad-4dca-8dcb-b20d29d50e14-KB.tsv Editorial -Why does the light not work? The simplest way to troubleshoot your smart light is to turn it off and on. \n 890b1efc-27ad-4dca-8dcb-b20d29d50e14-KB.tsv Editorial -How long does the light's battery last for? The battery will last approximately 10 - 12 weeks with regular use. \n 890b1efc-27ad-4dca-8dcb-b20d29d50e14-KB.tsv Editorial -What type of light bulb do I need? A 26-Watt compact fluorescent light bulb that features both energy savings and long-life performance. 890b1efc-27ad-4dca-8dcb-b20d29d50e14-KB.tsv Editorial -Hi Hello Editorial \ No newline at end of file diff --git a/samples/11.qnamaker/config.py b/samples/11.qnamaker/config.py deleted file mode 100644 index 068a30d35..000000000 --- a/samples/11.qnamaker/config.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - -""" Bot Configuration """ - - -class DefaultConfig: - """ Bot Configuration """ - - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") - QNA_KNOWLEDGEBASE_ID = os.environ.get("QnAKnowledgebaseId", "") - QNA_ENDPOINT_KEY = os.environ.get("QnAEndpointKey", "") - QNA_ENDPOINT_HOST = os.environ.get("QnAEndpointHostName", "") diff --git a/samples/11.qnamaker/deploymentTemplates/template-with-preexisting-rg.json b/samples/11.qnamaker/deploymentTemplates/template-with-preexisting-rg.json deleted file mode 100644 index bff8c096d..000000000 --- a/samples/11.qnamaker/deploymentTemplates/template-with-preexisting-rg.json +++ /dev/null @@ -1,242 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "appId": { - "type": "string", - "metadata": { - "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." - } - }, - "appSecret": { - "type": "string", - "metadata": { - "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." - } - }, - "botId": { - "type": "string", - "metadata": { - "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." - } - }, - "botSku": { - "defaultValue": "F0", - "type": "string", - "metadata": { - "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." - } - }, - "newAppServicePlanName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "The name of the new App Service Plan." - } - }, - "newAppServicePlanSku": { - "type": "object", - "defaultValue": { - "name": "S1", - "tier": "Standard", - "size": "S1", - "family": "S", - "capacity": 1 - }, - "metadata": { - "description": "The SKU of the App Service Plan. Defaults to Standard values." - } - }, - "appServicePlanLocation": { - "type": "string", - "metadata": { - "description": "The location of the App Service Plan." - } - }, - "existingAppServicePlan": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Name of the existing App Service Plan used to create the Web App for the bot." - } - }, - "newWebAppName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." - } - } - }, - "variables": { - "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", - "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", - "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", - "publishingUsername": "[concat('$', parameters('newWebAppName'))]", - "resourcesLocation": "[parameters('appServicePlanLocation')]", - "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", - "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", - "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" - }, - "resources": [ - { - "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", - "type": "Microsoft.Web/serverfarms", - "apiVersion": "2016-09-01", - "name": "[variables('servicePlanName')]", - "location": "[variables('resourcesLocation')]", - "sku": "[parameters('newAppServicePlanSku')]", - "kind": "linux", - "properties": { - "name": "[variables('servicePlanName')]", - "perSiteScaling": false, - "reserved": true, - "targetWorkerCount": 0, - "targetWorkerSizeId": 0 - } - }, - { - "comments": "Create a Web App using a Linux App Service Plan", - "type": "Microsoft.Web/sites", - "apiVersion": "2016-08-01", - "name": "[variables('webAppName')]", - "location": "[variables('resourcesLocation')]", - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]" - ], - "kind": "app,linux", - "properties": { - "enabled": true, - "hostNameSslStates": [ - { - "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", - "sslState": "Disabled", - "hostType": "Standard" - }, - { - "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", - "sslState": "Disabled", - "hostType": "Repository" - } - ], - "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]", - "reserved": true, - "scmSiteAlsoStopped": false, - "clientAffinityEnabled": false, - "clientCertEnabled": false, - "hostNamesDisabled": false, - "containerSize": 0, - "dailyMemoryTimeQuota": 0, - "httpsOnly": false, - "siteConfig": { - "appSettings": [ - { - "name": "MicrosoftAppId", - "value": "[parameters('appId')]" - }, - { - "name": "MicrosoftAppPassword", - "value": "[parameters('appSecret')]" - }, - { - "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", - "value": "true" - } - ], - "cors": { - "allowedOrigins": [ - "https://botservice.hosting.portal.azure.net", - "https://hosting.onecloud.azure-test.net/" - ] - } - } - } - }, - { - "type": "Microsoft.Web/sites/config", - "apiVersion": "2016-08-01", - "name": "[concat(variables('webAppName'), '/web')]", - "location": "[variables('resourcesLocation')]", - "dependsOn": [ - "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" - ], - "properties": { - "numberOfWorkers": 1, - "defaultDocuments": [ - "Default.htm", - "Default.html", - "Default.asp", - "index.htm", - "index.html", - "iisstart.htm", - "default.aspx", - "index.php", - "hostingstart.html" - ], - "netFrameworkVersion": "v4.0", - "phpVersion": "", - "pythonVersion": "", - "nodeVersion": "", - "linuxFxVersion": "PYTHON|3.7", - "requestTracingEnabled": false, - "remoteDebuggingEnabled": false, - "remoteDebuggingVersion": "VS2017", - "httpLoggingEnabled": true, - "logsDirectorySizeLimit": 35, - "detailedErrorLoggingEnabled": false, - "publishingUsername": "[variables('publishingUsername')]", - "scmType": "None", - "use32BitWorkerProcess": true, - "webSocketsEnabled": false, - "alwaysOn": false, - "appCommandLine": "", - "managedPipelineMode": "Integrated", - "virtualApplications": [ - { - "virtualPath": "/", - "physicalPath": "site\\wwwroot", - "preloadEnabled": false, - "virtualDirectories": null - } - ], - "winAuthAdminState": 0, - "winAuthTenantState": 0, - "customAppPoolIdentityAdminState": false, - "customAppPoolIdentityTenantState": false, - "loadBalancing": "LeastRequests", - "routingRules": [], - "experiments": { - "rampUpRules": [] - }, - "autoHealEnabled": false, - "vnetName": "", - "minTlsVersion": "1.2", - "ftpsState": "AllAllowed", - "reservedInstanceCount": 0 - } - }, - { - "apiVersion": "2017-12-01", - "type": "Microsoft.BotService/botServices", - "name": "[parameters('botId')]", - "location": "global", - "kind": "bot", - "sku": { - "name": "[parameters('botSku')]" - }, - "properties": { - "name": "[parameters('botId')]", - "displayName": "[parameters('botId')]", - "endpoint": "[variables('botEndpoint')]", - "msaAppId": "[parameters('appId')]", - "developerAppInsightsApplicationId": null, - "developerAppInsightKey": null, - "publishingCredentials": null, - "storageResourceId": null - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" - ] - } - ] -} \ No newline at end of file diff --git a/samples/11.qnamaker/requirements.txt b/samples/11.qnamaker/requirements.txt deleted file mode 100644 index cf76fec34..000000000 --- a/samples/11.qnamaker/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -botbuilder-core>=4.4.0b1 -botbuilder-ai>=4.4.0b1 -flask>=1.0.3 diff --git a/samples/13.core-bot/README-LUIS.md b/samples/13.core-bot/README-LUIS.md deleted file mode 100644 index b6b9b925f..000000000 --- a/samples/13.core-bot/README-LUIS.md +++ /dev/null @@ -1,216 +0,0 @@ -# Setting up LUIS via CLI: - -This README contains information on how to create and deploy a LUIS application. When the bot is ready to be deployed to production, we recommend creating a LUIS Endpoint Resource for usage with your LUIS App. - -> _For instructions on how to create a LUIS Application via the LUIS portal, see these Quickstart steps:_ -> 1. _[Quickstart: Create a new app in the LUIS portal][Quickstart-create]_ -> 2. _[Quickstart: Deploy an app in the LUIS portal][Quickstart-deploy]_ - - [Quickstart-create]: https://docs.microsoft.com/azure/cognitive-services/luis/get-started-portal-build-app - [Quickstart-deploy]:https://docs.microsoft.com/azure/cognitive-services/luis/get-started-portal-deploy-app - -## Table of Contents: - -- [Prerequisites](#Prerequisites) -- [Import a new LUIS Application using a local LUIS application](#Import-a-new-LUIS-Application-using-a-local-LUIS-application) -- [How to create a LUIS Endpoint resource in Azure and pair it with a LUIS Application](#How-to-create-a-LUIS-Endpoint-resource-in-Azure-and-pair-it-with-a-LUIS-Application) - -___ - -## [Prerequisites](#Table-of-Contents): - -#### Install Azure CLI >=2.0.61: - -Visit the following page to find the correct installer for your OS: -- https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest - -#### Install LUIS CLI >=2.4.0: - -Open a CLI of your choice and type the following: - -```bash -npm i -g luis-apis@^2.4.0 -``` - -#### LUIS portal account: - -You should already have a LUIS account with either https://luis.ai, https://eu.luis.ai, or https://au.luis.ai. To determine where to create a LUIS account, consider where you will deploy your LUIS applications, and then place them in [the corresponding region][LUIS-Authoring-Regions]. - -After you've created your account, you need your [Authoring Key][LUIS-AKey] and a LUIS application ID. - - [LUIS-Authoring-Regions]: https://docs.microsoft.com/azure/cognitive-services/luis/luis-reference-regions#luis-authoring-regions] - [LUIS-AKey]: https://docs.microsoft.com/azure/cognitive-services/luis/luis-concept-keys#authoring-key - -___ - -## [Import a new LUIS Application using a local LUIS application](#Table-of-Contents) - -### 1. Import the local LUIS application to luis.ai - -```bash -luis import application --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appName "FlightBooking" --in "./cognitiveModels/FlightBooking.json" -``` - -Outputs the following JSON: - -```json -{ - "id": "########-####-####-####-############", - "name": "FlightBooking", - "description": "A LUIS model that uses intent and entities.", - "culture": "en-us", - "usageScenario": "", - "domain": "", - "versionsCount": 1, - "createdDateTime": "2019-03-29T18:32:02Z", - "endpoints": {}, - "endpointHitsCount": 0, - "activeVersion": "0.1", - "ownerEmail": "bot@contoso.com", - "tokenizerVersion": "1.0.0" -} -``` - -For the next step, you'll need the `"id"` value for `--appId` and the `"activeVersion"` value for `--versionId`. - -### 2. Train the LUIS Application - -```bash -luis train version --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appId "LuisAppId" --versionId "LuisAppversion" --wait -``` - -### 3. Publish the LUIS Application - -```bash -luis publish version --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appId "LuisAppId" --versionId "LuisAppversion" --publishRegion "LuisAppPublishRegion" -``` - -> `--region` corresponds to the region you _author_ your application in. The regions available for this are "westus", "westeurope" and "australiaeast".
-> These regions correspond to the three available portals, https://luis.ai, https://eu.luis.ai, or https://au.luis.ai.
-> `--publishRegion` corresponds to the region of the endpoint you're publishing to, (e.g. "westus", "southeastasia", "westeurope", "brazilsouth").
-> See the [reference docs][Endpoint-API] for a list of available publish/endpoint regions. - - [Endpoint-API]: https://westus.dev.cognitive.microsoft.com/docs/services/5819c76f40a6350ce09de1ac/operations/5819c77140a63516d81aee78 - -Outputs the following: - -```json - { - "versionId": "0.1", - "isStaging": false, - "endpointUrl": "https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/########-####-####-####-############", - "region": "westus", - "assignedEndpointKey": null, - "endpointRegion": "westus", - "failedRegions": "", - "publishedDateTime": "2019-03-29T18:40:32Z", - "directVersionPublish": false -} -``` - -To see how to create an LUIS Cognitive Service Resource in Azure, please see [the next README][README-LUIS]. This Resource should be used when you want to move your bot to production. The instructions will show you how to create and pair the resource with a LUIS Application. - - [README-LUIS]: ./README-LUIS.md - -___ - -## [How to create a LUIS Endpoint resource in Azure and pair it with a LUIS Application](#Table-of-Contents) - -### 1. Create a new LUIS Cognitive Services resource on Azure via Azure CLI - -> _Note:_
-> _If you don't have a Resource Group in your Azure subscription, you can create one through the Azure portal or through using:_ -> ```bash -> az group create --subscription "AzureSubscriptionGuid" --location "westus" --name "ResourceGroupName" -> ``` -> _To see a list of valid locations, use `az account list-locations`_ - - -```bash -# Use Azure CLI to create the LUIS Key resource on Azure -az cognitiveservices account create --kind "luis" --name "NewLuisResourceName" --sku "S0" --location "westus" --subscription "AzureSubscriptionGuid" -g "ResourceGroupName" -``` - -The command will output a response similar to the JSON below: - -```json -{ - "endpoint": "https://westus.api.cognitive.microsoft.com/luis/v2.0", - "etag": "\"########-####-####-####-############\"", - "id": "/subscriptions/########-####-####-####-############/resourceGroups/ResourceGroupName/providers/Microsoft.CognitiveServices/accounts/NewLuisResourceName", - "internalId": "################################", - "kind": "luis", - "location": "westus", - "name": "NewLuisResourceName", - "provisioningState": "Succeeded", - "resourceGroup": "ResourceGroupName", - "sku": { - "name": "S0", - "tier": null - }, - "tags": null, - "type": "Microsoft.CognitiveServices/accounts" -} -``` - - - -Take the output from the previous command and create a JSON file in the following format: - -```json -{ - "azureSubscriptionId": "00000000-0000-0000-0000-000000000000", - "resourceGroup": "ResourceGroupName", - "accountName": "NewLuisResourceName" -} -``` - -### 2. Retrieve ARM access token via Azure CLI - -```bash -az account get-access-token --subscription "AzureSubscriptionGuid" -``` - -This will return an object that looks like this: - -```json -{ - "accessToken": "eyJ0eXAiOiJKVtokentokentokentokentokeng1dCI6Ik4tbEMwbi05REFMcXdodUhZbkhRNjNHZUNYYyIsItokenI6Ik4tbEMwbi05REFMcXdodUhZbkhRNjNHZUNYYyJ9.eyJhdWQiOiJodHRwczovL21hbmFnZW1lbnQuY29yZS53aW5kb3dzLm5ldC8iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNTUzODc3MTUwLCJuYmYiOjE1NTM4NzcxNTAsImV4cCI6MTU1Mzg4MTA1MCwiX2NsYWltX25hbWVzIjp7Imdyb3VwcyI6InNyYzEifSwiX2NsYWltX3NvdXJjZXMiOnsic3JjMSI6eyJlbmRwb2ludCI6Imh0dHBzOi8vZ3JhcGgud2luZG93cy5uZXQvNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3L3VzZXJzL2ZmZTQyM2RkLWJhM2YtNDg0Ny04NjgyLWExNTI5MDA4MjM4Ny9nZXRNZW1iZXJPYmplY3RzIn19LCJhY3IiOiIxIiwiYWlvIjoiQVZRQXEvOEtBQUFBeGVUc201NDlhVHg4RE1mMFlRVnhGZmxxOE9RSC9PODR3QktuSmRqV1FqTkkwbmxLYzB0bHJEZzMyMFZ5bWZGaVVBSFBvNUFFUTNHL0FZNDRjdk01T3M0SEt0OVJkcE5JZW9WU0dzd0kvSkk9IiwiYW1yIjpbIndpYSIsIm1mYSJdLCJhcHBpZCI6IjA0YjA3Nzk1LThkZGItNDYxYS1iYmVlLTAyZjllMWJmN2I0NiIsImFwcGlkYWNyIjoiMCIsImRldmljZWlkIjoiNDhmNDVjNjEtMTg3Zi00MjUxLTlmZWItMTllZGFkZmMwMmE3IiwiZmFtaWx5X25hbWUiOiJHdW0iLCJnaXZlbl9uYW1lIjoiU3RldmVuIiwiaXBhZGRyIjoiMTY3LjIyMC4yLjU1IiwibmFtZSI6IlN0ZXZlbiBHdW0iLCJvaWQiOiJmZmU0MjNkZC1iYTNmLTQ4NDctODY4Mi1hMTUyOTAwODIzODciLCJvbnByZW1fc2lkIjoiUy0xLTUtMjEtMjEyNzUyMTE4NC0xNjA0MDEyOTIwLTE4ODc5Mjc1MjctMjYwOTgyODUiLCJwdWlkIjoiMTAwMzdGRkVBMDQ4NjlBNyIsInJoIjoiSSIsInNjcCI6InVzZXJfaW1wZXJzb25hdGlvbiIsInN1YiI6Ik1rMGRNMWszN0U5ckJyMjhieUhZYjZLSU85LXVFQVVkZFVhNWpkSUd1Nk0iLCJ0aWQiOiI3MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDciLCJ1bmlxdWVfbmFtZSI6InN0Z3VtQG1pY3Jvc29mdC5jb20iLCJ1cG4iOiJzdGd1bUBtaWNyb3NvZnQuY29tIiwidXRpIjoiT2w2NGN0TXY4RVNEQzZZQWRqRUFtokenInZlciI6IjEuMCJ9.kFAsEilE0mlS1pcpqxf4rEnRKeYsehyk-gz-zJHUrE__oad3QjgDSBDPrR_ikLdweynxbj86pgG4QFaHURNCeE6SzrbaIrNKw-n9jrEtokenlosOxg_0l2g1LeEUOi5Q4gQREAU_zvSbl-RY6sAadpOgNHtGvz3Rc6FZRITfkckSLmsKAOFoh-aWC6tFKG8P52rtB0qVVRz9tovBeNqkMYL49s9ypduygbXNVwSQhm5JszeWDgrFuVFHBUP_iENCQYGQpEZf_KvjmX1Ur1F9Eh9nb4yI2gFlKncKNsQl-tokenK7-tokentokentokentokentokentokenatoken", - "expiresOn": "2200-12-31 23:59:59.999999", - "subscription": "AzureSubscriptionGuid", - "tenant": "tenant-guid", - "tokenType": "Bearer" -} -``` - -The value needed for the next step is the `"accessToken"`. - -### 3. Use `luis add appazureaccount` to pair your LUIS resource with a LUIS Application - -```bash -luis add appazureaccount --in "path/to/created/requestBody.json" --appId "LuisAppId" --authoringKey "LuisAuthoringKey" --armToken "accessToken" -``` - -If successful, it should yield a response like this: - -```json -{ - "code": "Success", - "message": "Operation Successful" -} -``` - -### 4. See the LUIS Cognitive Services' keys - -```bash -az cognitiveservices account keys list --name "NewLuisResourceName" --subscription "AzureSubscriptionGuid" -g "ResourceGroupName" -``` - -This will return an object that looks like this: - -```json -{ - "key1": "9a69####dc8f####8eb4####399f####", - "key2": "####f99e####4b1a####fb3b####6b9f" -} -``` \ No newline at end of file diff --git a/samples/13.core-bot/README.md b/samples/13.core-bot/README.md deleted file mode 100644 index 01bfd900c..000000000 --- a/samples/13.core-bot/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# CoreBot - -Bot Framework v4 core bot sample. - -This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to: - -- Use [LUIS](https://www.luis.ai) to implement core AI capabilities -- Implement a multi-turn conversation using Dialogs -- Handle user interruptions for such things as `Help` or `Cancel` -- Prompt for and validate requests for information from the user - -## Prerequisites - -This sample **requires** prerequisites in order to run. - -### Overview - -This bot uses [LUIS](https://www.luis.ai), an AI based cognitive service, to implement language understanding. - -### Install Python 3.7 - - -### Create a LUIS Application to enable language understanding - -LUIS language model setup, training, and application configuration steps can be found [here](https://docs.microsoft.com/azure/bot-service/bot-builder-howto-v4-luis?view=azure-bot-service-4.0&tabs=cs). - -If you wish to create a LUIS application via the CLI, these steps can be found in the [README-LUIS.md](README-LUIS.md). - -## Running the sample -- Run `pip install -r requirements.txt` to install all dependencies -- Update LuisAppId, LuisAPIKey and LuisAPIHostName in `config.py` with the information retrieved from the [LUIS portal](https://www.luis.ai) -- Run `python app.py` -- Alternatively to the last command, you can set the file in an environment variable with `set FLASK_APP=app.py` in windows (`export FLASK_APP=app.py` in mac/linux) and then run `flask run --host=127.0.0.1 --port=3978` - - -## Testing the bot using Bot Framework Emulator - -[Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework Emulator version 4.3.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to the bot using Bot Framework Emulator - -- Launch Bot Framework Emulator -- Enter a Bot URL of `http://localhost:3978/api/messages` - - -## Further reading - -- [Bot Framework Documentation](https://docs.botframework.com) -- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) -- [Dialogs](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0) -- [Gathering Input Using Prompts](https://docs.microsoft.com/azure/bot-service/bot-builder-prompts?view=azure-bot-service-4.0&tabs=csharp) -- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) -- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) -- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) -- [.NET Core CLI tools](https://docs.microsoft.com/dotnet/core/tools/?tabs=netcore2x) -- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest) -- [Azure Portal](https://portal.azure.com) -- [Language Understanding using LUIS](https://docs.microsoft.com/azure/cognitive-services/luis/) -- [Channels and Bot Connector Service](https://docs.microsoft.com/azure/bot-service/bot-concepts?view=azure-bot-service-4.0) \ No newline at end of file diff --git a/samples/13.core-bot/adapter_with_error_handler.py b/samples/13.core-bot/adapter_with_error_handler.py deleted file mode 100644 index 1826e1e47..000000000 --- a/samples/13.core-bot/adapter_with_error_handler.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import sys -from datetime import datetime - -from botbuilder.core import ( - BotFrameworkAdapter, - BotFrameworkAdapterSettings, - ConversationState, - TurnContext, -) -from botbuilder.schema import ActivityTypes, Activity - - -class AdapterWithErrorHandler(BotFrameworkAdapter): - def __init__( - self, - settings: BotFrameworkAdapterSettings, - conversation_state: ConversationState, - ): - super().__init__(settings) - self._conversation_state = conversation_state - - # Catch-all for errors. - async def on_error(context: TurnContext, error: Exception): - # This check writes out errors to console log - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - - # Send a message to the user - await context.send_activity("The bot encountered an error or bug.") - await context.send_activity( - "To continue to run this bot, please fix the bot source code." - ) - # Send a trace activity if we're talking to the Bot Framework Emulator - if context.activity.channel_id == "emulator": - # Create a trace activity that contains the error object - trace_activity = Activity( - label="TurnError", - name="on_turn_error Trace", - timestamp=datetime.utcnow(), - type=ActivityTypes.trace, - value=f"{error}", - value_type="https://www.botframework.com/schemas/error", - ) - # Send a trace activity, which will be displayed in Bot Framework Emulator - await context.send_activity(trace_activity) - - # Clear out state - nonlocal self - await self._conversation_state.delete(context) - - self.on_turn_error = on_error diff --git a/samples/13.core-bot/app.py b/samples/13.core-bot/app.py deleted file mode 100644 index d09b2d991..000000000 --- a/samples/13.core-bot/app.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -""" -This sample shows how to create a bot that demonstrates the following: -- Use [LUIS](https://www.luis.ai) to implement core AI capabilities. -- Implement a multi-turn conversation using Dialogs. -- Handle user interruptions for such things as `Help` or `Cancel`. -- Prompt for and validate requests for information from the user. -""" - -import asyncio - -from flask import Flask, request, Response -from botbuilder.core import ( - BotFrameworkAdapterSettings, - ConversationState, - MemoryStorage, - UserState, -) -from botbuilder.schema import Activity -from dialogs import MainDialog, BookingDialog -from bots import DialogAndWelcomeBot - -from adapter_with_error_handler import AdapterWithErrorHandler -from flight_booking_recognizer import FlightBookingRecognizer - -# Create the loop and Flask app -LOOP = asyncio.get_event_loop() -APP = Flask(__name__, instance_relative_config=True) -APP.config.from_object("config.DefaultConfig") - -SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"]) - -# Create MemoryStorage, UserState and ConversationState -MEMORY = MemoryStorage() -USER_STATE = UserState(MEMORY) -CONVERSATION_STATE = ConversationState(MEMORY) - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -ADAPTER = AdapterWithErrorHandler(SETTINGS, CONVERSATION_STATE) - -# Create dialogs and Bot -RECOGNIZER = FlightBookingRecognizer(APP.config) -BOOKING_DIALOG = BookingDialog() -DIALOG = MainDialog(RECOGNIZER, BOOKING_DIALOG) -BOT = DialogAndWelcomeBot(CONVERSATION_STATE, USER_STATE, DIALOG) - - -# Listen for incoming requests on /api/messages. -@APP.route("/api/messages", methods=["POST"]) -def messages(): - # Main bot message handler. - if "application/json" in request.headers["Content-Type"]: - body = request.json - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = ( - request.headers["Authorization"] if "Authorization" in request.headers else "" - ) - - try: - task = LOOP.create_task( - ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - ) - LOOP.run_until_complete(task) - return Response(status=201) - except Exception as exception: - raise exception - - -if __name__ == "__main__": - try: - APP.run(debug=False, port=APP.config["PORT"]) # nosec debug - except Exception as exception: - raise exception diff --git a/samples/13.core-bot/booking_details.py b/samples/13.core-bot/booking_details.py deleted file mode 100644 index 9c2d2a1bc..000000000 --- a/samples/13.core-bot/booking_details.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -class BookingDetails: - def __init__( - self, - destination: str = None, - origin: str = None, - travel_date: str = None, - unsupported_airports=None, - ): - if unsupported_airports is None: - unsupported_airports = [] - self.destination = destination - self.origin = origin - self.travel_date = travel_date - self.unsupported_airports = unsupported_airports diff --git a/samples/13.core-bot/bots/__init__.py b/samples/13.core-bot/bots/__init__.py deleted file mode 100644 index 6925db302..000000000 --- a/samples/13.core-bot/bots/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .dialog_bot import DialogBot -from .dialog_and_welcome_bot import DialogAndWelcomeBot - -__all__ = ["DialogBot", "DialogAndWelcomeBot"] diff --git a/samples/13.core-bot/bots/dialog_and_welcome_bot.py b/samples/13.core-bot/bots/dialog_and_welcome_bot.py deleted file mode 100644 index bfe8957af..000000000 --- a/samples/13.core-bot/bots/dialog_and_welcome_bot.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import json -import os.path - -from typing import List -from botbuilder.core import ( - ConversationState, - MessageFactory, - UserState, - TurnContext, -) -from botbuilder.dialogs import Dialog -from botbuilder.schema import Attachment, ChannelAccount -from helpers.dialog_helper import DialogHelper - -from .dialog_bot import DialogBot - - -class DialogAndWelcomeBot(DialogBot): - def __init__( - self, - conversation_state: ConversationState, - user_state: UserState, - dialog: Dialog, - ): - super(DialogAndWelcomeBot, self).__init__( - conversation_state, user_state, dialog - ) - - async def on_members_added_activity( - self, members_added: List[ChannelAccount], turn_context: TurnContext - ): - for member in members_added: - # Greet anyone that was not the target (recipient) of this message. - # To learn more about Adaptive Cards, see https://aka.ms/msbot-adaptivecards for more details. - if member.id != turn_context.activity.recipient.id: - welcome_card = self.create_adaptive_card_attachment() - response = MessageFactory.attachment(welcome_card) - await turn_context.send_activity(response) - await DialogHelper.run_dialog( - self.dialog, - turn_context, - self.conversation_state.create_property("DialogState"), - ) - - # Load attachment from file. - def create_adaptive_card_attachment(self): - relative_path = os.path.abspath(os.path.dirname(__file__)) - path = os.path.join(relative_path, "../cards/welcomeCard.json") - with open(path) as in_file: - card = json.load(in_file) - - return Attachment( - content_type="application/vnd.microsoft.card.adaptive", content=card - ) diff --git a/samples/13.core-bot/bots/dialog_bot.py b/samples/13.core-bot/bots/dialog_bot.py deleted file mode 100644 index eb560a1be..000000000 --- a/samples/13.core-bot/bots/dialog_bot.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import ActivityHandler, ConversationState, UserState, TurnContext -from botbuilder.dialogs import Dialog -from helpers.dialog_helper import DialogHelper - - -class DialogBot(ActivityHandler): - def __init__( - self, - conversation_state: ConversationState, - user_state: UserState, - dialog: Dialog, - ): - if conversation_state is None: - raise Exception( - "[DialogBot]: Missing parameter. conversation_state is required" - ) - if user_state is None: - raise Exception("[DialogBot]: Missing parameter. user_state is required") - if dialog is None: - raise Exception("[DialogBot]: Missing parameter. dialog is required") - - self.conversation_state = conversation_state - self.user_state = user_state - self.dialog = dialog - - async def on_turn(self, turn_context: TurnContext): - await super().on_turn(turn_context) - - # Save any state changes that might have occurred during the turn. - await self.conversation_state.save_changes(turn_context, False) - await self.user_state.save_changes(turn_context, False) - - async def on_message_activity(self, turn_context: TurnContext): - await DialogHelper.run_dialog( - self.dialog, - turn_context, - self.conversation_state.create_property("DialogState"), - ) diff --git a/samples/13.core-bot/cards/welcomeCard.json b/samples/13.core-bot/cards/welcomeCard.json deleted file mode 100644 index cc10cda9f..000000000 --- a/samples/13.core-bot/cards/welcomeCard.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", - "type": "AdaptiveCard", - "version": "1.0", - "body": [ - { - "type": "Image", - "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU", - "size": "stretch" - }, - { - "type": "TextBlock", - "spacing": "medium", - "size": "default", - "weight": "bolder", - "text": "Welcome to Bot Framework!", - "wrap": true, - "maxLines": 0 - }, - { - "type": "TextBlock", - "size": "default", - "isSubtle": "true", - "text": "Now that you have successfully run your bot, follow the links in this Adaptive Card to expand your knowledge of Bot Framework.", - "wrap": true, - "maxLines": 0 - } - ], - "actions": [ - { - "type": "Action.OpenUrl", - "title": "Get an overview", - "url": "https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0" - }, - { - "type": "Action.OpenUrl", - "title": "Ask a question", - "url": "https://stackoverflow.com/questions/tagged/botframework" - }, - { - "type": "Action.OpenUrl", - "title": "Learn how to deploy", - "url": "https://docs.microsoft.com/azure/bot-service/bot-builder-howto-deploy-azure?view=azure-bot-service-4.0" - } - ] -} \ No newline at end of file diff --git a/samples/13.core-bot/cognitiveModels/FlightBooking.json b/samples/13.core-bot/cognitiveModels/FlightBooking.json deleted file mode 100644 index f0e4b9770..000000000 --- a/samples/13.core-bot/cognitiveModels/FlightBooking.json +++ /dev/null @@ -1,339 +0,0 @@ -{ - "luis_schema_version": "3.2.0", - "versionId": "0.1", - "name": "FlightBooking", - "desc": "Luis Model for CoreBot", - "culture": "en-us", - "tokenizerVersion": "1.0.0", - "intents": [ - { - "name": "BookFlight" - }, - { - "name": "Cancel" - }, - { - "name": "GetWeather" - }, - { - "name": "None" - } - ], - "entities": [], - "composites": [ - { - "name": "From", - "children": [ - "Airport" - ], - "roles": [] - }, - { - "name": "To", - "children": [ - "Airport" - ], - "roles": [] - } - ], - "closedLists": [ - { - "name": "Airport", - "subLists": [ - { - "canonicalForm": "Paris", - "list": [ - "paris", - "cdg" - ] - }, - { - "canonicalForm": "London", - "list": [ - "london", - "lhr" - ] - }, - { - "canonicalForm": "Berlin", - "list": [ - "berlin", - "txl" - ] - }, - { - "canonicalForm": "New York", - "list": [ - "new york", - "jfk" - ] - }, - { - "canonicalForm": "Seattle", - "list": [ - "seattle", - "sea" - ] - } - ], - "roles": [] - } - ], - "patternAnyEntities": [], - "regex_entities": [], - "prebuiltEntities": [ - { - "name": "datetimeV2", - "roles": [] - } - ], - "model_features": [], - "regex_features": [], - "patterns": [], - "utterances": [ - { - "text": "book a flight", - "intent": "BookFlight", - "entities": [] - }, - { - "text": "book a flight from new york", - "intent": "BookFlight", - "entities": [ - { - "entity": "From", - "startPos": 19, - "endPos": 26 - } - ] - }, - { - "text": "book a flight from seattle", - "intent": "BookFlight", - "entities": [ - { - "entity": "From", - "startPos": 19, - "endPos": 25 - } - ] - }, - { - "text": "book a hotel in new york", - "intent": "None", - "entities": [] - }, - { - "text": "book a restaurant", - "intent": "None", - "entities": [] - }, - { - "text": "book flight from london to paris on feb 14th", - "intent": "BookFlight", - "entities": [ - { - "entity": "From", - "startPos": 17, - "endPos": 22 - }, - { - "entity": "To", - "startPos": 27, - "endPos": 31 - } - ] - }, - { - "text": "book flight to berlin on feb 14th", - "intent": "BookFlight", - "entities": [ - { - "entity": "To", - "startPos": 15, - "endPos": 20 - } - ] - }, - { - "text": "book me a flight from london to paris", - "intent": "BookFlight", - "entities": [ - { - "entity": "From", - "startPos": 22, - "endPos": 27 - }, - { - "entity": "To", - "startPos": 32, - "endPos": 36 - } - ] - }, - { - "text": "bye", - "intent": "Cancel", - "entities": [] - }, - { - "text": "cancel booking", - "intent": "Cancel", - "entities": [] - }, - { - "text": "exit", - "intent": "Cancel", - "entities": [] - }, - { - "text": "find an airport near me", - "intent": "None", - "entities": [] - }, - { - "text": "flight to paris", - "intent": "BookFlight", - "entities": [ - { - "entity": "To", - "startPos": 10, - "endPos": 14 - } - ] - }, - { - "text": "flight to paris from london on feb 14th", - "intent": "BookFlight", - "entities": [ - { - "entity": "To", - "startPos": 10, - "endPos": 14 - }, - { - "entity": "From", - "startPos": 21, - "endPos": 26 - } - ] - }, - { - "text": "fly from berlin to paris on may 5th", - "intent": "BookFlight", - "entities": [ - { - "entity": "From", - "startPos": 9, - "endPos": 14 - }, - { - "entity": "To", - "startPos": 19, - "endPos": 23 - } - ] - }, - { - "text": "go to paris", - "intent": "BookFlight", - "entities": [ - { - "entity": "To", - "startPos": 6, - "endPos": 10 - } - ] - }, - { - "text": "going from paris to berlin", - "intent": "BookFlight", - "entities": [ - { - "entity": "From", - "startPos": 11, - "endPos": 15 - }, - { - "entity": "To", - "startPos": 20, - "endPos": 25 - } - ] - }, - { - "text": "i'd like to rent a car", - "intent": "None", - "entities": [] - }, - { - "text": "ignore", - "intent": "Cancel", - "entities": [] - }, - { - "text": "travel from new york to paris", - "intent": "BookFlight", - "entities": [ - { - "entity": "From", - "startPos": 12, - "endPos": 19 - }, - { - "entity": "To", - "startPos": 24, - "endPos": 28 - } - ] - }, - { - "text": "travel to new york", - "intent": "BookFlight", - "entities": [ - { - "entity": "To", - "startPos": 10, - "endPos": 17 - } - ] - }, - { - "text": "travel to paris", - "intent": "BookFlight", - "entities": [ - { - "entity": "To", - "startPos": 10, - "endPos": 14 - } - ] - }, - { - "text": "what's the forecast for this friday?", - "intent": "GetWeather", - "entities": [] - }, - { - "text": "what's the weather like for tomorrow", - "intent": "GetWeather", - "entities": [] - }, - { - "text": "what's the weather like in new york", - "intent": "GetWeather", - "entities": [] - }, - { - "text": "what's the weather like?", - "intent": "GetWeather", - "entities": [] - }, - { - "text": "winter is coming", - "intent": "None", - "entities": [] - } - ], - "settings": [] -} \ No newline at end of file diff --git a/samples/13.core-bot/config.py b/samples/13.core-bot/config.py deleted file mode 100644 index 83f1bbbdf..000000000 --- a/samples/13.core-bot/config.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - -""" Bot Configuration """ - - -class DefaultConfig: - """ Bot Configuration """ - - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") - LUIS_APP_ID = os.environ.get("LuisAppId", "") - LUIS_API_KEY = os.environ.get("LuisAPIKey", "") - # LUIS endpoint host name, ie "westus.api.cognitive.microsoft.com" - LUIS_API_HOST_NAME = os.environ.get("LuisAPIHostName", "") diff --git a/samples/13.core-bot/dialogs/__init__.py b/samples/13.core-bot/dialogs/__init__.py deleted file mode 100644 index 567539f96..000000000 --- a/samples/13.core-bot/dialogs/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .booking_dialog import BookingDialog -from .cancel_and_help_dialog import CancelAndHelpDialog -from .date_resolver_dialog import DateResolverDialog -from .main_dialog import MainDialog - -__all__ = ["BookingDialog", "CancelAndHelpDialog", "DateResolverDialog", "MainDialog"] diff --git a/samples/13.core-bot/dialogs/booking_dialog.py b/samples/13.core-bot/dialogs/booking_dialog.py deleted file mode 100644 index 5b4381919..000000000 --- a/samples/13.core-bot/dialogs/booking_dialog.py +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from datatypes_date_time.timex import Timex - -from botbuilder.dialogs import WaterfallDialog, WaterfallStepContext, DialogTurnResult -from botbuilder.dialogs.prompts import ConfirmPrompt, TextPrompt, PromptOptions -from botbuilder.core import MessageFactory -from botbuilder.schema import InputHints -from .cancel_and_help_dialog import CancelAndHelpDialog -from .date_resolver_dialog import DateResolverDialog - - -class BookingDialog(CancelAndHelpDialog): - def __init__(self, dialog_id: str = None): - super(BookingDialog, self).__init__(dialog_id or BookingDialog.__name__) - - self.add_dialog(TextPrompt(TextPrompt.__name__)) - self.add_dialog(ConfirmPrompt(ConfirmPrompt.__name__)) - self.add_dialog(DateResolverDialog(DateResolverDialog.__name__)) - self.add_dialog( - WaterfallDialog( - WaterfallDialog.__name__, - [ - self.destination_step, - self.origin_step, - self.travel_date_step, - self.confirm_step, - self.final_step, - ], - ) - ) - - self.initial_dialog_id = WaterfallDialog.__name__ - - async def destination_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - """ - If a destination city has not been provided, prompt for one. - :param step_context: - :return DialogTurnResult: - """ - booking_details = step_context.options - - if booking_details.destination is None: - message_text = "Where would you like to travel to?" - prompt_message = MessageFactory.text( - message_text, message_text, InputHints.expecting_input - ) - return await step_context.prompt( - TextPrompt.__name__, PromptOptions(prompt=prompt_message) - ) - return await step_context.next(booking_details.destination) - - async def origin_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - """ - If an origin city has not been provided, prompt for one. - :param step_context: - :return DialogTurnResult: - """ - booking_details = step_context.options - - # Capture the response to the previous step's prompt - booking_details.destination = step_context.result - if booking_details.origin is None: - message_text = "From what city will you be travelling?" - prompt_message = MessageFactory.text( - message_text, message_text, InputHints.expecting_input - ) - return await step_context.prompt( - TextPrompt.__name__, PromptOptions(prompt=prompt_message) - ) - return await step_context.next(booking_details.origin) - - async def travel_date_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - """ - If a travel date has not been provided, prompt for one. - This will use the DATE_RESOLVER_DIALOG. - :param step_context: - :return DialogTurnResult: - """ - booking_details = step_context.options - - # Capture the results of the previous step - booking_details.origin = step_context.result - if not booking_details.travel_date or self.is_ambiguous( - booking_details.travel_date - ): - return await step_context.begin_dialog( - DateResolverDialog.__name__, booking_details.travel_date - ) - return await step_context.next(booking_details.travel_date) - - async def confirm_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - """ - Confirm the information the user has provided. - :param step_context: - :return DialogTurnResult: - """ - booking_details = step_context.options - - # Capture the results of the previous step - booking_details.travel_date = step_context.result - message_text = ( - f"Please confirm, I have you traveling to: { booking_details.destination } from: " - f"{ booking_details.origin } on: { booking_details.travel_date}." - ) - prompt_message = MessageFactory.text( - message_text, message_text, InputHints.expecting_input - ) - - # Offer a YES/NO prompt. - return await step_context.prompt( - ConfirmPrompt.__name__, PromptOptions(prompt=prompt_message) - ) - - async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - """ - Complete the interaction and end the dialog. - :param step_context: - :return DialogTurnResult: - """ - if step_context.result: - booking_details = step_context.options - - return await step_context.end_dialog(booking_details) - return await step_context.end_dialog() - - def is_ambiguous(self, timex: str) -> bool: - timex_property = Timex(timex) - return "definite" not in timex_property.types diff --git a/samples/13.core-bot/dialogs/cancel_and_help_dialog.py b/samples/13.core-bot/dialogs/cancel_and_help_dialog.py deleted file mode 100644 index f8bcc77d0..000000000 --- a/samples/13.core-bot/dialogs/cancel_and_help_dialog.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.dialogs import ( - ComponentDialog, - DialogContext, - DialogTurnResult, - DialogTurnStatus, -) -from botbuilder.schema import ActivityTypes, InputHints -from botbuilder.core import MessageFactory - - -class CancelAndHelpDialog(ComponentDialog): - def __init__(self, dialog_id: str): - super(CancelAndHelpDialog, self).__init__(dialog_id) - - async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult: - result = await self.interrupt(inner_dc) - if result is not None: - return result - - return await super(CancelAndHelpDialog, self).on_continue_dialog(inner_dc) - - async def interrupt(self, inner_dc: DialogContext) -> DialogTurnResult: - if inner_dc.context.activity.type == ActivityTypes.message: - text = inner_dc.context.activity.text.lower() - - help_message_text = "Show Help..." - help_message = MessageFactory.text( - help_message_text, help_message_text, InputHints.expecting_input - ) - - if text in ("help", "?"): - await inner_dc.context.send_activity(help_message) - return DialogTurnResult(DialogTurnStatus.Waiting) - - cancel_message_text = "Cancelling" - cancel_message = MessageFactory.text( - cancel_message_text, cancel_message_text, InputHints.ignoring_input - ) - - if text in ("cancel", "quit"): - await inner_dc.context.send_activity(cancel_message) - return await inner_dc.cancel_all_dialogs() - - return None diff --git a/samples/13.core-bot/dialogs/date_resolver_dialog.py b/samples/13.core-bot/dialogs/date_resolver_dialog.py deleted file mode 100644 index a34f47a7a..000000000 --- a/samples/13.core-bot/dialogs/date_resolver_dialog.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from datatypes_date_time.timex import Timex - -from botbuilder.core import MessageFactory -from botbuilder.dialogs import WaterfallDialog, DialogTurnResult, WaterfallStepContext -from botbuilder.dialogs.prompts import ( - DateTimePrompt, - PromptValidatorContext, - PromptOptions, - DateTimeResolution, -) -from botbuilder.schema import InputHints -from .cancel_and_help_dialog import CancelAndHelpDialog - - -class DateResolverDialog(CancelAndHelpDialog): - def __init__(self, dialog_id: str = None): - super(DateResolverDialog, self).__init__( - dialog_id or DateResolverDialog.__name__ - ) - - self.add_dialog( - DateTimePrompt( - DateTimePrompt.__name__, DateResolverDialog.datetime_prompt_validator - ) - ) - self.add_dialog( - WaterfallDialog( - WaterfallDialog.__name__ + "2", [self.initial_step, self.final_step] - ) - ) - - self.initial_dialog_id = WaterfallDialog.__name__ + "2" - - async def initial_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - timex = step_context.options - - prompt_msg_text = "On what date would you like to travel?" - prompt_msg = MessageFactory.text( - prompt_msg_text, prompt_msg_text, InputHints.expecting_input - ) - - reprompt_msg_text = "I'm sorry, for best results, please enter your travel date including the month, " \ - "day and year. " - reprompt_msg = MessageFactory.text( - reprompt_msg_text, reprompt_msg_text, InputHints.expecting_input - ) - - if timex is None: - # We were not given any date at all so prompt the user. - return await step_context.prompt( - DateTimePrompt.__name__, - PromptOptions(prompt=prompt_msg, retry_prompt=reprompt_msg), - ) - # We have a Date we just need to check it is unambiguous. - if "definite" not in Timex(timex).types: - # This is essentially a "reprompt" of the data we were given up front. - return await step_context.prompt( - DateTimePrompt.__name__, PromptOptions(prompt=reprompt_msg) - ) - - return await step_context.next(DateTimeResolution(timex=timex)) - - async def final_step(self, step_context: WaterfallStepContext): - timex = step_context.result[0].timex - return await step_context.end_dialog(timex) - - @staticmethod - async def datetime_prompt_validator(prompt_context: PromptValidatorContext) -> bool: - if prompt_context.recognized.succeeded: - timex = prompt_context.recognized.value[0].timex.split("T")[0] - - # TODO: Needs TimexProperty - return "definite" in Timex(timex).types - - return False diff --git a/samples/13.core-bot/dialogs/main_dialog.py b/samples/13.core-bot/dialogs/main_dialog.py deleted file mode 100644 index 82dfaa00b..000000000 --- a/samples/13.core-bot/dialogs/main_dialog.py +++ /dev/null @@ -1,132 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.dialogs import ( - ComponentDialog, - WaterfallDialog, - WaterfallStepContext, - DialogTurnResult, -) -from botbuilder.dialogs.prompts import TextPrompt, PromptOptions -from botbuilder.core import MessageFactory, TurnContext -from botbuilder.schema import InputHints - -from booking_details import BookingDetails -from flight_booking_recognizer import FlightBookingRecognizer -from helpers.luis_helper import LuisHelper, Intent -from .booking_dialog import BookingDialog - - -class MainDialog(ComponentDialog): - def __init__( - self, luis_recognizer: FlightBookingRecognizer, booking_dialog: BookingDialog - ): - super(MainDialog, self).__init__(MainDialog.__name__) - - self._luis_recognizer = luis_recognizer - self._booking_dialog_id = booking_dialog.id - - self.add_dialog(TextPrompt(TextPrompt.__name__)) - self.add_dialog(booking_dialog) - self.add_dialog( - WaterfallDialog( - "WFDialog", [self.intro_step, self.act_step, self.final_step] - ) - ) - - self.initial_dialog_id = "WFDialog" - - async def intro_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - if not self._luis_recognizer.is_configured: - await step_context.context.send_activity( - MessageFactory.text( - "NOTE: LUIS is not configured. To enable all capabilities, add 'LuisAppId', 'LuisAPIKey' and " - "'LuisAPIHostName' to the appsettings.json file.", - input_hint=InputHints.ignoring_input, - ) - ) - - return await step_context.next(None) - message_text = ( - str(step_context.options) - if step_context.options - else "What can I help you with today?" - ) - prompt_message = MessageFactory.text( - message_text, message_text, InputHints.expecting_input - ) - - return await step_context.prompt( - TextPrompt.__name__, PromptOptions(prompt=prompt_message) - ) - - async def act_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - if not self._luis_recognizer.is_configured: - # LUIS is not configured, we just run the BookingDialog path with an empty BookingDetailsInstance. - return await step_context.begin_dialog( - self._booking_dialog_id, BookingDetails() - ) - - # Call LUIS and gather any potential booking details. (Note the TurnContext has the response to the prompt.) - intent, luis_result = await LuisHelper.execute_luis_query( - self._luis_recognizer, step_context.context - ) - - if intent == Intent.BOOK_FLIGHT.value and luis_result: - # Show a warning for Origin and Destination if we can't resolve them. - await MainDialog._show_warning_for_unsupported_cities( - step_context.context, luis_result - ) - - # Run the BookingDialog giving it whatever details we have from the LUIS call. - return await step_context.begin_dialog(self._booking_dialog_id, luis_result) - - if intent == Intent.GET_WEATHER.value: - get_weather_text = "TODO: get weather flow here" - get_weather_message = MessageFactory.text( - get_weather_text, get_weather_text, InputHints.ignoring_input - ) - await step_context.context.send_activity(get_weather_message) - - else: - didnt_understand_text = ( - "Sorry, I didn't get that. Please try asking in a different way" - ) - didnt_understand_message = MessageFactory.text( - didnt_understand_text, didnt_understand_text, InputHints.ignoring_input - ) - await step_context.context.send_activity(didnt_understand_message) - - return await step_context.next(None) - - async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - # If the child dialog ("BookingDialog") was cancelled or the user failed to confirm, - # the Result here will be null. - if step_context.result is not None: - result = step_context.result - - # Now we have all the booking details call the booking service. - - # If the call to the booking service was successful tell the user. - # time_property = Timex(result.travel_date) - # travel_date_msg = time_property.to_natural_language(datetime.now()) - msg_txt = f"I have you booked to {result.destination} from {result.origin} on {result.travel_date}" - message = MessageFactory.text(msg_txt, msg_txt, InputHints.ignoring_input) - await step_context.context.send_activity(message) - - prompt_message = "What else can I do for you?" - return await step_context.replace_dialog(self.id, prompt_message) - - @staticmethod - async def _show_warning_for_unsupported_cities( - context: TurnContext, luis_result: BookingDetails - ) -> None: - if luis_result.unsupported_airports: - message_text = ( - f"Sorry but the following airports are not supported:" - f" {', '.join(luis_result.unsupported_airports)}" - ) - message = MessageFactory.text( - message_text, message_text, InputHints.ignoring_input - ) - await context.send_activity(message) diff --git a/samples/13.core-bot/flight_booking_recognizer.py b/samples/13.core-bot/flight_booking_recognizer.py deleted file mode 100644 index 7476103c7..000000000 --- a/samples/13.core-bot/flight_booking_recognizer.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.ai.luis import LuisApplication, LuisRecognizer -from botbuilder.core import Recognizer, RecognizerResult, TurnContext - - -class FlightBookingRecognizer(Recognizer): - def __init__(self, configuration: dict): - self._recognizer = None - - luis_is_configured = ( - configuration["LUIS_APP_ID"] - and configuration["LUIS_API_KEY"] - and configuration["LUIS_API_HOST_NAME"] - ) - if luis_is_configured: - luis_application = LuisApplication( - configuration["LUIS_APP_ID"], - configuration["LUIS_API_KEY"], - "https://" + configuration["LUIS_API_HOST_NAME"], - ) - - self._recognizer = LuisRecognizer(luis_application) - - @property - def is_configured(self) -> bool: - # Returns true if luis is configured in the appsettings.json and initialized. - return self._recognizer is not None - - async def recognize(self, turn_context: TurnContext) -> RecognizerResult: - return await self._recognizer.recognize(turn_context) diff --git a/samples/13.core-bot/helpers/__init__.py b/samples/13.core-bot/helpers/__init__.py deleted file mode 100644 index 699f8693c..000000000 --- a/samples/13.core-bot/helpers/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from . import activity_helper, luis_helper, dialog_helper - -__all__ = ["activity_helper", "dialog_helper", "luis_helper"] diff --git a/samples/13.core-bot/helpers/activity_helper.py b/samples/13.core-bot/helpers/activity_helper.py deleted file mode 100644 index 29a24823e..000000000 --- a/samples/13.core-bot/helpers/activity_helper.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from datetime import datetime -from botbuilder.schema import ( - Activity, - ActivityTypes, - ChannelAccount, - ConversationAccount, -) - - -def create_activity_reply(activity: Activity, text: str = None, locale: str = None): - - return Activity( - type=ActivityTypes.message, - timestamp=datetime.utcnow(), - from_property=ChannelAccount( - id=getattr(activity.recipient, "id", None), - name=getattr(activity.recipient, "name", None), - ), - recipient=ChannelAccount( - id=activity.from_property.id, name=activity.from_property.name - ), - reply_to_id=activity.id, - service_url=activity.service_url, - channel_id=activity.channel_id, - conversation=ConversationAccount( - is_group=activity.conversation.is_group, - id=activity.conversation.id, - name=activity.conversation.name, - ), - text=text or "", - locale=locale or "", - attachments=[], - entities=[], - ) diff --git a/samples/13.core-bot/helpers/dialog_helper.py b/samples/13.core-bot/helpers/dialog_helper.py deleted file mode 100644 index 6b2646b0b..000000000 --- a/samples/13.core-bot/helpers/dialog_helper.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import StatePropertyAccessor, TurnContext -from botbuilder.dialogs import Dialog, DialogSet, DialogTurnStatus - - -class DialogHelper: - @staticmethod - async def run_dialog( - dialog: Dialog, turn_context: TurnContext, accessor: StatePropertyAccessor - ): - dialog_set = DialogSet(accessor) - dialog_set.add(dialog) - - dialog_context = await dialog_set.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dialog_context.begin_dialog(dialog.id) diff --git a/samples/13.core-bot/helpers/luis_helper.py b/samples/13.core-bot/helpers/luis_helper.py deleted file mode 100644 index 3e28bc47e..000000000 --- a/samples/13.core-bot/helpers/luis_helper.py +++ /dev/null @@ -1,102 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from enum import Enum -from typing import Dict -from botbuilder.ai.luis import LuisRecognizer -from botbuilder.core import IntentScore, TopIntent, TurnContext - -from booking_details import BookingDetails - - -class Intent(Enum): - BOOK_FLIGHT = "BookFlight" - CANCEL = "Cancel" - GET_WEATHER = "GetWeather" - NONE_INTENT = "NoneIntent" - - -def top_intent(intents: Dict[Intent, dict]) -> TopIntent: - max_intent = Intent.NONE_INTENT - max_value = 0.0 - - for intent, value in intents: - intent_score = IntentScore(value) - if intent_score.score > max_value: - max_intent, max_value = intent, intent_score.score - - return TopIntent(max_intent, max_value) - - -class LuisHelper: - @staticmethod - async def execute_luis_query( - luis_recognizer: LuisRecognizer, turn_context: TurnContext - ) -> (Intent, object): - """ - Returns an object with preformatted LUIS results for the bot's dialogs to consume. - """ - result = None - intent = None - - try: - recognizer_result = await luis_recognizer.recognize(turn_context) - - intent = ( - sorted( - recognizer_result.intents, - key=recognizer_result.intents.get, - reverse=True, - )[:1][0] - if recognizer_result.intents - else None - ) - - if intent == Intent.BOOK_FLIGHT.value: - result = BookingDetails() - - # We need to get the result from the LUIS JSON which at every level returns an array. - to_entities = recognizer_result.entities.get("$instance", {}).get( - "To", [] - ) - if len(to_entities) > 0: - if recognizer_result.entities.get("To", [{"$instance": {}}])[0][ - "$instance" - ]: - result.destination = to_entities[0]["text"].capitalize() - else: - result.unsupported_airports.append( - to_entities[0]["text"].capitalize() - ) - - from_entities = recognizer_result.entities.get("$instance", {}).get( - "From", [] - ) - if len(from_entities) > 0: - if recognizer_result.entities.get("From", [{"$instance": {}}])[0][ - "$instance" - ]: - result.origin = from_entities[0]["text"].capitalize() - else: - result.unsupported_airports.append( - from_entities[0]["text"].capitalize() - ) - - # This value will be a TIMEX. And we are only interested in a Date so grab the first result and drop - # the Time part. TIMEX is a format that represents DateTime expressions that include some ambiguity. - # e.g. missing a Year. - date_entities = recognizer_result.entities.get("datetime", []) - if date_entities: - timex = date_entities[0]["timex"] - - if timex: - datetime = timex[0].split("T")[0] - - result.travel_date = datetime - - else: - result.travel_date = None - - except Exception as exception: - print(exception) - - return intent, result diff --git a/samples/13.core-bot/requirements.txt b/samples/13.core-bot/requirements.txt deleted file mode 100644 index c11eb2923..000000000 --- a/samples/13.core-bot/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -botbuilder-dialogs>=4.4.0.b1 -botbuilder-ai>=4.4.0.b1 -datatypes-date-time>=1.0.0.a2 -flask>=1.0.3 - diff --git a/samples/15.handling-attachments/README.md b/samples/15.handling-attachments/README.md deleted file mode 100644 index 678b34c11..000000000 --- a/samples/15.handling-attachments/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Handling Attachments - -Bot Framework v4 handling attachments bot sample - -This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to send outgoing attachments and how to save attachments to disk. - -## Running the sample -- Clone the repository -```bash -git clone https://github.com/Microsoft/botbuilder-python.git -``` -- Activate your desired virtual environment -- Bring up a terminal, navigate to `botbuilder-python\samples\15.handling-attachments` folder -- In the terminal, type `pip install -r requirements.txt` -- In the terminal, type `python app.py` - -## Testing the bot using Bot Framework Emulator -[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to bot using Bot Framework Emulator -- Launch Bot Framework Emulator -- File -> Open Bot -- Paste this URL in the emulator window - http://localhost:3978/api/messages - -## Attachments - -A message exchange between user and bot may contain cards and media attachments, such as images, video, audio, and files. -The types of attachments that may be sent and received varies by channel. Additionally, a bot may also receive file attachments. - -## Further reading - -- [Bot Framework Documentation](https://docs.botframework.com) -- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) -- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) -- [Attachments](https://docs.microsoft.com/en-us/azure/bot-service/nodejs/bot-builder-nodejs-send-receive-attachments?view=azure-bot-service-4.0) -- [Channels and Bot Connector Service](https://docs.microsoft.com/en-us/azure/bot-service/bot-concepts?view=azure-bot-service-4.0) diff --git a/samples/15.handling-attachments/app.py b/samples/15.handling-attachments/app.py deleted file mode 100644 index 47758a1e3..000000000 --- a/samples/15.handling-attachments/app.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import asyncio -import sys -from datetime import datetime - -from flask import Flask, request, Response -from botbuilder.core import ( - BotFrameworkAdapterSettings, - TurnContext, - BotFrameworkAdapter, -) -from botbuilder.schema import Activity, ActivityTypes - -from bots import AttachmentsBot - -# Create the loop and Flask app -LOOP = asyncio.get_event_loop() -app = Flask(__name__, instance_relative_config=True) -app.config.from_object("config.DefaultConfig") - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings(app.config["APP_ID"], app.config["APP_PASSWORD"]) -ADAPTER = BotFrameworkAdapter(SETTINGS) - - -# Catch-all for errors. -async def on_error(context: TurnContext, error: Exception): - # This check writes out errors to console log .vs. app insights. - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - - # Send a message to the user - await context.send_activity("The bot encountered an error or bug.") - await context.send_activity( - "To continue to run this bot, please fix the bot source code." - ) - # Send a trace activity if we're talking to the Bot Framework Emulator - if context.activity.channel_id == "emulator": - # Create a trace activity that contains the error object - trace_activity = Activity( - label="TurnError", - name="on_turn_error Trace", - timestamp=datetime.utcnow(), - type=ActivityTypes.trace, - value=f"{error}", - value_type="https://www.botframework.com/schemas/error", - ) - # Send a trace activity, which will be displayed in Bot Framework Emulator - await context.send_activity(trace_activity) - - -ADAPTER.on_turn_error = on_error - -# Create the Bot -BOT = AttachmentsBot() - -# Listen for incoming requests on /api/messages -@app.route("/api/messages", methods=["POST"]) -def messages(): - # Main bot message handler. - if "application/json" in request.headers["Content-Type"]: - body = request.json - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = ( - request.headers["Authorization"] if "Authorization" in request.headers else "" - ) - - try: - task = LOOP.create_task( - ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - ) - LOOP.run_until_complete(task) - return Response(status=201) - except Exception as exception: - raise exception - - -if __name__ == "__main__": - try: - app.run(debug=False, port=app.config["PORT"]) # nosec debug - except Exception as exception: - raise exception diff --git a/samples/15.handling-attachments/bots/__init__.py b/samples/15.handling-attachments/bots/__init__.py deleted file mode 100644 index 28e703782..000000000 --- a/samples/15.handling-attachments/bots/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .attachments_bot import AttachmentsBot - -__all__ = ["AttachmentsBot"] diff --git a/samples/15.handling-attachments/bots/attachments_bot.py b/samples/15.handling-attachments/bots/attachments_bot.py deleted file mode 100644 index 51fd8bb50..000000000 --- a/samples/15.handling-attachments/bots/attachments_bot.py +++ /dev/null @@ -1,218 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os -import urllib.parse -import urllib.request -import base64 -import json - -from botbuilder.core import ActivityHandler, MessageFactory, TurnContext, CardFactory -from botbuilder.schema import ( - ChannelAccount, - HeroCard, - CardAction, - ActivityTypes, - Attachment, - AttachmentData, - Activity, - ActionTypes, -) - - -class AttachmentsBot(ActivityHandler): - """ - Represents a bot that processes incoming activities. - For each user interaction, an instance of this class is created and the OnTurnAsync method is called. - This is a Transient lifetime service. Transient lifetime services are created - each time they're requested. For each Activity received, a new instance of this - class is created. Objects that are expensive to construct, or have a lifetime - beyond the single turn, should be carefully managed. - """ - - async def on_members_added_activity( - self, members_added: [ChannelAccount], turn_context: TurnContext - ): - await self._send_welcome_message(turn_context) - - async def on_message_activity(self, turn_context: TurnContext): - if ( - turn_context.activity.attachments - and len(turn_context.activity.attachments) > 0 - ): - await self._handle_incoming_attachment(turn_context) - else: - await self._handle_outgoing_attachment(turn_context) - - await self._display_options(turn_context) - - async def _send_welcome_message(self, turn_context: TurnContext): - """ - Greet the user and give them instructions on how to interact with the bot. - :param turn_context: - :return: - """ - for member in turn_context.activity.members_added: - if member.id != turn_context.activity.recipient.id: - await turn_context.send_activity( - f"Welcome to AttachmentsBot {member.name}. This bot will introduce " - f"you to Attachments. Please select an option" - ) - await self._display_options(turn_context) - - async def _handle_incoming_attachment(self, turn_context: TurnContext): - """ - Handle attachments uploaded by users. The bot receives an Attachment in an Activity. - The activity has a List of attachments. - Not all channels allow users to upload files. Some channels have restrictions - on file type, size, and other attributes. Consult the documentation for the channel for - more information. For example Skype's limits are here - . - :param turn_context: - :return: - """ - for attachment in turn_context.activity.attachments: - attachment_info = await self._download_attachment_and_write(attachment) - if "filename" in attachment_info: - await turn_context.send_activity( - f"Attachment {attachment_info['filename']} has been received to {attachment_info['local_path']}" - ) - - async def _download_attachment_and_write(self, attachment: Attachment) -> dict: - """ - Retrieve the attachment via the attachment's contentUrl. - :param attachment: - :return: Dict: keys "filename", "local_path" - """ - try: - response = urllib.request.urlopen(attachment.content_url) - headers = response.info() - - # If user uploads JSON file, this prevents it from being written as - # "{"type":"Buffer","data":[123,13,10,32,32,34,108..." - if headers["content-type"] == "application/json": - data = bytes(json.load(response)["data"]) - else: - data = response.read() - - local_filename = os.path.join(os.getcwd(), attachment.name) - with open(local_filename, "wb") as out_file: - out_file.write(data) - - return {"filename": attachment.name, "local_path": local_filename} - except Exception as exception: - print(exception) - return {} - - async def _handle_outgoing_attachment(self, turn_context: TurnContext): - reply = Activity(type=ActivityTypes.message) - - first_char = turn_context.activity.text[0] - if first_char == "1": - reply.text = "This is an inline attachment." - reply.attachments = [self._get_inline_attachment()] - elif first_char == "2": - reply.text = "This is an internet attachment." - reply.attachments = [self._get_internet_attachment()] - elif first_char == "3": - reply.text = "This is an uploaded attachment." - reply.attachments = [await self._get_upload_attachment(turn_context)] - else: - reply.text = "Your input was not recognized, please try again." - - await turn_context.send_activity(reply) - - async def _display_options(self, turn_context: TurnContext): - """ - Create a HeroCard with options for the user to interact with the bot. - :param turn_context: - :return: - """ - - # Note that some channels require different values to be used in order to get buttons to display text. - # In this code the emulator is accounted for with the 'title' parameter, but in other channels you may - # need to provide a value for other parameters like 'text' or 'displayText'. - card = HeroCard( - text="You can upload an image or select one of the following choices", - buttons=[ - CardAction( - type=ActionTypes.im_back, title="1. Inline Attachment", value="1" - ), - CardAction( - type=ActionTypes.im_back, title="2. Internet Attachment", value="2" - ), - CardAction( - type=ActionTypes.im_back, title="3. Uploaded Attachment", value="3" - ), - ], - ) - - reply = MessageFactory.attachment(CardFactory.hero_card(card)) - await turn_context.send_activity(reply) - - def _get_inline_attachment(self) -> Attachment: - """ - Creates an inline attachment sent from the bot to the user using a base64 string. - Using a base64 string to send an attachment will not work on all channels. - Additionally, some channels will only allow certain file types to be sent this way. - For example a .png file may work but a .pdf file may not on some channels. - Please consult the channel documentation for specifics. - :return: Attachment - """ - file_path = os.path.join(os.getcwd(), "resources/architecture-resize.png") - with open(file_path, "rb") as in_file: - base64_image = base64.b64encode(in_file.read()).decode() - - return Attachment( - name="architecture-resize.png", - content_type="image/png", - content_url=f"data:image/png;base64,{base64_image}", - ) - - async def _get_upload_attachment(self, turn_context: TurnContext) -> Attachment: - """ - Creates an "Attachment" to be sent from the bot to the user from an uploaded file. - :param turn_context: - :return: Attachment - """ - with open( - os.path.join(os.getcwd(), "resources/architecture-resize.png"), "rb" - ) as in_file: - image_data = in_file.read() - - connector = turn_context.adapter.create_connector_client( - turn_context.activity.service_url - ) - conversation_id = turn_context.activity.conversation.id - response = await connector.conversations.upload_attachment( - conversation_id, - AttachmentData( - name="architecture-resize.png", - original_base64=image_data, - type="image/png", - ), - ) - - base_uri: str = connector.config.base_url - attachment_uri = ( - base_uri - + ("" if base_uri.endswith("/") else "/") - + f"v3/attachments/{response.id}/views/original" - ) - - return Attachment( - name="architecture-resize.png", - content_type="image/png", - content_url=attachment_uri, - ) - - def _get_internet_attachment(self) -> Attachment: - """ - Creates an Attachment to be sent from the bot to the user from a HTTP URL. - :return: Attachment - """ - return Attachment( - name="architecture-resize.png", - content_type="image/png", - content_url="https://docs.microsoft.com/en-us/bot-framework/media/how-it-works/architecture-resize.png", - ) diff --git a/samples/15.handling-attachments/config.py b/samples/15.handling-attachments/config.py deleted file mode 100644 index e007d0fa9..000000000 --- a/samples/15.handling-attachments/config.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - -""" Bot Configuration """ - - -class DefaultConfig: - """ Bot Configuration """ - - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") diff --git a/samples/15.handling-attachments/deploymentTemplates/template-with-preexisting-rg.json b/samples/15.handling-attachments/deploymentTemplates/template-with-preexisting-rg.json deleted file mode 100644 index bff8c096d..000000000 --- a/samples/15.handling-attachments/deploymentTemplates/template-with-preexisting-rg.json +++ /dev/null @@ -1,242 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "appId": { - "type": "string", - "metadata": { - "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." - } - }, - "appSecret": { - "type": "string", - "metadata": { - "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." - } - }, - "botId": { - "type": "string", - "metadata": { - "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." - } - }, - "botSku": { - "defaultValue": "F0", - "type": "string", - "metadata": { - "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." - } - }, - "newAppServicePlanName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "The name of the new App Service Plan." - } - }, - "newAppServicePlanSku": { - "type": "object", - "defaultValue": { - "name": "S1", - "tier": "Standard", - "size": "S1", - "family": "S", - "capacity": 1 - }, - "metadata": { - "description": "The SKU of the App Service Plan. Defaults to Standard values." - } - }, - "appServicePlanLocation": { - "type": "string", - "metadata": { - "description": "The location of the App Service Plan." - } - }, - "existingAppServicePlan": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Name of the existing App Service Plan used to create the Web App for the bot." - } - }, - "newWebAppName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." - } - } - }, - "variables": { - "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", - "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", - "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", - "publishingUsername": "[concat('$', parameters('newWebAppName'))]", - "resourcesLocation": "[parameters('appServicePlanLocation')]", - "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", - "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", - "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" - }, - "resources": [ - { - "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", - "type": "Microsoft.Web/serverfarms", - "apiVersion": "2016-09-01", - "name": "[variables('servicePlanName')]", - "location": "[variables('resourcesLocation')]", - "sku": "[parameters('newAppServicePlanSku')]", - "kind": "linux", - "properties": { - "name": "[variables('servicePlanName')]", - "perSiteScaling": false, - "reserved": true, - "targetWorkerCount": 0, - "targetWorkerSizeId": 0 - } - }, - { - "comments": "Create a Web App using a Linux App Service Plan", - "type": "Microsoft.Web/sites", - "apiVersion": "2016-08-01", - "name": "[variables('webAppName')]", - "location": "[variables('resourcesLocation')]", - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]" - ], - "kind": "app,linux", - "properties": { - "enabled": true, - "hostNameSslStates": [ - { - "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", - "sslState": "Disabled", - "hostType": "Standard" - }, - { - "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", - "sslState": "Disabled", - "hostType": "Repository" - } - ], - "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]", - "reserved": true, - "scmSiteAlsoStopped": false, - "clientAffinityEnabled": false, - "clientCertEnabled": false, - "hostNamesDisabled": false, - "containerSize": 0, - "dailyMemoryTimeQuota": 0, - "httpsOnly": false, - "siteConfig": { - "appSettings": [ - { - "name": "MicrosoftAppId", - "value": "[parameters('appId')]" - }, - { - "name": "MicrosoftAppPassword", - "value": "[parameters('appSecret')]" - }, - { - "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", - "value": "true" - } - ], - "cors": { - "allowedOrigins": [ - "https://botservice.hosting.portal.azure.net", - "https://hosting.onecloud.azure-test.net/" - ] - } - } - } - }, - { - "type": "Microsoft.Web/sites/config", - "apiVersion": "2016-08-01", - "name": "[concat(variables('webAppName'), '/web')]", - "location": "[variables('resourcesLocation')]", - "dependsOn": [ - "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" - ], - "properties": { - "numberOfWorkers": 1, - "defaultDocuments": [ - "Default.htm", - "Default.html", - "Default.asp", - "index.htm", - "index.html", - "iisstart.htm", - "default.aspx", - "index.php", - "hostingstart.html" - ], - "netFrameworkVersion": "v4.0", - "phpVersion": "", - "pythonVersion": "", - "nodeVersion": "", - "linuxFxVersion": "PYTHON|3.7", - "requestTracingEnabled": false, - "remoteDebuggingEnabled": false, - "remoteDebuggingVersion": "VS2017", - "httpLoggingEnabled": true, - "logsDirectorySizeLimit": 35, - "detailedErrorLoggingEnabled": false, - "publishingUsername": "[variables('publishingUsername')]", - "scmType": "None", - "use32BitWorkerProcess": true, - "webSocketsEnabled": false, - "alwaysOn": false, - "appCommandLine": "", - "managedPipelineMode": "Integrated", - "virtualApplications": [ - { - "virtualPath": "/", - "physicalPath": "site\\wwwroot", - "preloadEnabled": false, - "virtualDirectories": null - } - ], - "winAuthAdminState": 0, - "winAuthTenantState": 0, - "customAppPoolIdentityAdminState": false, - "customAppPoolIdentityTenantState": false, - "loadBalancing": "LeastRequests", - "routingRules": [], - "experiments": { - "rampUpRules": [] - }, - "autoHealEnabled": false, - "vnetName": "", - "minTlsVersion": "1.2", - "ftpsState": "AllAllowed", - "reservedInstanceCount": 0 - } - }, - { - "apiVersion": "2017-12-01", - "type": "Microsoft.BotService/botServices", - "name": "[parameters('botId')]", - "location": "global", - "kind": "bot", - "sku": { - "name": "[parameters('botSku')]" - }, - "properties": { - "name": "[parameters('botId')]", - "displayName": "[parameters('botId')]", - "endpoint": "[variables('botEndpoint')]", - "msaAppId": "[parameters('appId')]", - "developerAppInsightsApplicationId": null, - "developerAppInsightKey": null, - "publishingCredentials": null, - "storageResourceId": null - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" - ] - } - ] -} \ No newline at end of file diff --git a/samples/15.handling-attachments/requirements.txt b/samples/15.handling-attachments/requirements.txt deleted file mode 100644 index eca52e268..000000000 --- a/samples/15.handling-attachments/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -jsonpickle -botbuilder-core>=4.4.0b1 -flask>=1.0.3 diff --git a/samples/15.handling-attachments/resources/architecture-resize.png b/samples/15.handling-attachments/resources/architecture-resize.png deleted file mode 100644 index 43419ed44e0aa6ab88b50239b8e0fdfeb4087186..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 241516 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

Bq>K7N*zY|DnT>$V&M1|VU&Ud0Hix-|jc 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س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%0DY0^mU%clPUS%y? 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

g*#|TO|2COZQ}3mdoNanQ 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

J7{GK^izu_n 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¥Br&n`R;Z;fYM@^$5C`pPIsD_}$y(oA2FL=n^ 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�^V945f>MfzRtVkY2HwMj;Ef(AwIq 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 z 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 diff --git a/samples/16.proactive-messages/README.md b/samples/16.proactive-messages/README.md deleted file mode 100644 index 109fb4085..000000000 --- a/samples/16.proactive-messages/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# Proactive messages - -Bot Framework v4 proactive messages bot sample - -This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to send proactive messages to users by capturing a conversation reference, then using it later to initialize outbound messages. - -## Concepts introduced in this sample - -Typically, each message that a bot sends to the user directly relates to the user's prior input. In some cases, a bot may need to send the user a message that is not directly related to the current topic of conversation. These types of messages are called proactive messages. - -Proactive messages can be useful in a variety of scenarios. If a bot sets a timer or reminder, it will need to notify the user when the time arrives. Or, if a bot receives a notification from an external system, it may need to communicate that information to the user immediately. For example, if the user has previously asked the bot to monitor the price of a product, the bot can alert the user if the price of the product has dropped by 20%. Or, if a bot requires some time to compile a response to the user's question, it may inform the user of the delay and allow the conversation to continue in the meantime. When the bot finishes compiling the response to the question, it will share that information with the user. - -This project has a notify endpoint that will trigger the proactive messages to be sent to -all users who have previously messaged the bot. - -## Running the sample -- Clone the repository -```bash -git clone https://github.com/Microsoft/botbuilder-python.git -``` -- Activate your desired virtual environment -- Bring up a terminal, navigate to `botbuilder-python\samples\16.proactive-messages` folder -- In the terminal, type `pip install -r requirements.txt` -- In the terminal, type `python app.py` - -## Testing the bot using Bot Framework Emulator -[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to bot using Bot Framework Emulator -- Launch Bot Framework Emulator -- File -> Open Bot -- Paste this URL in the emulator window - http://localhost:3978/api/messages - -With the Bot Framework Emulator connected to your running bot, the sample will now respond to an HTTP GET that will trigger a proactive message. The proactive message can be triggered from the command line using `curl` or similar tooling, or can be triggered by opening a browser windows and nagivating to `http://localhost:3978/api/notify`. - -### Using curl - -- Send a get request to `http://localhost:3978/api/notify` to proactively message users from the bot. - - ```bash - curl get http://localhost:3978/api/notify - ``` - -- Using the Bot Framework Emulator, notice a message was proactively sent to the user from the bot. - -### Using the Browser - -- Launch a web browser -- Navigate to `http://localhost:3978/api/notify` -- Using the Bot Framework Emulator, notice a message was proactively sent to the user from the bot. - -## Proactive Messages - -In addition to responding to incoming messages, bots are frequently called on to send "proactive" messages based on activity, scheduled tasks, or external events. - -In order to send a proactive message using Bot Framework, the bot must first capture a conversation reference from an incoming message using `TurnContext.get_conversation_reference()`. This reference can be stored for later use. - -To send proactive messages, acquire a conversation reference, then use `adapter.continue_conversation()` to create a TurnContext object that will allow the bot to deliver the new outgoing message. - -## Further reading - -- [Bot Framework Documentation](https://docs.botframework.com) -- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) -- [Send proactive messages](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-proactive-message?view=azure-bot-service-4.0&tabs=js) diff --git a/samples/16.proactive-messages/app.py b/samples/16.proactive-messages/app.py deleted file mode 100644 index 62ddb40c9..000000000 --- a/samples/16.proactive-messages/app.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import asyncio -import sys -import uuid -from datetime import datetime -from typing import Dict - -from flask import Flask, request, Response -from botbuilder.core import ( - BotFrameworkAdapterSettings, - TurnContext, - BotFrameworkAdapter, -) -from botbuilder.schema import Activity, ActivityTypes, ConversationReference - -from bots import ProactiveBot - -# Create the loop and Flask app -LOOP = asyncio.get_event_loop() -APP = Flask(__name__, instance_relative_config=True) -APP.config.from_object("config.DefaultConfig") - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"]) -ADAPTER = BotFrameworkAdapter(SETTINGS) - - -# Catch-all for errors. -async def on_error(context: TurnContext, error: Exception): - # This check writes out errors to console log .vs. app insights. - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - - # Send a message to the user - await context.send_activity("The bot encountered an error or bug.") - await context.send_activity( - "To continue to run this bot, please fix the bot source code." - ) - # Send a trace activity if we're talking to the Bot Framework Emulator - if context.activity.channel_id == "emulator": - # Create a trace activity that contains the error object - trace_activity = Activity( - label="TurnError", - name="on_turn_error Trace", - timestamp=datetime.utcnow(), - type=ActivityTypes.trace, - value=f"{error}", - value_type="https://www.botframework.com/schemas/error", - ) - # Send a trace activity, which will be displayed in Bot Framework Emulator - await context.send_activity(trace_activity) - - -ADAPTER.on_turn_error = on_error - -# Create a shared dictionary. The Bot will add conversation references when users -# join the conversation and send messages. -CONVERSATION_REFERENCES: Dict[str, ConversationReference] = dict() - -# If the channel is the Emulator, and authentication is not in use, the AppId will be null. -# We generate a random AppId for this case only. This is not required for production, since -# the AppId will have a value. -APP_ID = SETTINGS.app_id if SETTINGS.app_id else uuid.uuid4() - -# Create the Bot -BOT = ProactiveBot(CONVERSATION_REFERENCES) - -# Listen for incoming requests on /api/messages. -@APP.route("/api/messages", methods=["POST"]) -def messages(): - # Main bot message handler. - if "application/json" in request.headers["Content-Type"]: - body = request.json - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = ( - request.headers["Authorization"] if "Authorization" in request.headers else "" - ) - - try: - task = LOOP.create_task( - ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - ) - LOOP.run_until_complete(task) - return Response(status=201) - except Exception as exception: - raise exception - - -# Listen for requests on /api/notify, and send a messages to all conversation members. -@APP.route("/api/notify") -def notify(): - try: - task = LOOP.create_task(_send_proactive_message()) - LOOP.run_until_complete(task) - - return Response(status=201, response="Proactive messages have been sent") - except Exception as exception: - raise exception - - -# Send a message to all conversation members. -# This uses the shared Dictionary that the Bot adds conversation references to. -async def _send_proactive_message(): - for conversation_reference in CONVERSATION_REFERENCES.values(): - return await ADAPTER.continue_conversation( - conversation_reference, - lambda turn_context: turn_context.send_activity("proactive hello"), - APP_ID, - ) - - -if __name__ == "__main__": - try: - APP.run(debug=False, port=APP.config["PORT"]) # nosec debug - except Exception as exception: - raise exception diff --git a/samples/16.proactive-messages/bots/__init__.py b/samples/16.proactive-messages/bots/__init__.py deleted file mode 100644 index 72c8ccc0c..000000000 --- a/samples/16.proactive-messages/bots/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .proactive_bot import ProactiveBot - -__all__ = ["ProactiveBot"] diff --git a/samples/16.proactive-messages/bots/proactive_bot.py b/samples/16.proactive-messages/bots/proactive_bot.py deleted file mode 100644 index c65626899..000000000 --- a/samples/16.proactive-messages/bots/proactive_bot.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from typing import Dict - -from botbuilder.core import ActivityHandler, TurnContext -from botbuilder.schema import ChannelAccount, ConversationReference, Activity - - -class ProactiveBot(ActivityHandler): - def __init__(self, conversation_references: Dict[str, ConversationReference]): - self.conversation_references = conversation_references - - async def on_conversation_update_activity(self, turn_context: TurnContext): - self._add_conversation_reference(turn_context.activity) - return await super().on_conversation_update_activity(turn_context) - - async def on_members_added_activity( - self, members_added: [ChannelAccount], turn_context: TurnContext - ): - for member in members_added: - if member.id != turn_context.activity.recipient.id: - await turn_context.send_activity( - "Welcome to the Proactive Bot sample. Navigate to " - "http://localhost:3978/api/notify to proactively message everyone " - "who has previously messaged this bot." - ) - - async def on_message_activity(self, turn_context: TurnContext): - self._add_conversation_reference(turn_context.activity) - return await turn_context.send_activity( - f"You sent: {turn_context.activity.text}" - ) - - def _add_conversation_reference(self, activity: Activity): - """ - This populates the shared Dictionary that holds conversation references. In this sample, - this dictionary is used to send a message to members when /api/notify is hit. - :param activity: - :return: - """ - conversation_reference = TurnContext.get_conversation_reference(activity) - self.conversation_references[ - conversation_reference.user.id - ] = conversation_reference diff --git a/samples/16.proactive-messages/config.py b/samples/16.proactive-messages/config.py deleted file mode 100644 index e007d0fa9..000000000 --- a/samples/16.proactive-messages/config.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - -""" Bot Configuration """ - - -class DefaultConfig: - """ Bot Configuration """ - - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") diff --git a/samples/16.proactive-messages/requirements.txt b/samples/16.proactive-messages/requirements.txt deleted file mode 100644 index 7e54b62ec..000000000 --- a/samples/16.proactive-messages/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -botbuilder-core>=4.4.0b1 -flask>=1.0.3 diff --git a/samples/17.multilingual-bot/README.md b/samples/17.multilingual-bot/README.md deleted file mode 100644 index 41666b6f3..000000000 --- a/samples/17.multilingual-bot/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Multilingual Bot - -Bot Framework v4 multilingual bot sample - -This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to translate incoming and outgoing text using a custom middleware and the [Microsoft Translator Text API](https://docs.microsoft.com/en-us/azure/cognitive-services/translator/). - -## Concepts introduced in this sample - -Translation Middleware: We create a translation middleware that can translate text from bot to user and from user to bot, allowing the creation of multi-lingual bots. - -The middleware is driven by user state. This means that users can specify their language preference, and the middleware automatically will intercept messages back and forth and present them to the user in their preferred language. - -Users can change their language preference anytime, and since this gets written to the user state, the middleware will read this state and instantly modify its behavior to honor the newly selected preferred language. - -The [Microsoft Translator Text API](https://docs.microsoft.com/en-us/azure/cognitive-services/translator/), Microsoft Translator Text API is a cloud-based machine translation service. With this API you can translate text in near real-time from any app or service through a simple REST API call. -The API uses the most modern neural machine translation technology, as well as offering statistical machine translation technology. - -## Running the sample -- Clone the repository -```bash -git clone https://github.com/Microsoft/botbuilder-python.git -``` -- Activate your desired virtual environment -- Bring up a terminal, navigate to `botbuilder-python\samples\17.multilingual-bot` folder -- In the terminal, type `pip install -r requirements.txt` - -- To consume the Microsoft Translator Text API, first obtain a key following the instructions in the [Microsoft Translator Text API documentation](https://docs.microsoft.com/en-us/azure/cognitive-services/translator/translator-text-how-to-signup). Paste the key in the `SUBSCRIPTION_KEY` and `SUBSCRIPTION_REGION` settings in the `config.py` file. - -- In the terminal, type `python app.py` - -## Testing the bot using Bot Framework Emulator -[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to bot using Bot Framework Emulator -- Launch Bot Framework Emulator -- File -> Open Bot -- Paste this URL in the emulator window - http://localhost:3978/api/messages - - -### Creating a custom middleware - -Translation Middleware: We create a translation middleware than can translate text from bot to user and from user to bot, allowing the creation of multilingual bots. -Users can specify their language preference, which is stored in the user state. The translation middleware translates to and from the user's preferred language. - -### Microsoft Translator Text API - -The [Microsoft Translator Text API](https://docs.microsoft.com/en-us/azure/cognitive-services/translator/), Microsoft Translator Text API is a cloud-based machine translation service. With this API you can translate text in near real-time from any app or service through a simple REST API call. -The API uses the most modern neural machine translation technology, as well as offering statistical machine translation technology. - -# Further reading - -- [Bot Framework Documentation](https://docs.botframework.com) -- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) -- [Bot State](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-storage-concept?view=azure-bot-service-4.0) -- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) -- [Channels and Bot Connector Service](https://docs.microsoft.com/en-us/azure/bot-service/bot-concepts?view=azure-bot-service-4.0) \ No newline at end of file diff --git a/samples/17.multilingual-bot/app.py b/samples/17.multilingual-bot/app.py deleted file mode 100644 index bdba1af1a..000000000 --- a/samples/17.multilingual-bot/app.py +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import asyncio -import sys -from datetime import datetime - -from flask import Flask, request, Response -from botbuilder.core import ( - BotFrameworkAdapter, - BotFrameworkAdapterSettings, - MemoryStorage, - TurnContext, - UserState, -) -from botbuilder.schema import Activity, ActivityTypes - -from bots import MultiLingualBot - -# Create the loop and Flask app -from translation import TranslationMiddleware, MicrosoftTranslator - -LOOP = asyncio.get_event_loop() -app = Flask(__name__, instance_relative_config=True) -app.config.from_object("config.DefaultConfig") - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings(app.config["APP_ID"], app.config["APP_PASSWORD"]) -ADAPTER = BotFrameworkAdapter(SETTINGS) - - -# Catch-all for errors. -async def on_error(context: TurnContext, error: Exception): - # This check writes out errors to console log .vs. app insights. - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - - # Send a message to the user - await context.send_activity("The bot encountered an error or bug.") - await context.send_activity( - "To continue to run this bot, please fix the bot source code." - ) - # Send a trace activity if we're talking to the Bot Framework Emulator - if context.activity.channel_id == "emulator": - # Create a trace activity that contains the error object - trace_activity = Activity( - label="TurnError", - name="on_turn_error Trace", - timestamp=datetime.utcnow(), - type=ActivityTypes.trace, - value=f"{error}", - value_type="https://www.botframework.com/schemas/error", - ) - # Send a trace activity, which will be displayed in Bot Framework Emulator - await context.send_activity(trace_activity) - - -ADAPTER.on_turn_error = on_error - -# Create MemoryStorage and state -MEMORY = MemoryStorage() -USER_STATE = UserState(MEMORY) - -# Create translation middleware and add to adapter -TRANSLATOR = MicrosoftTranslator( - app.config["SUBSCRIPTION_KEY"], app.config["SUBSCRIPTION_REGION"] -) -TRANSLATION_MIDDLEWARE = TranslationMiddleware(TRANSLATOR, USER_STATE) -ADAPTER.use(TRANSLATION_MIDDLEWARE) - -# Create Bot -BOT = MultiLingualBot(USER_STATE) - - -# Listen for incoming requests on /api/messages. -@app.route("/api/messages", methods=["POST"]) -def messages(): - # Main bot message handler. - if "application/json" in request.headers["Content-Type"]: - body = request.json - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = ( - request.headers["Authorization"] if "Authorization" in request.headers else "" - ) - - try: - task = LOOP.create_task( - ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - ) - LOOP.run_until_complete(task) - return Response(status=201) - except Exception as exception: - raise exception - - -if __name__ == "__main__": - try: - app.run(debug=False, port=app.config["PORT"]) # nosec debug - except Exception as exception: - raise exception diff --git a/samples/17.multilingual-bot/bots/__init__.py b/samples/17.multilingual-bot/bots/__init__.py deleted file mode 100644 index 377f4a8ec..000000000 --- a/samples/17.multilingual-bot/bots/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .multilingual_bot import MultiLingualBot - -__all__ = ["MultiLingualBot"] diff --git a/samples/17.multilingual-bot/bots/multilingual_bot.py b/samples/17.multilingual-bot/bots/multilingual_bot.py deleted file mode 100644 index b2bcf24fa..000000000 --- a/samples/17.multilingual-bot/bots/multilingual_bot.py +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import json -import os - -from botbuilder.core import ( - ActivityHandler, - TurnContext, - UserState, - CardFactory, - MessageFactory, -) -from botbuilder.schema import ( - ChannelAccount, - Attachment, - SuggestedActions, - CardAction, - ActionTypes, -) - -from translation.translation_settings import TranslationSettings - - -class MultiLingualBot(ActivityHandler): - """ - This bot demonstrates how to use Microsoft Translator. - More information can be found at: - https://docs.microsoft.com/en-us/azure/cognitive-services/translator/translator-info-overview" - """ - - def __init__(self, user_state: UserState): - if user_state is None: - raise TypeError( - "[MultiLingualBot]: Missing parameter. user_state is required but None was given" - ) - - self.user_state = user_state - - self.language_preference_accessor = self.user_state.create_property( - "LanguagePreference" - ) - - async def on_members_added_activity( - self, members_added: [ChannelAccount], turn_context: TurnContext - ): - # Greet anyone that was not the target (recipient) of this message. - # To learn more about Adaptive Cards, see https://aka.ms/msbot-adaptivecards for more details. - for member in members_added: - if member.id != turn_context.activity.recipient.id: - await turn_context.send_activity( - MessageFactory.attachment(self._create_adaptive_card_attachment()) - ) - await turn_context.send_activity( - "This bot will introduce you to translation middleware. Say 'hi' to get started." - ) - - async def on_message_activity(self, turn_context: TurnContext): - if self._is_language_change_requested(turn_context.activity.text): - # If the user requested a language change through the suggested actions with values "es" or "en", - # simply change the user's language preference in the user state. - # The translation middleware will catch this setting and translate both ways to the user's - # selected language. - # If Spanish was selected by the user, the reply below will actually be shown in Spanish to the user. - current_language = turn_context.activity.text.lower() - if current_language in ( - TranslationSettings.english_english.value, TranslationSettings.spanish_english.value - ): - lang = TranslationSettings.english_english.value - else: - lang = TranslationSettings.english_spanish.value - - await self.language_preference_accessor.set(turn_context, lang) - - await turn_context.send_activity(f"Your current language code is: {lang}") - - # Save the user profile updates into the user state. - await self.user_state.save_changes(turn_context) - else: - # Show the user the possible options for language. If the user chooses a different language - # than the default, then the translation middleware will pick it up from the user state and - # translate messages both ways, i.e. user to bot and bot to user. - reply = MessageFactory.text("Choose your language:") - reply.suggested_actions = SuggestedActions( - actions=[ - CardAction( - title="Español", - type=ActionTypes.post_back, - value=TranslationSettings.english_spanish.value, - ), - CardAction( - title="English", - type=ActionTypes.post_back, - value=TranslationSettings.english_english.value, - ), - ] - ) - - await turn_context.send_activity(reply) - - def _create_adaptive_card_attachment(self) -> Attachment: - """ - Load attachment from file. - :return: - """ - card_path = os.path.join(os.getcwd(), "cards/welcomeCard.json") - with open(card_path, "rt") as in_file: - card_data = json.load(in_file) - - return CardFactory.adaptive_card(card_data) - - def _is_language_change_requested(self, utterance: str) -> bool: - if not utterance: - return False - - utterance = utterance.lower() - return utterance in ( - TranslationSettings.english_spanish.value, - TranslationSettings.english_english.value, - TranslationSettings.spanish_spanish.value, - TranslationSettings.spanish_english.value - ) diff --git a/samples/17.multilingual-bot/cards/welcomeCard.json b/samples/17.multilingual-bot/cards/welcomeCard.json deleted file mode 100644 index 100aa5287..000000000 --- a/samples/17.multilingual-bot/cards/welcomeCard.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", - "type": "AdaptiveCard", - "version": "1.0", - "body": [ - { - "type": "Image", - "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU", - "size": "stretch" - }, - { - "type": "TextBlock", - "spacing": "medium", - "size": "default", - "weight": "bolder", - "text": "Welcome to Bot Framework!", - "wrap": true, - "maxLines": 0 - }, - { - "type": "TextBlock", - "size": "default", - "isSubtle": "true", - "text": "Now that you have successfully run your bot, follow the links in this Adaptive Card to expand your knowledge of Bot Framework.", - "wrap": true, - "maxLines": 0 - } - ], - "actions": [ - { - "type": "Action.OpenUrl", - "title": "Get an overview", - "url": "https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0" - }, - { - "type": "Action.OpenUrl", - "title": "Ask a question", - "url": "https://stackoverflow.com/questions/tagged/botframework" - }, - { - "type": "Action.OpenUrl", - "title": "Learn how to deploy", - "url": "https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-deploy-azure?view=azure-bot-service-4.0" - } - ] -} \ No newline at end of file diff --git a/samples/17.multilingual-bot/config.py b/samples/17.multilingual-bot/config.py deleted file mode 100644 index 7d323dda5..000000000 --- a/samples/17.multilingual-bot/config.py +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - -""" Bot Configuration """ - - -class DefaultConfig: - """ Bot Configuration """ - - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") - SUBSCRIPTION_KEY = os.environ.get("SubscriptionKey", "") - SUBSCRIPTION_REGION = os.environ.get("SubscriptionRegion", "") diff --git a/samples/17.multilingual-bot/deploymentTemplates/template-with-preexisting-rg.json b/samples/17.multilingual-bot/deploymentTemplates/template-with-preexisting-rg.json deleted file mode 100644 index bff8c096d..000000000 --- a/samples/17.multilingual-bot/deploymentTemplates/template-with-preexisting-rg.json +++ /dev/null @@ -1,242 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "appId": { - "type": "string", - "metadata": { - "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." - } - }, - "appSecret": { - "type": "string", - "metadata": { - "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." - } - }, - "botId": { - "type": "string", - "metadata": { - "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." - } - }, - "botSku": { - "defaultValue": "F0", - "type": "string", - "metadata": { - "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." - } - }, - "newAppServicePlanName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "The name of the new App Service Plan." - } - }, - "newAppServicePlanSku": { - "type": "object", - "defaultValue": { - "name": "S1", - "tier": "Standard", - "size": "S1", - "family": "S", - "capacity": 1 - }, - "metadata": { - "description": "The SKU of the App Service Plan. Defaults to Standard values." - } - }, - "appServicePlanLocation": { - "type": "string", - "metadata": { - "description": "The location of the App Service Plan." - } - }, - "existingAppServicePlan": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Name of the existing App Service Plan used to create the Web App for the bot." - } - }, - "newWebAppName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." - } - } - }, - "variables": { - "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", - "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", - "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", - "publishingUsername": "[concat('$', parameters('newWebAppName'))]", - "resourcesLocation": "[parameters('appServicePlanLocation')]", - "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", - "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", - "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" - }, - "resources": [ - { - "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", - "type": "Microsoft.Web/serverfarms", - "apiVersion": "2016-09-01", - "name": "[variables('servicePlanName')]", - "location": "[variables('resourcesLocation')]", - "sku": "[parameters('newAppServicePlanSku')]", - "kind": "linux", - "properties": { - "name": "[variables('servicePlanName')]", - "perSiteScaling": false, - "reserved": true, - "targetWorkerCount": 0, - "targetWorkerSizeId": 0 - } - }, - { - "comments": "Create a Web App using a Linux App Service Plan", - "type": "Microsoft.Web/sites", - "apiVersion": "2016-08-01", - "name": "[variables('webAppName')]", - "location": "[variables('resourcesLocation')]", - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]" - ], - "kind": "app,linux", - "properties": { - "enabled": true, - "hostNameSslStates": [ - { - "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", - "sslState": "Disabled", - "hostType": "Standard" - }, - { - "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", - "sslState": "Disabled", - "hostType": "Repository" - } - ], - "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]", - "reserved": true, - "scmSiteAlsoStopped": false, - "clientAffinityEnabled": false, - "clientCertEnabled": false, - "hostNamesDisabled": false, - "containerSize": 0, - "dailyMemoryTimeQuota": 0, - "httpsOnly": false, - "siteConfig": { - "appSettings": [ - { - "name": "MicrosoftAppId", - "value": "[parameters('appId')]" - }, - { - "name": "MicrosoftAppPassword", - "value": "[parameters('appSecret')]" - }, - { - "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", - "value": "true" - } - ], - "cors": { - "allowedOrigins": [ - "https://botservice.hosting.portal.azure.net", - "https://hosting.onecloud.azure-test.net/" - ] - } - } - } - }, - { - "type": "Microsoft.Web/sites/config", - "apiVersion": "2016-08-01", - "name": "[concat(variables('webAppName'), '/web')]", - "location": "[variables('resourcesLocation')]", - "dependsOn": [ - "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" - ], - "properties": { - "numberOfWorkers": 1, - "defaultDocuments": [ - "Default.htm", - "Default.html", - "Default.asp", - "index.htm", - "index.html", - "iisstart.htm", - "default.aspx", - "index.php", - "hostingstart.html" - ], - "netFrameworkVersion": "v4.0", - "phpVersion": "", - "pythonVersion": "", - "nodeVersion": "", - "linuxFxVersion": "PYTHON|3.7", - "requestTracingEnabled": false, - "remoteDebuggingEnabled": false, - "remoteDebuggingVersion": "VS2017", - "httpLoggingEnabled": true, - "logsDirectorySizeLimit": 35, - "detailedErrorLoggingEnabled": false, - "publishingUsername": "[variables('publishingUsername')]", - "scmType": "None", - "use32BitWorkerProcess": true, - "webSocketsEnabled": false, - "alwaysOn": false, - "appCommandLine": "", - "managedPipelineMode": "Integrated", - "virtualApplications": [ - { - "virtualPath": "/", - "physicalPath": "site\\wwwroot", - "preloadEnabled": false, - "virtualDirectories": null - } - ], - "winAuthAdminState": 0, - "winAuthTenantState": 0, - "customAppPoolIdentityAdminState": false, - "customAppPoolIdentityTenantState": false, - "loadBalancing": "LeastRequests", - "routingRules": [], - "experiments": { - "rampUpRules": [] - }, - "autoHealEnabled": false, - "vnetName": "", - "minTlsVersion": "1.2", - "ftpsState": "AllAllowed", - "reservedInstanceCount": 0 - } - }, - { - "apiVersion": "2017-12-01", - "type": "Microsoft.BotService/botServices", - "name": "[parameters('botId')]", - "location": "global", - "kind": "bot", - "sku": { - "name": "[parameters('botSku')]" - }, - "properties": { - "name": "[parameters('botId')]", - "displayName": "[parameters('botId')]", - "endpoint": "[variables('botEndpoint')]", - "msaAppId": "[parameters('appId')]", - "developerAppInsightsApplicationId": null, - "developerAppInsightKey": null, - "publishingCredentials": null, - "storageResourceId": null - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" - ] - } - ] -} \ No newline at end of file diff --git a/samples/17.multilingual-bot/requirements.txt b/samples/17.multilingual-bot/requirements.txt deleted file mode 100644 index 32e489163..000000000 --- a/samples/17.multilingual-bot/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -requests -botbuilder-core>=4.4.0b1 -flask>=1.0.3 diff --git a/samples/17.multilingual-bot/translation/__init__.py b/samples/17.multilingual-bot/translation/__init__.py deleted file mode 100644 index 7112f41c0..000000000 --- a/samples/17.multilingual-bot/translation/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .microsoft_translator import MicrosoftTranslator -from .translation_middleware import TranslationMiddleware - -__all__ = ["MicrosoftTranslator", "TranslationMiddleware"] diff --git a/samples/17.multilingual-bot/translation/microsoft_translator.py b/samples/17.multilingual-bot/translation/microsoft_translator.py deleted file mode 100644 index 9af148fc6..000000000 --- a/samples/17.multilingual-bot/translation/microsoft_translator.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import uuid -import requests - - -class MicrosoftTranslator: - def __init__(self, subscription_key: str, subscription_region: str): - self.subscription_key = subscription_key - self.subscription_region = subscription_region - - # Don't forget to replace with your Cog Services location! - # Our Flask route will supply two arguments: text_input and language_output. - # When the translate text button is pressed in our Flask app, the Ajax request - # will grab these values from our web app, and use them in the request. - # See main.js for Ajax calls. - async def translate(self, text_input, language_output): - base_url = "https://api.cognitive.microsofttranslator.com" - path = "/translate?api-version=3.0" - params = "&to=" + language_output - constructed_url = base_url + path + params - - headers = { - "Ocp-Apim-Subscription-Key": self.subscription_key, - "Ocp-Apim-Subscription-Region": self.subscription_region, - "Content-type": "application/json", - "X-ClientTraceId": str(uuid.uuid4()), - } - - # You can pass more than one object in body. - body = [{"text": text_input}] - response = requests.post(constructed_url, headers=headers, json=body) - json_response = response.json() - - # for this sample, return the first translation - return json_response[0]["translations"][0]["text"] diff --git a/samples/17.multilingual-bot/translation/translation_middleware.py b/samples/17.multilingual-bot/translation/translation_middleware.py deleted file mode 100644 index b983b2acb..000000000 --- a/samples/17.multilingual-bot/translation/translation_middleware.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from typing import Callable, Awaitable, List - -from botbuilder.core import Middleware, UserState, TurnContext -from botbuilder.schema import Activity, ActivityTypes - -from translation import MicrosoftTranslator -from translation.translation_settings import TranslationSettings - - -class TranslationMiddleware(Middleware): - """ - Middleware for translating text between the user and bot. - Uses the Microsoft Translator Text API. - """ - - def __init__(self, translator: MicrosoftTranslator, user_state: UserState): - self.translator = translator - self.language_preference_accessor = user_state.create_property( - "LanguagePreference" - ) - - async def on_turn( - self, context: TurnContext, logic: Callable[[TurnContext], Awaitable] - ): - """ - Processes an incoming activity. - :param context: - :param logic: - :return: - """ - translate = await self._should_translate(context) - if translate and context.activity.type == ActivityTypes.message: - context.activity.text = await self.translator.translate( - context.activity.text, TranslationSettings.default_language.value - ) - - async def aux_on_send( - ctx: TurnContext, activities: List[Activity], next_send: Callable - ): - user_language = await self.language_preference_accessor.get( - ctx, TranslationSettings.default_language.value - ) - should_translate = ( - user_language != TranslationSettings.default_language.value - ) - - # Translate messages sent to the user to user language - if should_translate: - for activity in activities: - await self._translate_message_activity(activity, user_language) - - return await next_send() - - async def aux_on_update( - ctx: TurnContext, activity: Activity, next_update: Callable - ): - user_language = await self.language_preference_accessor.get( - ctx, TranslationSettings.default_language.value - ) - should_translate = ( - user_language != TranslationSettings.default_language.value - ) - - # Translate messages sent to the user to user language - if should_translate and activity.type == ActivityTypes.message: - await self._translate_message_activity(activity, user_language) - - return await next_update() - - context.on_send_activities(aux_on_send) - context.on_update_activity(aux_on_update) - - await logic() - - async def _should_translate(self, turn_context: TurnContext) -> bool: - user_language = await self.language_preference_accessor.get( - turn_context, TranslationSettings.default_language.value - ) - return user_language != TranslationSettings.default_language.value - - async def _translate_message_activity(self, activity: Activity, target_locale: str): - if activity.type == ActivityTypes.message: - activity.text = await self.translator.translate( - activity.text, target_locale - ) diff --git a/samples/17.multilingual-bot/translation/translation_settings.py b/samples/17.multilingual-bot/translation/translation_settings.py deleted file mode 100644 index aee41542d..000000000 --- a/samples/17.multilingual-bot/translation/translation_settings.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from enum import Enum - - -class TranslationSettings(str, Enum): - default_language = "en" - english_english = "en" - english_spanish = "es" - spanish_english = "in" - spanish_spanish = "it" diff --git a/samples/18.bot-authentication/README.md b/samples/18.bot-authentication/README.md deleted file mode 100644 index 2902756f5..000000000 --- a/samples/18.bot-authentication/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# Bot Authentication - -Bot Framework v4 bot authentication sample - -This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to use authentication in your bot using OAuth. - -The sample uses the bot authentication capabilities in [Azure Bot Service](https://docs.botframework.com), providing features to make it easier to develop a bot that authenticates users to various identity providers such as Azure AD (Azure Active Directory), GitHub, Uber, etc. - -NOTE: Microsoft Teams currently differs slightly in the way auth is integrated with the bot. Refer to sample 46.teams-auth. - -## Running the sample -- Clone the repository -```bash -git clone https://github.com/Microsoft/botbuilder-python.git -``` -- Activate your desired virtual environment -- Bring up a terminal, navigate to `botbuilder-python\samples\18.bot-authentication` folder -- In the terminal, type `pip install -r requirements.txt` -- Deploy your bot to Azure, see [Deploy your bot to Azure](https://aka.ms/azuredeployment) -- [Add Authentication to your bot via Azure Bot Service](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-authentication?view=azure-bot-service-4.0&tabs=csharp) -- Modify `APP_ID`, `APP_PASSWORD`, and `CONNECTION_NAME` in `config.py` - -After Authentication has been configured via Azure Bot Service, you can test the bot. - -- In the terminal, type `python app.py` - -## Testing the bot using Bot Framework Emulator - -[Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework Emulator version 4.3.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to the bot using Bot Framework Emulator - -- Launch Bot Framework Emulator -- File -> Open Bot -- Enter a Bot URL of `http://localhost:3978/api/messages` -- Enter the app id and password - -## Authentication - -This sample uses bot authentication capabilities in Azure Bot Service, providing features to make it easier to develop a bot that authenticates users to various identity providers such as Azure AD (Azure Active Directory), GitHub, Uber, etc. These updates also take steps towards an improved user experience by eliminating the magic code verification for some clients. - -## Deploy the bot to Azure - -To learn more about deploying a bot to Azure, see [Deploy your bot to Azure](https://aka.ms/azuredeployment) for a complete list of deployment instructions. - -## Further reading - -- [Bot Framework Documentation](https://docs.botframework.com) -- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) -- [Azure Portal](https://portal.azure.com) -- [Add Authentication to Your Bot Via Azure Bot Service](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-authentication?view=azure-bot-service-4.0&tabs=csharp) -- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) -- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) -- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest) diff --git a/samples/18.bot-authentication/app.py b/samples/18.bot-authentication/app.py deleted file mode 100644 index c8910b155..000000000 --- a/samples/18.bot-authentication/app.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import asyncio -import sys -from datetime import datetime - -from flask import Flask, request, Response -from botbuilder.core import ( - BotFrameworkAdapter, - BotFrameworkAdapterSettings, - ConversationState, - MemoryStorage, - TurnContext, - UserState, -) -from botbuilder.schema import Activity, ActivityTypes - -from bots import AuthBot - -# Create the loop and Flask app -from dialogs import MainDialog - -LOOP = asyncio.get_event_loop() -app = Flask(__name__, instance_relative_config=True) -app.config.from_object("config.DefaultConfig") - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings(app.config["APP_ID"], app.config["APP_PASSWORD"]) -ADAPTER = BotFrameworkAdapter(SETTINGS) - - -# Catch-all for errors. -async def on_error(context: TurnContext, error: Exception): - # This check writes out errors to console log .vs. app insights. - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - - # Send a message to the user - await context.send_activity("The bot encountered an error or bug.") - await context.send_activity( - "To continue to run this bot, please fix the bot source code." - ) - - # Send a trace activity if we're talking to the Bot Framework Emulator - if context.activity.channel_id == "emulator": - # Create a trace activity that contains the error object - trace_activity = Activity( - label="TurnError", - name="on_turn_error Trace", - timestamp=datetime.utcnow(), - type=ActivityTypes.trace, - value=f"{error}", - value_type="https://www.botframework.com/schemas/error", - ) - # Send a trace activity, which will be displayed in Bot Framework Emulator - await context.send_activity(trace_activity) - - -ADAPTER.on_turn_error = on_error - -# Create MemoryStorage and state -MEMORY = MemoryStorage() -USER_STATE = UserState(MEMORY) -CONVERSATION_STATE = ConversationState(MEMORY) - -# Create dialog -DIALOG = MainDialog(app.config["CONNECTION_NAME"]) - -# Create Bot -BOT = AuthBot(CONVERSATION_STATE, USER_STATE, DIALOG) - - -# Listen for incoming requests on /api/messages. -@app.route("/api/messages", methods=["POST"]) -def messages(): - # Main bot message handler. - if "application/json" in request.headers["Content-Type"]: - body = request.json - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = ( - request.headers["Authorization"] if "Authorization" in request.headers else "" - ) - - try: - task = LOOP.create_task( - ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - ) - LOOP.run_until_complete(task) - return Response(status=201) - except Exception as exception: - raise exception - - -if __name__ == "__main__": - try: - app.run(debug=False, port=app.config["PORT"]) # nosec debug - except Exception as exception: - raise exception diff --git a/samples/18.bot-authentication/bots/__init__.py b/samples/18.bot-authentication/bots/__init__.py deleted file mode 100644 index d6506ffcb..000000000 --- a/samples/18.bot-authentication/bots/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .dialog_bot import DialogBot -from .auth_bot import AuthBot - -__all__ = ["DialogBot", "AuthBot"] diff --git a/samples/18.bot-authentication/bots/auth_bot.py b/samples/18.bot-authentication/bots/auth_bot.py deleted file mode 100644 index 93166f655..000000000 --- a/samples/18.bot-authentication/bots/auth_bot.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from typing import List -from botbuilder.core import ( - ConversationState, - UserState, - TurnContext, -) -from botbuilder.dialogs import Dialog -from botbuilder.schema import ChannelAccount - -from helpers.dialog_helper import DialogHelper -from .dialog_bot import DialogBot - - -class AuthBot(DialogBot): - def __init__( - self, - conversation_state: ConversationState, - user_state: UserState, - dialog: Dialog, - ): - super(AuthBot, self).__init__(conversation_state, user_state, dialog) - - async def on_members_added_activity( - self, members_added: List[ChannelAccount], turn_context: TurnContext - ): - for member in members_added: - # Greet anyone that was not the target (recipient) of this message. - # To learn more about Adaptive Cards, see https://aka.ms/msbot-adaptivecards for more details. - if member.id != turn_context.activity.recipient.id: - await turn_context.send_activity( - "Welcome to AuthenticationBot. Type anything to get logged in. Type " - "'logout' to sign-out." - ) - - async def on_token_response_event(self, turn_context: TurnContext): - # Run the Dialog with the new Token Response Event Activity. - await DialogHelper.run_dialog( - self.dialog, - turn_context, - self.conversation_state.create_property("DialogState"), - ) diff --git a/samples/18.bot-authentication/bots/dialog_bot.py b/samples/18.bot-authentication/bots/dialog_bot.py deleted file mode 100644 index eb560a1be..000000000 --- a/samples/18.bot-authentication/bots/dialog_bot.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import ActivityHandler, ConversationState, UserState, TurnContext -from botbuilder.dialogs import Dialog -from helpers.dialog_helper import DialogHelper - - -class DialogBot(ActivityHandler): - def __init__( - self, - conversation_state: ConversationState, - user_state: UserState, - dialog: Dialog, - ): - if conversation_state is None: - raise Exception( - "[DialogBot]: Missing parameter. conversation_state is required" - ) - if user_state is None: - raise Exception("[DialogBot]: Missing parameter. user_state is required") - if dialog is None: - raise Exception("[DialogBot]: Missing parameter. dialog is required") - - self.conversation_state = conversation_state - self.user_state = user_state - self.dialog = dialog - - async def on_turn(self, turn_context: TurnContext): - await super().on_turn(turn_context) - - # Save any state changes that might have occurred during the turn. - await self.conversation_state.save_changes(turn_context, False) - await self.user_state.save_changes(turn_context, False) - - async def on_message_activity(self, turn_context: TurnContext): - await DialogHelper.run_dialog( - self.dialog, - turn_context, - self.conversation_state.create_property("DialogState"), - ) diff --git a/samples/18.bot-authentication/config.py b/samples/18.bot-authentication/config.py deleted file mode 100644 index 0acc113a3..000000000 --- a/samples/18.bot-authentication/config.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - -""" Bot Configuration """ - - -class DefaultConfig: - """ Bot Configuration """ - - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") - CONNECTION_NAME = os.environ.get("ConnectionName", "") diff --git a/samples/18.bot-authentication/deploymentTemplates/template-with-preexisting-rg.json b/samples/18.bot-authentication/deploymentTemplates/template-with-preexisting-rg.json deleted file mode 100644 index bff8c096d..000000000 --- a/samples/18.bot-authentication/deploymentTemplates/template-with-preexisting-rg.json +++ /dev/null @@ -1,242 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "appId": { - "type": "string", - "metadata": { - "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." - } - }, - "appSecret": { - "type": "string", - "metadata": { - "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." - } - }, - "botId": { - "type": "string", - "metadata": { - "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." - } - }, - "botSku": { - "defaultValue": "F0", - "type": "string", - "metadata": { - "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." - } - }, - "newAppServicePlanName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "The name of the new App Service Plan." - } - }, - "newAppServicePlanSku": { - "type": "object", - "defaultValue": { - "name": "S1", - "tier": "Standard", - "size": "S1", - "family": "S", - "capacity": 1 - }, - "metadata": { - "description": "The SKU of the App Service Plan. Defaults to Standard values." - } - }, - "appServicePlanLocation": { - "type": "string", - "metadata": { - "description": "The location of the App Service Plan." - } - }, - "existingAppServicePlan": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Name of the existing App Service Plan used to create the Web App for the bot." - } - }, - "newWebAppName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." - } - } - }, - "variables": { - "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", - "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", - "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", - "publishingUsername": "[concat('$', parameters('newWebAppName'))]", - "resourcesLocation": "[parameters('appServicePlanLocation')]", - "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", - "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", - "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" - }, - "resources": [ - { - "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", - "type": "Microsoft.Web/serverfarms", - "apiVersion": "2016-09-01", - "name": "[variables('servicePlanName')]", - "location": "[variables('resourcesLocation')]", - "sku": "[parameters('newAppServicePlanSku')]", - "kind": "linux", - "properties": { - "name": "[variables('servicePlanName')]", - "perSiteScaling": false, - "reserved": true, - "targetWorkerCount": 0, - "targetWorkerSizeId": 0 - } - }, - { - "comments": "Create a Web App using a Linux App Service Plan", - "type": "Microsoft.Web/sites", - "apiVersion": "2016-08-01", - "name": "[variables('webAppName')]", - "location": "[variables('resourcesLocation')]", - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]" - ], - "kind": "app,linux", - "properties": { - "enabled": true, - "hostNameSslStates": [ - { - "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", - "sslState": "Disabled", - "hostType": "Standard" - }, - { - "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", - "sslState": "Disabled", - "hostType": "Repository" - } - ], - "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]", - "reserved": true, - "scmSiteAlsoStopped": false, - "clientAffinityEnabled": false, - "clientCertEnabled": false, - "hostNamesDisabled": false, - "containerSize": 0, - "dailyMemoryTimeQuota": 0, - "httpsOnly": false, - "siteConfig": { - "appSettings": [ - { - "name": "MicrosoftAppId", - "value": "[parameters('appId')]" - }, - { - "name": "MicrosoftAppPassword", - "value": "[parameters('appSecret')]" - }, - { - "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", - "value": "true" - } - ], - "cors": { - "allowedOrigins": [ - "https://botservice.hosting.portal.azure.net", - "https://hosting.onecloud.azure-test.net/" - ] - } - } - } - }, - { - "type": "Microsoft.Web/sites/config", - "apiVersion": "2016-08-01", - "name": "[concat(variables('webAppName'), '/web')]", - "location": "[variables('resourcesLocation')]", - "dependsOn": [ - "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" - ], - "properties": { - "numberOfWorkers": 1, - "defaultDocuments": [ - "Default.htm", - "Default.html", - "Default.asp", - "index.htm", - "index.html", - "iisstart.htm", - "default.aspx", - "index.php", - "hostingstart.html" - ], - "netFrameworkVersion": "v4.0", - "phpVersion": "", - "pythonVersion": "", - "nodeVersion": "", - "linuxFxVersion": "PYTHON|3.7", - "requestTracingEnabled": false, - "remoteDebuggingEnabled": false, - "remoteDebuggingVersion": "VS2017", - "httpLoggingEnabled": true, - "logsDirectorySizeLimit": 35, - "detailedErrorLoggingEnabled": false, - "publishingUsername": "[variables('publishingUsername')]", - "scmType": "None", - "use32BitWorkerProcess": true, - "webSocketsEnabled": false, - "alwaysOn": false, - "appCommandLine": "", - "managedPipelineMode": "Integrated", - "virtualApplications": [ - { - "virtualPath": "/", - "physicalPath": "site\\wwwroot", - "preloadEnabled": false, - "virtualDirectories": null - } - ], - "winAuthAdminState": 0, - "winAuthTenantState": 0, - "customAppPoolIdentityAdminState": false, - "customAppPoolIdentityTenantState": false, - "loadBalancing": "LeastRequests", - "routingRules": [], - "experiments": { - "rampUpRules": [] - }, - "autoHealEnabled": false, - "vnetName": "", - "minTlsVersion": "1.2", - "ftpsState": "AllAllowed", - "reservedInstanceCount": 0 - } - }, - { - "apiVersion": "2017-12-01", - "type": "Microsoft.BotService/botServices", - "name": "[parameters('botId')]", - "location": "global", - "kind": "bot", - "sku": { - "name": "[parameters('botSku')]" - }, - "properties": { - "name": "[parameters('botId')]", - "displayName": "[parameters('botId')]", - "endpoint": "[variables('botEndpoint')]", - "msaAppId": "[parameters('appId')]", - "developerAppInsightsApplicationId": null, - "developerAppInsightKey": null, - "publishingCredentials": null, - "storageResourceId": null - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" - ] - } - ] -} \ No newline at end of file diff --git a/samples/18.bot-authentication/dialogs/__init__.py b/samples/18.bot-authentication/dialogs/__init__.py deleted file mode 100644 index ab5189cd5..000000000 --- a/samples/18.bot-authentication/dialogs/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .logout_dialog import LogoutDialog -from .main_dialog import MainDialog - -__all__ = ["LogoutDialog", "MainDialog"] diff --git a/samples/18.bot-authentication/dialogs/logout_dialog.py b/samples/18.bot-authentication/dialogs/logout_dialog.py deleted file mode 100644 index de77e5c04..000000000 --- a/samples/18.bot-authentication/dialogs/logout_dialog.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.dialogs import DialogTurnResult, ComponentDialog, DialogContext -from botbuilder.core import BotFrameworkAdapter -from botbuilder.schema import ActivityTypes - - -class LogoutDialog(ComponentDialog): - def __init__(self, dialog_id: str, connection_name: str): - super(LogoutDialog, self).__init__(dialog_id) - - self.connection_name = connection_name - - async def on_begin_dialog( - self, inner_dc: DialogContext, options: object - ) -> DialogTurnResult: - return await inner_dc.begin_dialog(self.initial_dialog_id, options) - - async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult: - return await inner_dc.continue_dialog() - - async def _interrupt(self, inner_dc: DialogContext): - if inner_dc.context.activity.type == ActivityTypes.message: - text = inner_dc.context.activity.text.lower() - if text == "logout": - bot_adapter: BotFrameworkAdapter = inner_dc.context.adapter - await bot_adapter.sign_out_user(inner_dc.context, self.connection_name) - return await inner_dc.cancel_all_dialogs() diff --git a/samples/18.bot-authentication/dialogs/main_dialog.py b/samples/18.bot-authentication/dialogs/main_dialog.py deleted file mode 100644 index 964a3aff2..000000000 --- a/samples/18.bot-authentication/dialogs/main_dialog.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import MessageFactory -from botbuilder.dialogs import ( - WaterfallDialog, - WaterfallStepContext, - DialogTurnResult, - PromptOptions, -) -from botbuilder.dialogs.prompts import OAuthPrompt, OAuthPromptSettings, ConfirmPrompt - -from dialogs import LogoutDialog - - -class MainDialog(LogoutDialog): - def __init__(self, connection_name: str): - super(MainDialog, self).__init__(MainDialog.__name__, connection_name) - - self.add_dialog( - OAuthPrompt( - OAuthPrompt.__name__, - OAuthPromptSettings( - connection_name=connection_name, - text="Please Sign In", - title="Sign In", - timeout=300000, - ), - ) - ) - - self.add_dialog(ConfirmPrompt(ConfirmPrompt.__name__)) - - self.add_dialog( - WaterfallDialog( - "WFDialog", - [ - self.prompt_step, - self.login_step, - self.display_token_phase1, - self.display_token_phase2, - ], - ) - ) - - self.initial_dialog_id = "WFDialog" - - async def prompt_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - return await step_context.begin_dialog(OAuthPrompt.__name__) - - async def login_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - # Get the token from the previous step. Note that we could also have gotten the - # token directly from the prompt itself. There is an example of this in the next method. - if step_context.result: - await step_context.context.send_activity("You are now logged in.") - return await step_context.prompt( - ConfirmPrompt.__name__, - PromptOptions( - prompt=MessageFactory.text("Would you like to view your token?") - ), - ) - - await step_context.context.send_activity( - "Login was not successful please try again." - ) - return await step_context.end_dialog() - - async def display_token_phase1( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - await step_context.context.send_activity("Thank you.") - - if step_context.result: - # Call the prompt again because we need the token. The reasons for this are: - # 1. If the user is already logged in we do not need to store the token locally in the bot and worry - # about refreshing it. We can always just call the prompt again to get the token. - # 2. We never know how long it will take a user to respond. By the time the - # user responds the token may have expired. The user would then be prompted to login again. - # - # There is no reason to store the token locally in the bot because we can always just call - # the OAuth prompt to get the token or get a new token if needed. - return await step_context.begin_dialog(OAuthPrompt.__name__) - - return await step_context.end_dialog() - - async def display_token_phase2( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - if step_context.result: - await step_context.context.send_activity( - f"Here is your token {step_context.result['token']}" - ) - - return await step_context.end_dialog() diff --git a/samples/18.bot-authentication/helpers/__init__.py b/samples/18.bot-authentication/helpers/__init__.py deleted file mode 100644 index a824eb8f4..000000000 --- a/samples/18.bot-authentication/helpers/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from . import dialog_helper - -__all__ = ["dialog_helper"] diff --git a/samples/18.bot-authentication/helpers/dialog_helper.py b/samples/18.bot-authentication/helpers/dialog_helper.py deleted file mode 100644 index 6b2646b0b..000000000 --- a/samples/18.bot-authentication/helpers/dialog_helper.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import StatePropertyAccessor, TurnContext -from botbuilder.dialogs import Dialog, DialogSet, DialogTurnStatus - - -class DialogHelper: - @staticmethod - async def run_dialog( - dialog: Dialog, turn_context: TurnContext, accessor: StatePropertyAccessor - ): - dialog_set = DialogSet(accessor) - dialog_set.add(dialog) - - dialog_context = await dialog_set.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dialog_context.begin_dialog(dialog.id) diff --git a/samples/18.bot-authentication/requirements.txt b/samples/18.bot-authentication/requirements.txt deleted file mode 100644 index 7e54b62ec..000000000 --- a/samples/18.bot-authentication/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -botbuilder-core>=4.4.0b1 -flask>=1.0.3 diff --git a/samples/19.custom-dialogs/README.md b/samples/19.custom-dialogs/README.md deleted file mode 100644 index 14874d971..000000000 --- a/samples/19.custom-dialogs/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Custom Dialogs - -Bot Framework v4 custom dialogs bot sample - -This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to sub-class the `Dialog` class to create different bot control mechanism like simple slot filling. - -BotFramework provides a built-in base class called `Dialog`. By subclassing `Dialog`, developers can create new ways to define and control dialog flows used by the bot. - -## Running the sample -- Clone the repository -```bash -git clone https://github.com/Microsoft/botbuilder-python.git -``` -- Activate your desired virtual environment -- Bring up a terminal, navigate to `botbuilder-python\samples\19.custom-dialogs` folder -- In the terminal, type `pip install -r requirements.txt` -- In the terminal, type `python app.py` - -## Testing the bot using Bot Framework Emulator -[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to bot using Bot Framework Emulator -- Launch Bot Framework Emulator -- File -> Open Bot -- Paste this URL in the emulator window - http://localhost:3978/api/messages - -## Custom Dialogs - -BotFramework provides a built-in base class called `Dialog`. By subclassing Dialog, developers -can create new ways to define and control dialog flows used by the bot. By adhering to the -features of this class, developers will create custom dialogs that can be used side-by-side -with other dialog types, as well as built-in or custom prompts. - -This example demonstrates a custom Dialog class called `SlotFillingDialog`, which takes a -series of "slots" which define a value the bot needs to collect from the user, as well -as the prompt it should use. The bot will iterate through all of the slots until they are -all full, at which point the dialog completes. - -# Further reading - -- [Bot Framework Documentation](https://docs.botframework.com) -- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) -- [Dialogs](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0) -- [Dialog class reference](https://docs.microsoft.com/en-us/javascript/api/botbuilder-dialogs/dialog) -- [Manage complex conversation flows with dialogs](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-dialog-manage-complex-conversation-flow?view=azure-bot-service-4.0) -- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) diff --git a/samples/19.custom-dialogs/app.py b/samples/19.custom-dialogs/app.py deleted file mode 100644 index 1c3579210..000000000 --- a/samples/19.custom-dialogs/app.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import asyncio -import sys -from datetime import datetime - -from flask import Flask, request, Response -from botbuilder.core import ( - BotFrameworkAdapter, - BotFrameworkAdapterSettings, - ConversationState, - MemoryStorage, - TurnContext, - UserState, -) -from botbuilder.schema import Activity, ActivityTypes - -from bots import DialogBot - -# Create the loop and Flask app -from dialogs.root_dialog import RootDialog - -LOOP = asyncio.get_event_loop() -APP = Flask(__name__, instance_relative_config=True) -APP.config.from_object("config.DefaultConfig") - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"]) -ADAPTER = BotFrameworkAdapter(SETTINGS) - - -# Catch-all for errors. -async def on_error(context: TurnContext, error: Exception): - # This check writes out errors to console log .vs. app insights. - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - - # Send a message to the user - await context.send_activity("The bot encountered an error or bug.") - await context.send_activity( - "To continue to run this bot, please fix the bot source code." - ) - # Send a trace activity if we're talking to the Bot Framework Emulator - if context.activity.channel_id == "emulator": - # Create a trace activity that contains the error object - trace_activity = Activity( - label="TurnError", - name="on_turn_error Trace", - timestamp=datetime.utcnow(), - type=ActivityTypes.trace, - value=f"{error}", - value_type="https://www.botframework.com/schemas/error", - ) - # Send a trace activity, which will be displayed in Bot Framework Emulator - await context.send_activity(trace_activity) - - -ADAPTER.on_turn_error = on_error - -# Create MemoryStorage and state -MEMORY = MemoryStorage() -USER_STATE = UserState(MEMORY) -CONVERSATION_STATE = ConversationState(MEMORY) - -# Create Dialog and Bot -DIALOG = RootDialog(USER_STATE) -BOT = DialogBot(CONVERSATION_STATE, USER_STATE, DIALOG) - - -# Listen for incoming requests on /api/messages. -@APP.route("/api/messages", methods=["POST"]) -def messages(): - # Main bot message handler. - if "application/json" in request.headers["Content-Type"]: - body = request.json - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = ( - request.headers["Authorization"] if "Authorization" in request.headers else "" - ) - - try: - task = LOOP.create_task( - ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - ) - LOOP.run_until_complete(task) - return Response(status=201) - except Exception as exception: - raise exception - - -if __name__ == "__main__": - try: - APP.run(debug=False, port=APP.config["PORT"]) # nosec debug - except Exception as exception: - raise exception diff --git a/samples/19.custom-dialogs/bots/__init__.py b/samples/19.custom-dialogs/bots/__init__.py deleted file mode 100644 index 306aca22c..000000000 --- a/samples/19.custom-dialogs/bots/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .dialog_bot import DialogBot - -__all__ = ["DialogBot"] diff --git a/samples/19.custom-dialogs/bots/dialog_bot.py b/samples/19.custom-dialogs/bots/dialog_bot.py deleted file mode 100644 index 2edc0dbe4..000000000 --- a/samples/19.custom-dialogs/bots/dialog_bot.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import ActivityHandler, ConversationState, TurnContext, UserState -from botbuilder.dialogs import Dialog - -from helpers.dialog_helper import DialogHelper - - -class DialogBot(ActivityHandler): - def __init__( - self, - conversation_state: ConversationState, - user_state: UserState, - dialog: Dialog, - ): - self.conversation_state = conversation_state - self.user_state = user_state - self.dialog = dialog - - async def on_turn(self, turn_context: TurnContext): - await super().on_turn(turn_context) - - # Save any state changes that might have occurred during the turn. - await self.conversation_state.save_changes(turn_context) - await self.user_state.save_changes(turn_context) - - async def on_message_activity(self, turn_context: TurnContext): - # Run the Dialog with the new message Activity. - await DialogHelper.run_dialog( - self.dialog, - turn_context, - self.conversation_state.create_property("DialogState"), - ) diff --git a/samples/19.custom-dialogs/config.py b/samples/19.custom-dialogs/config.py deleted file mode 100644 index e007d0fa9..000000000 --- a/samples/19.custom-dialogs/config.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - -""" Bot Configuration """ - - -class DefaultConfig: - """ Bot Configuration """ - - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") diff --git a/samples/19.custom-dialogs/dialogs/__init__.py b/samples/19.custom-dialogs/dialogs/__init__.py deleted file mode 100644 index 83d4d61d3..000000000 --- a/samples/19.custom-dialogs/dialogs/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .slot_filling_dialog import SlotFillingDialog -from .root_dialog import RootDialog - -__all__ = ["RootDialog", "SlotFillingDialog"] diff --git a/samples/19.custom-dialogs/dialogs/root_dialog.py b/samples/19.custom-dialogs/dialogs/root_dialog.py deleted file mode 100644 index 5d371ce6a..000000000 --- a/samples/19.custom-dialogs/dialogs/root_dialog.py +++ /dev/null @@ -1,134 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from typing import Dict -from recognizers_text import Culture - -from botbuilder.dialogs import ( - ComponentDialog, - WaterfallDialog, - WaterfallStepContext, - DialogTurnResult, - NumberPrompt, - PromptValidatorContext, -) -from botbuilder.dialogs.prompts import TextPrompt -from botbuilder.core import MessageFactory, UserState - -from dialogs import SlotFillingDialog -from dialogs.slot_details import SlotDetails - - -class RootDialog(ComponentDialog): - def __init__(self, user_state: UserState): - super(RootDialog, self).__init__(RootDialog.__name__) - - self.user_state_accessor = user_state.create_property("result") - - # Rather than explicitly coding a Waterfall we have only to declare what properties we want collected. - # In this example we will want two text prompts to run, one for the first name and one for the last - fullname_slots = [ - SlotDetails( - name="first", dialog_id="text", prompt="Please enter your first name." - ), - SlotDetails( - name="last", dialog_id="text", prompt="Please enter your last name." - ), - ] - - # This defines an address dialog that collects street, city and zip properties. - address_slots = [ - SlotDetails( - name="street", - dialog_id="text", - prompt="Please enter the street address.", - ), - SlotDetails(name="city", dialog_id="text", prompt="Please enter the city."), - SlotDetails(name="zip", dialog_id="text", prompt="Please enter the zip."), - ] - - # Dialogs can be nested and the slot filling dialog makes use of that. In this example some of the child - # dialogs are slot filling dialogs themselves. - slots = [ - SlotDetails(name="fullname", dialog_id="fullname",), - SlotDetails( - name="age", dialog_id="number", prompt="Please enter your age." - ), - SlotDetails( - name="shoesize", - dialog_id="shoesize", - prompt="Please enter your shoe size.", - retry_prompt="You must enter a size between 0 and 16. Half sizes are acceptable.", - ), - SlotDetails(name="address", dialog_id="address"), - ] - - # Add the various dialogs that will be used to the DialogSet. - self.add_dialog(SlotFillingDialog("address", address_slots)) - self.add_dialog(SlotFillingDialog("fullname", fullname_slots)) - self.add_dialog(TextPrompt("text")) - self.add_dialog(NumberPrompt("number", default_locale=Culture.English)) - self.add_dialog( - NumberPrompt( - "shoesize", - RootDialog.shoe_size_validator, - default_locale=Culture.English, - ) - ) - self.add_dialog(SlotFillingDialog("slot-dialog", slots)) - - # Defines a simple two step Waterfall to test the slot dialog. - self.add_dialog( - WaterfallDialog("waterfall", [self.start_dialog, self.process_result]) - ) - - # The initial child Dialog to run. - self.initial_dialog_id = "waterfall" - - async def start_dialog( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - # Start the child dialog. This will run the top slot dialog than will complete when all the properties are - # gathered. - return await step_context.begin_dialog("slot-dialog") - - async def process_result( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - # To demonstrate that the slot dialog collected all the properties we will echo them back to the user. - if isinstance(step_context.result, dict) and len(step_context.result) > 0: - fullname: Dict[str, object] = step_context.result["fullname"] - shoe_size: float = step_context.result["shoesize"] - address: dict = step_context.result["address"] - - # store the response on UserState - obj: dict = await self.user_state_accessor.get(step_context.context, dict) - obj["data"] = {} - obj["data"]["fullname"] = f"{fullname.get('first')} {fullname.get('last')}" - obj["data"]["shoesize"] = f"{shoe_size}" - obj["data"][ - "address" - ] = f"{address['street']}, {address['city']}, {address['zip']}" - - # show user the values - await step_context.context.send_activity( - MessageFactory.text(obj["data"]["fullname"]) - ) - await step_context.context.send_activity( - MessageFactory.text(obj["data"]["shoesize"]) - ) - await step_context.context.send_activity( - MessageFactory.text(obj["data"]["address"]) - ) - - return await step_context.end_dialog() - - @staticmethod - async def shoe_size_validator(prompt_context: PromptValidatorContext) -> bool: - shoe_size = round(prompt_context.recognized.value, 1) - - # show sizes can range from 0 to 16, whole or half sizes only - if 0 <= shoe_size <= 16 and (shoe_size * 2) % 1 == 0: - prompt_context.recognized.value = shoe_size - return True - return False diff --git a/samples/19.custom-dialogs/dialogs/slot_details.py b/samples/19.custom-dialogs/dialogs/slot_details.py deleted file mode 100644 index 172d81c67..000000000 --- a/samples/19.custom-dialogs/dialogs/slot_details.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import MessageFactory -from botbuilder.dialogs import PromptOptions - - -class SlotDetails: - def __init__( - self, - name: str, - dialog_id: str, - options: PromptOptions = None, - prompt: str = None, - retry_prompt: str = None, - ): - self.name = name - self.dialog_id = dialog_id - self.options = ( - options - if options - else PromptOptions( - prompt=MessageFactory.text(prompt), - retry_prompt=None - if retry_prompt is None - else MessageFactory.text(retry_prompt), - ) - ) diff --git a/samples/19.custom-dialogs/dialogs/slot_filling_dialog.py b/samples/19.custom-dialogs/dialogs/slot_filling_dialog.py deleted file mode 100644 index 6e354431a..000000000 --- a/samples/19.custom-dialogs/dialogs/slot_filling_dialog.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from typing import List, Dict - -from botbuilder.dialogs import ( - DialogContext, - DialogTurnResult, - Dialog, - DialogInstance, - DialogReason, -) -from botbuilder.schema import ActivityTypes - -from dialogs.slot_details import SlotDetails - - -class SlotFillingDialog(Dialog): - """ - This is an example of implementing a custom Dialog class. This is similar to the Waterfall dialog in the - framework; however, it is based on a Dictionary rather than a sequential set of functions. The dialog is defined - by a list of 'slots', each slot represents a property we want to gather and the dialog we will be using to - collect it. Often the property is simply an atomic piece of data such as a number or a date. But sometimes the - property is itself a complex object, in which case we can use the slot dialog to collect that compound property. - """ - - def __init__(self, dialog_id: str, slots: List[SlotDetails]): - super(SlotFillingDialog, self).__init__(dialog_id) - - # Custom dialogs might define their own custom state. Similarly to the Waterfall dialog we will have a set of - # values in the ConversationState. However, rather than persisting an index we will persist the last property - # we prompted for. This way when we resume this code following a prompt we will have remembered what property - # we were filling. - self.SLOT_NAME = "slot" - self.PERSISTED_VALUES = "values" - - # The list of slots defines the properties to collect and the dialogs to use to collect them. - self.slots = slots - - async def begin_dialog( - self, dialog_context: "DialogContext", options: object = None - ): - if dialog_context.context.activity.type != ActivityTypes.message: - return await dialog_context.end_dialog({}) - return await self._run_prompt(dialog_context) - - async def continue_dialog(self, dialog_context: "DialogContext"): - if dialog_context.context.activity.type != ActivityTypes.message: - return Dialog.end_of_turn - return await self._run_prompt(dialog_context) - - async def resume_dialog( - self, dialog_context: DialogContext, reason: DialogReason, result: object - ): - slot_name = dialog_context.active_dialog.state[self.SLOT_NAME] - values = self._get_persisted_values(dialog_context.active_dialog) - values[slot_name] = result - - return await self._run_prompt(dialog_context) - - async def _run_prompt(self, dialog_context: DialogContext) -> DialogTurnResult: - """ - This helper function contains the core logic of this dialog. The main idea is to compare the state we have - gathered with the list of slots we have been asked to fill. When we find an empty slot we execute the - corresponding prompt. - :param dialog_context: - :return: - """ - state = self._get_persisted_values(dialog_context.active_dialog) - - # Run through the list of slots until we find one that hasn't been filled yet. - unfilled_slot = None - for slot_detail in self.slots: - if slot_detail.name not in state: - unfilled_slot = slot_detail - break - - # If we have an unfilled slot we will try to fill it - if unfilled_slot: - # The name of the slot we will be prompting to fill. - dialog_context.active_dialog.state[self.SLOT_NAME] = unfilled_slot.name - - # Run the child dialog - return await dialog_context.begin_dialog( - unfilled_slot.dialog_id, unfilled_slot.options - ) - - # No more slots to fill so end the dialog. - return await dialog_context.end_dialog(state) - - def _get_persisted_values( - self, dialog_instance: DialogInstance - ) -> Dict[str, object]: - obj = dialog_instance.state.get(self.PERSISTED_VALUES) - - if not obj: - obj = {} - dialog_instance.state[self.PERSISTED_VALUES] = obj - - return obj diff --git a/samples/19.custom-dialogs/helpers/__init__.py b/samples/19.custom-dialogs/helpers/__init__.py deleted file mode 100644 index a824eb8f4..000000000 --- a/samples/19.custom-dialogs/helpers/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from . import dialog_helper - -__all__ = ["dialog_helper"] diff --git a/samples/19.custom-dialogs/helpers/dialog_helper.py b/samples/19.custom-dialogs/helpers/dialog_helper.py deleted file mode 100644 index 6b2646b0b..000000000 --- a/samples/19.custom-dialogs/helpers/dialog_helper.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import StatePropertyAccessor, TurnContext -from botbuilder.dialogs import Dialog, DialogSet, DialogTurnStatus - - -class DialogHelper: - @staticmethod - async def run_dialog( - dialog: Dialog, turn_context: TurnContext, accessor: StatePropertyAccessor - ): - dialog_set = DialogSet(accessor) - dialog_set.add(dialog) - - dialog_context = await dialog_set.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dialog_context.begin_dialog(dialog.id) diff --git a/samples/19.custom-dialogs/requirements.txt b/samples/19.custom-dialogs/requirements.txt deleted file mode 100644 index 7e54b62ec..000000000 --- a/samples/19.custom-dialogs/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -botbuilder-core>=4.4.0b1 -flask>=1.0.3 diff --git a/samples/21.corebot-app-insights/NOTICE.md b/samples/21.corebot-app-insights/NOTICE.md deleted file mode 100644 index 056c7237f..000000000 --- a/samples/21.corebot-app-insights/NOTICE.md +++ /dev/null @@ -1,8 +0,0 @@ -## NOTICE - -Please note that while the 21.corebot-app-insights sample is licensed under the MIT license, the sample has dependencies that use other types of licenses. - -Since Microsoft does not modify nor distribute these dependencies, it is the sole responsibility of the user to determine the correct/compliant usage of these dependencies. Please refer to the -[bot requirements](./bot/requirements.txt), [model requirements](./model/setup.py) and [model runtime requirements](./model_runtime_svc/setup.py) for a list of the **direct** dependencies. - -Please also note that the sample depends on the `requests` package, which has a dependency `chardet` that uses LGPL license. \ No newline at end of file diff --git a/samples/21.corebot-app-insights/README-LUIS.md b/samples/21.corebot-app-insights/README-LUIS.md deleted file mode 100644 index 61bde7719..000000000 --- a/samples/21.corebot-app-insights/README-LUIS.md +++ /dev/null @@ -1,216 +0,0 @@ -# Setting up LUIS via CLI: - -This README contains information on how to create and deploy a LUIS application. When the bot is ready to be deployed to production, we recommend creating a LUIS Endpoint Resource for usage with your LUIS App. - -> _For instructions on how to create a LUIS Application via the LUIS portal, see these Quickstart steps:_ -> 1. _[Quickstart: Create a new app in the LUIS portal][Quickstart-create]_ -> 2. _[Quickstart: Deploy an app in the LUIS portal][Quickstart-deploy]_ - - [Quickstart-create]: https://docs.microsoft.com/azure/cognitive-services/luis/get-started-portal-build-app - [Quickstart-deploy]:https://docs.microsoft.com/azure/cognitive-services/luis/get-started-portal-deploy-app - -## Table of Contents: - -- [Prerequisites](#Prerequisites) -- [Import a new LUIS Application using a local LUIS application](#Import-a-new-LUIS-Application-using-a-local-LUIS-application) -- [How to create a LUIS Endpoint resource in Azure and pair it with a LUIS Application](#How-to-create-a-LUIS-Endpoint-resource-in-Azure-and-pair-it-with-a-LUIS-Application) - -___ - -## [Prerequisites](#Table-of-Contents): - -#### Install Azure CLI >=2.0.61: - -Visit the following page to find the correct installer for your OS: -- https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest - -#### Install LUIS CLI >=2.4.0: - -Open a CLI of your choice and type the following: - -```bash -npm i -g luis-apis@^2.4.0 -``` - -#### LUIS portal account: - -You should already have a LUIS account with either https://luis.ai, https://eu.luis.ai, or https://au.luis.ai. To determine where to create a LUIS account, consider where you will deploy your LUIS applications, and then place them in [the corresponding region][LUIS-Authoring-Regions]. - -After you've created your account, you need your [Authoring Key][LUIS-AKey] and a LUIS application ID. - - [LUIS-Authoring-Regions]: https://docs.microsoft.com/azure/cognitive-services/luis/luis-reference-regions#luis-authoring-regions] - [LUIS-AKey]: https://docs.microsoft.com/azure/cognitive-services/luis/luis-concept-keys#authoring-key - -___ - -## [Import a new LUIS Application using a local LUIS application](#Table-of-Contents) - -### 1. Import the local LUIS application to luis.ai - -```bash -luis import application --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appName "FlightBooking" --in "./cognitiveModels/FlightBooking.json" -``` - -Outputs the following JSON: - -```json -{ - "id": "########-####-####-####-############", - "name": "FlightBooking", - "description": "A LUIS model that uses intent and entities.", - "culture": "en-us", - "usageScenario": "", - "domain": "", - "versionsCount": 1, - "createdDateTime": "2019-03-29T18:32:02Z", - "endpoints": {}, - "endpointHitsCount": 0, - "activeVersion": "0.1", - "ownerEmail": "bot@contoso.com", - "tokenizerVersion": "1.0.0" -} -``` - -For the next step, you'll need the `"id"` value for `--appId` and the `"activeVersion"` value for `--versionId`. - -### 2. Train the LUIS Application - -```bash -luis train version --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appId "LuisAppId" --versionId "LuisAppversion" --wait -``` - -### 3. Publish the LUIS Application - -```bash -luis publish version --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appId "LuisAppId" --versionId "LuisAppversion" --publishRegion "LuisAppPublishRegion" -``` - -> `--region` corresponds to the region you _author_ your application in. The regions available for this are "westus", "westeurope" and "australiaeast".
-> These regions correspond to the three available portals, https://luis.ai, https://eu.luis.ai, or https://au.luis.ai.
-> `--publishRegion` corresponds to the region of the endpoint you're publishing to, (e.g. "westus", "southeastasia", "westeurope", "brazilsouth").
-> See the [reference docs][Endpoint-API] for a list of available publish/endpoint regions. - - [Endpoint-API]: https://westus.dev.cognitive.microsoft.com/docs/services/5819c76f40a6350ce09de1ac/operations/5819c77140a63516d81aee78 - -Outputs the following: - -```json - { - "versionId": "0.1", - "isStaging": false, - "endpointUrl": "https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/########-####-####-####-############", - "region": "westus", - "assignedEndpointKey": null, - "endpointRegion": "westus", - "failedRegions": "", - "publishedDateTime": "2019-03-29T18:40:32Z", - "directVersionPublish": false -} -``` - -To see how to create an LUIS Cognitive Service Resource in Azure, please see [the next README][README-LUIS]. This Resource should be used when you want to move your bot to production. The instructions will show you how to create and pair the resource with a LUIS Application. - - [README-LUIS]: ./README-LUIS.md - -___ - -## [How to create a LUIS Endpoint resource in Azure and pair it with a LUIS Application](#Table-of-Contents) - -### 1. Create a new LUIS Cognitive Services resource on Azure via Azure CLI - -> _Note:_
-> _If you don't have a Resource Group in your Azure subscription, you can create one through the Azure portal or through using:_ -> ```bash -> az group create --subscription "AzureSubscriptionGuid" --location "westus" --name "ResourceGroupName" -> ``` -> _To see a list of valid locations, use `az account list-locations`_ - - -```bash -# Use Azure CLI to create the LUIS Key resource on Azure -az cognitiveservices account create --kind "luis" --name "NewLuisResourceName" --sku "S0" --location "westus" --subscription "AzureSubscriptionGuid" -g "ResourceGroupName" -``` - -The command will output a response similar to the JSON below: - -```json -{ - "endpoint": "https://westus.api.cognitive.microsoft.com/luis/v2.0", - "etag": "\"########-####-####-####-############\"", - "id": "/subscriptions/########-####-####-####-############/resourceGroups/ResourceGroupName/providers/Microsoft.CognitiveServices/accounts/NewLuisResourceName", - "internalId": "################################", - "kind": "luis", - "location": "westus", - "name": "NewLuisResourceName", - "provisioningState": "Succeeded", - "resourceGroup": "ResourceGroupName", - "sku": { - "name": "S0", - "tier": null - }, - "tags": null, - "type": "Microsoft.CognitiveServices/accounts" -} -``` - - - -Take the output from the previous command and create a JSON file in the following format: - -```json -{ - "azureSubscriptionId": "00000000-0000-0000-0000-000000000000", - "resourceGroup": "ResourceGroupName", - "accountName": "NewLuisResourceName" -} -``` - -### 2. Retrieve ARM access token via Azure CLI - -```bash -az account get-access-token --subscription "AzureSubscriptionGuid" -``` - -This will return an object that looks like this: - -```json -{ - "accessToken": "eyJ0eXAiOiJKVtokentokentokentokentokeng1dCI6Ik4tbEMwbi05REFMcXdodUhZbkhRNjNHZUNYYyIsItokenI6Ik4tbEMwbi05REFMcXdodUhZbkhRNjNHZUNYYyJ9.eyJhdWQiOiJodHRwczovL21hbmFnZW1lbnQuY29yZS53aW5kb3dzLm5ldC8iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNTUzODc3MTUwLCJuYmYiOjE1NTM4NzcxNTAsImV4cCI6MTU1Mzg4MTA1MCwiX2NsYWltX25hbWVzIjp7Imdyb3VwcyI6InNyYzEifSwiX2NsYWltX3NvdXJjZXMiOnsic3JjMSI6eyJlbmRwb2ludCI6Imh0dHBzOi8vZ3JhcGgud2luZG93cy5uZXQvNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3L3VzZXJzL2ZmZTQyM2RkLWJhM2YtNDg0Ny04NjgyLWExNTI5MDA4MjM4Ny9nZXRNZW1iZXJPYmplY3RzIn19LCJhY3IiOiIxIiwiYWlvIjoiQVZRQXEvOEtBQUFBeGVUc201NDlhVHg4RE1mMFlRVnhGZmxxOE9RSC9PODR3QktuSmRqV1FqTkkwbmxLYzB0bHJEZzMyMFZ5bWZGaVVBSFBvNUFFUTNHL0FZNDRjdk01T3M0SEt0OVJkcE5JZW9WU0dzd0kvSkk9IiwiYW1yIjpbIndpYSIsIm1mYSJdLCJhcHBpZCI6IjA0YjA3Nzk1LThkZGItNDYxYS1iYmVlLTAyZjllMWJmN2I0NiIsImFwcGlkYWNyIjoiMCIsImRldmljZWlkIjoiNDhmNDVjNjEtMTg3Zi00MjUxLTlmZWItMTllZGFkZmMwMmE3IiwiZmFtaWx5X25hbWUiOiJHdW0iLCJnaXZlbl9uYW1lIjoiU3RldmVuIiwiaXBhZGRyIjoiMTY3LjIyMC4yLjU1IiwibmFtZSI6IlN0ZXZlbiBHdW0iLCJvaWQiOiJmZmU0MjNkZC1iYTNmLTQ4NDctODY4Mi1hMTUyOTAwODIzODciLCJvbnByZW1fc2lkIjoiUy0xLTUtMjEtMjEyNzUyMTE4NC0xNjA0MDEyOTIwLTE4ODc5Mjc1MjctMjYwOTgyODUiLCJwdWlkIjoiMTAwMzdGRkVBMDQ4NjlBNyIsInJoIjoiSSIsInNjcCI6InVzZXJfaW1wZXJzb25hdGlvbiIsInN1YiI6Ik1rMGRNMWszN0U5ckJyMjhieUhZYjZLSU85LXVFQVVkZFVhNWpkSUd1Nk0iLCJ0aWQiOiI3MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDciLCJ1bmlxdWVfbmFtZSI6InN0Z3VtQG1pY3Jvc29mdC5jb20iLCJ1cG4iOiJzdGd1bUBtaWNyb3NvZnQuY29tIiwidXRpIjoiT2w2NGN0TXY4RVNEQzZZQWRqRUFtokenInZlciI6IjEuMCJ9.kFAsEilE0mlS1pcpqxf4rEnRKeYsehyk-gz-zJHUrE__oad3QjgDSBDPrR_ikLdweynxbj86pgG4QFaHURNCeE6SzrbaIrNKw-n9jrEtokenlosOxg_0l2g1LeEUOi5Q4gQREAU_zvSbl-RY6sAadpOgNHtGvz3Rc6FZRITfkckSLmsKAOFoh-aWC6tFKG8P52rtB0qVVRz9tovBeNqkMYL49s9ypduygbXNVwSQhm5JszeWDgrFuVFHBUP_iENCQYGQpEZf_KvjmX1Ur1F9Eh9nb4yI2gFlKncKNsQl-tokenK7-tokentokentokentokentokentokenatoken", - "expiresOn": "2200-12-31 23:59:59.999999", - "subscription": "AzureSubscriptionGuid", - "tenant": "tenant-guid", - "tokenType": "Bearer" -} -``` - -The value needed for the next step is the `"accessToken"`. - -### 3. Use `luis add appazureaccount` to pair your LUIS resource with a LUIS Application - -```bash -luis add appazureaccount --in "path/to/created/requestBody.json" --appId "LuisAppId" --authoringKey "LuisAuthoringKey" --armToken "accessToken" -``` - -If successful, it should yield a response like this: - -```json -{ - "code": "Success", - "message": "Operation Successful" -} -``` - -### 4. See the LUIS Cognitive Services' keys - -```bash -az cognitiveservices account keys list --name "NewLuisResourceName" --subscription "AzureSubscriptionGuid" -g "ResourceGroupName" -``` - -This will return an object that looks like this: - -```json -{ - "key1": "9a69####dc8f####8eb4####399f####", - "key2": "####f99e####4b1a####fb3b####6b9f" -} -``` \ No newline at end of file diff --git a/samples/21.corebot-app-insights/README.md b/samples/21.corebot-app-insights/README.md deleted file mode 100644 index bea70586c..000000000 --- a/samples/21.corebot-app-insights/README.md +++ /dev/null @@ -1,65 +0,0 @@ -# CoreBot with Application Insights - -Bot Framework v4 core bot sample. - -This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to: - -- Use [LUIS](https://www.luis.ai) to implement core AI capabilities -- Implement a multi-turn conversation using Dialogs -- Handle user interruptions for such things as `Help` or `Cancel` -- Prompt for and validate requests for information from the user -- Use [Application Insights](https://docs.microsoft.com/azure/azure-monitor/app/cloudservices) to monitor your bot - -## Prerequisites - -### Install Python 3.6 - -### Overview - -This bot uses [LUIS](https://www.luis.ai), an AI based cognitive service, to implement language understanding -and [Application Insights](https://docs.microsoft.com/azure/azure-monitor/app/cloudservices), an extensible Application Performance Management (APM) service for web developers on multiple platforms. - -### Create a LUIS Application to enable language understanding - -LUIS language model setup, training, and application configuration steps can be found [here](https://docs.microsoft.com/azure/bot-service/bot-builder-howto-v4-luis?view=azure-bot-service-4.0&tabs=cs). - -If you wish to create a LUIS application via the CLI, these steps can be found in the [README-LUIS.md](README-LUIS.md). - -### Add Application Insights service to enable the bot monitoring -Application Insights resource creation steps can be found [here](https://docs.microsoft.com/azure/azure-monitor/app/create-new-resource). - -## Running the sample -- Run `pip install -r requirements.txt` to install all dependencies -- Update LuisAppId, LuisAPIKey and LuisAPIHostName in `config.py` with the information retrieved from the [LUIS portal](https://www.luis.ai) -- Update AppInsightsInstrumentationKey in `config.py` -- Run `python app.py` -- Alternatively to the last command, you can set the file in an environment variable with `set FLASK_APP=app.py` in windows (`export FLASK_APP=app.py` in mac/linux) and then run `flask run --host=127.0.0.1 --port=3978` - - -## Testing the bot using Bot Framework Emulator - -[Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework Emulator version 4.3.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to the bot using Bot Framework Emulator - -- Launch Bot Framework Emulator -- File -> Open Bot -- Enter a Bot URL of `http://localhost:3978/api/messages` - - -## Further reading - -- [Bot Framework Documentation](https://docs.botframework.com) -- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) -- [Dialogs](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0) -- [Gathering Input Using Prompts](https://docs.microsoft.com/azure/bot-service/bot-builder-prompts?view=azure-bot-service-4.0&tabs=csharp) -- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) -- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) -- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) -- [Language Understanding using LUIS](https://docs.microsoft.com/azure/cognitive-services/luis/) -- [Channels and Bot Connector Service](https://docs.microsoft.com/azure/bot-service/bot-concepts?view=azure-bot-service-4.0) -- [Application insights Overview](https://docs.microsoft.com/azure/azure-monitor/app/app-insights-overview) -- [Getting Started with Application Insights](https://github.com/Microsoft/ApplicationInsights-aspnetcore/wiki/Getting-Started-with-Application-Insights-for-ASP.NET-Core) -- [Filtering and preprocessing telemetry in the Application Insights SDK](https://docs.microsoft.com/azure/azure-monitor/app/api-filtering-sampling) diff --git a/samples/21.corebot-app-insights/app.py b/samples/21.corebot-app-insights/app.py deleted file mode 100644 index 91d2f29af..000000000 --- a/samples/21.corebot-app-insights/app.py +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -""" -This sample shows how to create a bot that demonstrates the following: -- Use [LUIS](https://www.luis.ai) to implement core AI capabilities. -- Implement a multi-turn conversation using Dialogs. -- Handle user interruptions for such things as `Help` or `Cancel`. -- Prompt for and validate requests for information from the user. - -""" - -import asyncio -import sys -from datetime import datetime - -from flask import Flask, request, Response -from botbuilder.core import ( - BotFrameworkAdapter, - BotFrameworkAdapterSettings, - ConversationState, - MemoryStorage, - UserState, - TurnContext, -) -from botbuilder.schema import Activity, ActivityTypes -from botbuilder.applicationinsights import ApplicationInsightsTelemetryClient -from botbuilder.applicationinsights.flask import BotTelemetryMiddleware - -from dialogs import MainDialog -from bots import DialogAndWelcomeBot - -# Create the loop and Flask app -LOOP = asyncio.get_event_loop() -APP = Flask(__name__, instance_relative_config=True) -APP.config.from_object("config.DefaultConfig") -APP.wsgi_app = BotTelemetryMiddleware(APP.wsgi_app) - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"]) -ADAPTER = BotFrameworkAdapter(SETTINGS) - - -# Catch-all for errors. -async def on_error(context: TurnContext, error: Exception): - # This check writes out errors to console log .vs. app insights. - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - - # Send a message to the user - await context.send_activity("The bot encountered an error or bug.") - await context.send_activity( - "To continue to run this bot, please fix the bot source code." - ) - # Send a trace activity if we're talking to the Bot Framework Emulator - if context.activity.channel_id == "emulator": - # Create a trace activity that contains the error object - trace_activity = Activity( - label="TurnError", - name="on_turn_error Trace", - timestamp=datetime.utcnow(), - type=ActivityTypes.trace, - value=f"{error}", - value_type="https://www.botframework.com/schemas/error", - ) - # Send a trace activity, which will be displayed in Bot Framework Emulator - await context.send_activity(trace_activity) - - # Clear out state - await CONVERSATION_STATE.delete(context) - - -# Set the error handler on the Adapter. -# In this case, we want an unbound method, so MethodType is not needed. -ADAPTER.on_turn_error = on_error - -# Create MemoryStorage, UserState and ConversationState -MEMORY = MemoryStorage() -USER_STATE = UserState(MEMORY) -CONVERSATION_STATE = ConversationState(MEMORY) - -# Create telemetry client -INSTRUMENTATION_KEY = APP.config["APPINSIGHTS_INSTRUMENTATION_KEY"] -TELEMETRY_CLIENT = ApplicationInsightsTelemetryClient(INSTRUMENTATION_KEY) - -# Create dialog and Bot -DIALOG = MainDialog(APP.config, telemetry_client=TELEMETRY_CLIENT) -BOT = DialogAndWelcomeBot(CONVERSATION_STATE, USER_STATE, DIALOG, TELEMETRY_CLIENT) - - -# Listen for incoming requests on /api/messages. -@APP.route("/api/messages", methods=["POST"]) -def messages(): - # Main bot message handler. - if "application/json" in request.headers["Content-Type"]: - body = request.json - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = ( - request.headers["Authorization"] if "Authorization" in request.headers else "" - ) - - try: - task = LOOP.create_task( - ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - ) - LOOP.run_until_complete(task) - return Response(status=201) - except Exception as exception: - raise exception - - -if __name__ == "__main__": - try: - APP.run(debug=True, port=APP.config["PORT"]) - - except Exception as exception: - raise exception diff --git a/samples/21.corebot-app-insights/booking_details.py b/samples/21.corebot-app-insights/booking_details.py deleted file mode 100644 index 81f420fa6..000000000 --- a/samples/21.corebot-app-insights/booking_details.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Booking detail.""" - - -class BookingDetails: - """Booking detail implementation""" - - def __init__( - self, destination: str = None, origin: str = None, travel_date: str = None - ): - self.destination = destination - self.origin = origin - self.travel_date = travel_date diff --git a/samples/21.corebot-app-insights/bots/__init__.py b/samples/21.corebot-app-insights/bots/__init__.py deleted file mode 100644 index 7c71ff86f..000000000 --- a/samples/21.corebot-app-insights/bots/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""bots module.""" - -from .dialog_bot import DialogBot -from .dialog_and_welcome_bot import DialogAndWelcomeBot - -__all__ = ["DialogBot", "DialogAndWelcomeBot"] diff --git a/samples/21.corebot-app-insights/bots/dialog_and_welcome_bot.py b/samples/21.corebot-app-insights/bots/dialog_and_welcome_bot.py deleted file mode 100644 index 80f37ea71..000000000 --- a/samples/21.corebot-app-insights/bots/dialog_and_welcome_bot.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Main dialog to welcome users.""" -import json -import os.path - -from typing import List -from botbuilder.dialogs import Dialog -from botbuilder.core import ( - TurnContext, - ConversationState, - UserState, - BotTelemetryClient, -) -from botbuilder.schema import Activity, Attachment, ChannelAccount -from helpers.activity_helper import create_activity_reply -from .dialog_bot import DialogBot - - -class DialogAndWelcomeBot(DialogBot): - """Main dialog to welcome users.""" - - def __init__( - self, - conversation_state: ConversationState, - user_state: UserState, - dialog: Dialog, - telemetry_client: BotTelemetryClient, - ): - super(DialogAndWelcomeBot, self).__init__( - conversation_state, user_state, dialog, telemetry_client - ) - self.telemetry_client = telemetry_client - - async def on_members_added_activity( - self, members_added: List[ChannelAccount], turn_context: TurnContext - ): - for member in members_added: - # Greet anyone that was not the target (recipient) of this message. - # To learn more about Adaptive Cards, see https://aka.ms/msbot-adaptivecards - # for more details. - if member.id != turn_context.activity.recipient.id: - welcome_card = self.create_adaptive_card_attachment() - response = self.create_response(turn_context.activity, welcome_card) - await turn_context.send_activity(response) - - def create_response(self, activity: Activity, attachment: Attachment): - """Create an attachment message response.""" - response = create_activity_reply(activity) - response.attachments = [attachment] - return response - - # Load attachment from file. - def create_adaptive_card_attachment(self): - """Create an adaptive card.""" - relative_path = os.path.abspath(os.path.dirname(__file__)) - path = os.path.join(relative_path, "resources/welcomeCard.json") - with open(path) as card_file: - card = json.load(card_file) - - return Attachment( - content_type="application/vnd.microsoft.card.adaptive", content=card - ) diff --git a/samples/21.corebot-app-insights/bots/dialog_bot.py b/samples/21.corebot-app-insights/bots/dialog_bot.py deleted file mode 100644 index 8c9322bc9..000000000 --- a/samples/21.corebot-app-insights/bots/dialog_bot.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Implements bot Activity handler.""" - -from botbuilder.core import ( - ActivityHandler, - ConversationState, - UserState, - TurnContext, - BotTelemetryClient, - NullTelemetryClient, -) -from botbuilder.dialogs import Dialog -from helpers.dialog_helper import DialogHelper - - -class DialogBot(ActivityHandler): - """Main activity handler for the bot.""" - - def __init__( - self, - conversation_state: ConversationState, - user_state: UserState, - dialog: Dialog, - telemetry_client: BotTelemetryClient, - ): - if conversation_state is None: - raise Exception( - "[DialogBot]: Missing parameter. conversation_state is required" - ) - if user_state is None: - raise Exception("[DialogBot]: Missing parameter. user_state is required") - if dialog is None: - raise Exception("[DialogBot]: Missing parameter. dialog is required") - - self.conversation_state = conversation_state - self.user_state = user_state - self.dialog = dialog - self.telemetry_client = telemetry_client - - async def on_turn(self, turn_context: TurnContext): - await super().on_turn(turn_context) - - # Save any state changes that might have occured during the turn. - await self.conversation_state.save_changes(turn_context, False) - await self.user_state.save_changes(turn_context, False) - - async def on_message_activity(self, turn_context: TurnContext): - await DialogHelper.run_dialog( - self.dialog, - turn_context, - self.conversation_state.create_property("DialogState"), - ) - - @property - def telemetry_client(self) -> BotTelemetryClient: - """ - Gets the telemetry client for logging events. - """ - return self._telemetry_client - - # pylint:disable=attribute-defined-outside-init - @telemetry_client.setter - def telemetry_client(self, value: BotTelemetryClient) -> None: - """ - Sets the telemetry client for logging events. - """ - if value is None: - self._telemetry_client = NullTelemetryClient() - else: - self._telemetry_client = value diff --git a/samples/21.corebot-app-insights/bots/resources/welcomeCard.json b/samples/21.corebot-app-insights/bots/resources/welcomeCard.json deleted file mode 100644 index d9a35548c..000000000 --- a/samples/21.corebot-app-insights/bots/resources/welcomeCard.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", - "type": "AdaptiveCard", - "version": "1.0", - "body": [ - { - "type": "Image", - "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU", - "size": "stretch" - }, - { - "type": "TextBlock", - "spacing": "medium", - "size": "default", - "weight": "bolder", - "text": "Welcome to Bot Framework!", - "wrap": true, - "maxLines": 0 - }, - { - "type": "TextBlock", - "size": "default", - "isSubtle": "true", - "text": "Now that you have successfully run your bot, follow the links in this Adaptive Card to expand your knowledge of Bot Framework.", - "wrap": true, - "maxLines": 0 - } - ], - "actions": [ - { - "type": "Action.OpenUrl", - "title": "Get an overview", - "url": "https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0" - }, - { - "type": "Action.OpenUrl", - "title": "Ask a question", - "url": "https://stackoverflow.com/questions/tagged/botframework" - }, - { - "type": "Action.OpenUrl", - "title": "Learn how to deploy", - "url": "https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-deploy-azure?view=azure-bot-service-4.0" - } - ] -} \ No newline at end of file diff --git a/samples/21.corebot-app-insights/cognitiveModels/FlightBooking.json b/samples/21.corebot-app-insights/cognitiveModels/FlightBooking.json deleted file mode 100644 index 5d1c9ec38..000000000 --- a/samples/21.corebot-app-insights/cognitiveModels/FlightBooking.json +++ /dev/null @@ -1,226 +0,0 @@ -{ - "luis_schema_version": "3.2.0", - "versionId": "0.1", - "name": "Airline Reservation", - "desc": "A LUIS model that uses intent and entities.", - "culture": "en-us", - "tokenizerVersion": "1.0.0", - "intents": [ - { - "name": "Book flight" - }, - { - "name": "Cancel" - }, - { - "name": "None" - } - ], - "entities": [], - "composites": [ - { - "name": "From", - "children": [ - "Airport" - ], - "roles": [] - }, - { - "name": "To", - "children": [ - "Airport" - ], - "roles": [] - } - ], - "closedLists": [ - { - "name": "Airport", - "subLists": [ - { - "canonicalForm": "Paris", - "list": [ - "paris" - ] - }, - { - "canonicalForm": "London", - "list": [ - "london" - ] - }, - { - "canonicalForm": "Berlin", - "list": [ - "berlin" - ] - }, - { - "canonicalForm": "New York", - "list": [ - "new york" - ] - } - ], - "roles": [] - } - ], - "patternAnyEntities": [], - "regex_entities": [], - "prebuiltEntities": [ - { - "name": "datetimeV2", - "roles": [] - } - ], - "model_features": [], - "regex_features": [], - "patterns": [], - "utterances": [ - { - "text": "book flight from london to paris on feb 14th", - "intent": "Book flight", - "entities": [ - { - "entity": "To", - "startPos": 27, - "endPos": 31 - }, - { - "entity": "From", - "startPos": 17, - "endPos": 22 - } - ] - }, - { - "text": "book flight to berlin on feb 14th", - "intent": "Book flight", - "entities": [ - { - "entity": "To", - "startPos": 15, - "endPos": 20 - } - ] - }, - { - "text": "book me a flight from london to paris", - "intent": "Book flight", - "entities": [ - { - "entity": "From", - "startPos": 22, - "endPos": 27 - }, - { - "entity": "To", - "startPos": 32, - "endPos": 36 - } - ] - }, - { - "text": "bye", - "intent": "Cancel", - "entities": [] - }, - { - "text": "cancel booking", - "intent": "Cancel", - "entities": [] - }, - { - "text": "exit", - "intent": "Cancel", - "entities": [] - }, - { - "text": "flight to paris", - "intent": "Book flight", - "entities": [ - { - "entity": "To", - "startPos": 10, - "endPos": 14 - } - ] - }, - { - "text": "flight to paris from london on feb 14th", - "intent": "Book flight", - "entities": [ - { - "entity": "To", - "startPos": 10, - "endPos": 14 - }, - { - "entity": "From", - "startPos": 21, - "endPos": 26 - } - ] - }, - { - "text": "fly from berlin to paris on may 5th", - "intent": "Book flight", - "entities": [ - { - "entity": "To", - "startPos": 19, - "endPos": 23 - }, - { - "entity": "From", - "startPos": 9, - "endPos": 14 - } - ] - }, - { - "text": "go to paris", - "intent": "Book flight", - "entities": [ - { - "entity": "To", - "startPos": 6, - "endPos": 10 - } - ] - }, - { - "text": "going from paris to berlin", - "intent": "Book flight", - "entities": [ - { - "entity": "To", - "startPos": 20, - "endPos": 25 - }, - { - "entity": "From", - "startPos": 11, - "endPos": 15 - } - ] - }, - { - "text": "ignore", - "intent": "Cancel", - "entities": [] - }, - { - "text": "travel to paris", - "intent": "Book flight", - "entities": [ - { - "entity": "To", - "startPos": 10, - "endPos": 14 - } - ] - } - ], - "settings": [] -} \ No newline at end of file diff --git a/samples/21.corebot-app-insights/config.py b/samples/21.corebot-app-insights/config.py deleted file mode 100644 index b3c87e304..000000000 --- a/samples/21.corebot-app-insights/config.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Configuration for the bot.""" - -import os - - -class DefaultConfig: - """Configuration for the bot.""" - - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") - LUIS_APP_ID = os.environ.get("LuisAppId", "") - LUIS_API_KEY = os.environ.get("LuisAPIKey", "") - # LUIS endpoint host name, ie "westus.api.cognitive.microsoft.com" - LUIS_API_HOST_NAME = os.environ.get("LuisAPIHostName", "") - APPINSIGHTS_INSTRUMENTATION_KEY = os.environ.get( - "AppInsightsInstrumentationKey", "" - ) diff --git a/samples/21.corebot-app-insights/dialogs/__init__.py b/samples/21.corebot-app-insights/dialogs/__init__.py deleted file mode 100644 index d37afdc97..000000000 --- a/samples/21.corebot-app-insights/dialogs/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Dialogs module""" -from .booking_dialog import BookingDialog -from .cancel_and_help_dialog import CancelAndHelpDialog -from .date_resolver_dialog import DateResolverDialog -from .main_dialog import MainDialog - -__all__ = ["BookingDialog", "CancelAndHelpDialog", "DateResolverDialog", "MainDialog"] diff --git a/samples/21.corebot-app-insights/dialogs/booking_dialog.py b/samples/21.corebot-app-insights/dialogs/booking_dialog.py deleted file mode 100644 index ab9b341b8..000000000 --- a/samples/21.corebot-app-insights/dialogs/booking_dialog.py +++ /dev/null @@ -1,132 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Flight booking dialog.""" - -from datatypes_date_time.timex import Timex - -from botbuilder.dialogs import WaterfallDialog, WaterfallStepContext, DialogTurnResult -from botbuilder.dialogs.prompts import ConfirmPrompt, TextPrompt, PromptOptions -from botbuilder.core import MessageFactory, BotTelemetryClient, NullTelemetryClient -from .cancel_and_help_dialog import CancelAndHelpDialog -from .date_resolver_dialog import DateResolverDialog - - -class BookingDialog(CancelAndHelpDialog): - """Flight booking implementation.""" - - def __init__( - self, - dialog_id: str = None, - telemetry_client: BotTelemetryClient = NullTelemetryClient(), - ): - super(BookingDialog, self).__init__( - dialog_id or BookingDialog.__name__, telemetry_client - ) - self.telemetry_client = telemetry_client - text_prompt = TextPrompt(TextPrompt.__name__) - text_prompt.telemetry_client = telemetry_client - - waterfall_dialog = WaterfallDialog( - WaterfallDialog.__name__, - [ - self.destination_step, - self.origin_step, - self.travel_date_step, - # self.confirm_step, - self.final_step, - ], - ) - waterfall_dialog.telemetry_client = telemetry_client - - self.add_dialog(text_prompt) - # self.add_dialog(ConfirmPrompt(ConfirmPrompt.__name__)) - self.add_dialog( - DateResolverDialog(DateResolverDialog.__name__, self.telemetry_client) - ) - self.add_dialog(waterfall_dialog) - - self.initial_dialog_id = WaterfallDialog.__name__ - - async def destination_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - """Prompt for destination.""" - booking_details = step_context.options - - if booking_details.destination is None: - return await step_context.prompt( - TextPrompt.__name__, - PromptOptions( - prompt=MessageFactory.text("To what city would you like to travel?") - ), - ) # pylint: disable=line-too-long,bad-continuation - - return await step_context.next(booking_details.destination) - - async def origin_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - """Prompt for origin city.""" - booking_details = step_context.options - - # Capture the response to the previous step's prompt - booking_details.destination = step_context.result - if booking_details.origin is None: - return await step_context.prompt( - TextPrompt.__name__, - PromptOptions( - prompt=MessageFactory.text("From what city will you be travelling?") - ), - ) # pylint: disable=line-too-long,bad-continuation - - return await step_context.next(booking_details.origin) - - async def travel_date_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - """Prompt for travel date. - This will use the DATE_RESOLVER_DIALOG.""" - - booking_details = step_context.options - - # Capture the results of the previous step - booking_details.origin = step_context.result - if not booking_details.travel_date or self.is_ambiguous( - booking_details.travel_date - ): - return await step_context.begin_dialog( - DateResolverDialog.__name__, booking_details.travel_date - ) # pylint: disable=line-too-long - - return await step_context.next(booking_details.travel_date) - - async def confirm_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - """Confirm the information the user has provided.""" - booking_details = step_context.options - - # Capture the results of the previous step - booking_details.travel_date = step_context.result - msg = ( - f"Please confirm, I have you traveling to: { booking_details.destination }" - f" from: { booking_details.origin } on: { booking_details.travel_date}." - ) - - # Offer a YES/NO prompt. - return await step_context.prompt( - ConfirmPrompt.__name__, PromptOptions(prompt=MessageFactory.text(msg)) - ) - - async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - """Complete the interaction and end the dialog.""" - if step_context.result: - booking_details = step_context.options - booking_details.travel_date = step_context.result - - return await step_context.end_dialog(booking_details) - - return await step_context.end_dialog() - - def is_ambiguous(self, timex: str) -> bool: - """Ensure time is correct.""" - timex_property = Timex(timex) - return "definite" not in timex_property.types diff --git a/samples/21.corebot-app-insights/dialogs/cancel_and_help_dialog.py b/samples/21.corebot-app-insights/dialogs/cancel_and_help_dialog.py deleted file mode 100644 index 4dab4dbe4..000000000 --- a/samples/21.corebot-app-insights/dialogs/cancel_and_help_dialog.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Handle cancel and help intents.""" - -from botbuilder.core import BotTelemetryClient, NullTelemetryClient -from botbuilder.dialogs import ( - ComponentDialog, - DialogContext, - DialogTurnResult, - DialogTurnStatus, -) -from botbuilder.schema import ActivityTypes - - -class CancelAndHelpDialog(ComponentDialog): - """Implementation of handling cancel and help.""" - - def __init__( - self, - dialog_id: str, - telemetry_client: BotTelemetryClient = NullTelemetryClient(), - ): - super(CancelAndHelpDialog, self).__init__(dialog_id) - self.telemetry_client = telemetry_client - - async def on_begin_dialog( - self, inner_dc: DialogContext, options: object - ) -> DialogTurnResult: - result = await self.interrupt(inner_dc) - if result is not None: - return result - - return await super(CancelAndHelpDialog, self).on_begin_dialog(inner_dc, options) - - async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult: - result = await self.interrupt(inner_dc) - if result is not None: - return result - - return await super(CancelAndHelpDialog, self).on_continue_dialog(inner_dc) - - async def interrupt(self, inner_dc: DialogContext) -> DialogTurnResult: - """Detect interruptions.""" - if inner_dc.context.activity.type == ActivityTypes.message: - text = inner_dc.context.activity.text.lower() - - if text in ("help", "?"): - await inner_dc.context.send_activity("Show Help...") - return DialogTurnResult(DialogTurnStatus.Waiting) - - if text in ("cancel", "quit"): - await inner_dc.context.send_activity("Cancelling") - return await inner_dc.cancel_all_dialogs() - - return None diff --git a/samples/21.corebot-app-insights/dialogs/date_resolver_dialog.py b/samples/21.corebot-app-insights/dialogs/date_resolver_dialog.py deleted file mode 100644 index baa5224ac..000000000 --- a/samples/21.corebot-app-insights/dialogs/date_resolver_dialog.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Handle date/time resolution for booking dialog.""" - -from datatypes_date_time.timex import Timex - -from botbuilder.core import MessageFactory, BotTelemetryClient, NullTelemetryClient -from botbuilder.dialogs import WaterfallDialog, DialogTurnResult, WaterfallStepContext -from botbuilder.dialogs.prompts import ( - DateTimePrompt, - PromptValidatorContext, - PromptOptions, - DateTimeResolution, -) -from .cancel_and_help_dialog import CancelAndHelpDialog - - -class DateResolverDialog(CancelAndHelpDialog): - """Resolve the date""" - - def __init__( - self, - dialog_id: str = None, - telemetry_client: BotTelemetryClient = NullTelemetryClient(), - ): - super(DateResolverDialog, self).__init__( - dialog_id or DateResolverDialog.__name__, telemetry_client - ) - self.telemetry_client = telemetry_client - - date_time_prompt = DateTimePrompt( - DateTimePrompt.__name__, DateResolverDialog.datetime_prompt_validator - ) - date_time_prompt.telemetry_client = telemetry_client - - waterfall_dialog = WaterfallDialog( - WaterfallDialog.__name__ + "2", [self.initial_step, self.final_step] - ) - waterfall_dialog.telemetry_client = telemetry_client - - self.add_dialog(date_time_prompt) - self.add_dialog(waterfall_dialog) - - self.initial_dialog_id = WaterfallDialog.__name__ + "2" - - async def initial_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - """Prompt for the date.""" - timex = step_context.options - - prompt_msg = "On what date would you like to travel?" - reprompt_msg = ( - "I'm sorry, for best results, please enter your travel " - "date including the month, day and year." - ) - - if timex is None: - # We were not given any date at all so prompt the user. - return await step_context.prompt( - DateTimePrompt.__name__, - PromptOptions( # pylint: disable=bad-continuation - prompt=MessageFactory.text(prompt_msg), - retry_prompt=MessageFactory.text(reprompt_msg), - ), - ) - - # We have a Date we just need to check it is unambiguous. - if "definite" in Timex(timex).types: - # This is essentially a "reprompt" of the data we were given up front. - return await step_context.prompt( - DateTimePrompt.__name__, PromptOptions(prompt=reprompt_msg) - ) - - return await step_context.next(DateTimeResolution(timex=timex)) - - async def final_step(self, step_context: WaterfallStepContext): - """Cleanup - set final return value and end dialog.""" - timex = step_context.result[0].timex - return await step_context.end_dialog(timex) - - @staticmethod - async def datetime_prompt_validator(prompt_context: PromptValidatorContext) -> bool: - """ Validate the date provided is in proper form. """ - if prompt_context.recognized.succeeded: - timex = prompt_context.recognized.value[0].timex.split("T")[0] - - # TODO: Needs TimexProperty - return "definite" in Timex(timex).types - - return False diff --git a/samples/21.corebot-app-insights/dialogs/main_dialog.py b/samples/21.corebot-app-insights/dialogs/main_dialog.py deleted file mode 100644 index 6e70deadd..000000000 --- a/samples/21.corebot-app-insights/dialogs/main_dialog.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Main dialog. """ - -from botbuilder.core import BotTelemetryClient, NullTelemetryClient -from botbuilder.dialogs import ( - ComponentDialog, - WaterfallDialog, - WaterfallStepContext, - DialogTurnResult, -) -from botbuilder.dialogs.prompts import TextPrompt, PromptOptions -from botbuilder.core import MessageFactory -from booking_details import BookingDetails -from helpers.luis_helper import LuisHelper -from .booking_dialog import BookingDialog - - -class MainDialog(ComponentDialog): - """Main dialog. """ - - def __init__( - self, - configuration: dict, - dialog_id: str = None, - telemetry_client: BotTelemetryClient = NullTelemetryClient(), - ): - super(MainDialog, self).__init__(dialog_id or MainDialog.__name__) - - self._configuration = configuration - self.telemetry_client = telemetry_client - - text_prompt = TextPrompt(TextPrompt.__name__) - text_prompt.telemetry_client = self.telemetry_client - - booking_dialog = BookingDialog(telemetry_client=self._telemetry_client) - booking_dialog.telemetry_client = self.telemetry_client - - wf_dialog = WaterfallDialog( - "WFDialog", [self.intro_step, self.act_step, self.final_step] - ) - wf_dialog.telemetry_client = self.telemetry_client - - self.add_dialog(text_prompt) - self.add_dialog(booking_dialog) - self.add_dialog(wf_dialog) - - self.initial_dialog_id = "WFDialog" - - async def intro_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - """Initial prompt.""" - if ( - not self._configuration.get("LUIS_APP_ID", "") - or not self._configuration.get("LUIS_API_KEY", "") - or not self._configuration.get("LUIS_API_HOST_NAME", "") - ): - await step_context.context.send_activity( - MessageFactory.text( - "NOTE: LUIS is not configured. To enable all" - " capabilities, add 'LUIS_APP_ID', 'LUIS_API_KEY' and 'LUIS_API_HOST_NAME'" - " to the config.py file." - ) - ) - - return await step_context.next(None) - - return await step_context.prompt( - TextPrompt.__name__, - PromptOptions( - prompt=MessageFactory.text("What can I help you with today?") - ), - ) - - async def act_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - """Use language understanding to gather details about booking.""" - # Call LUIS and gather any potential booking details. (Note the TurnContext - # has the response to the prompt.) - booking_details = ( - await LuisHelper.execute_luis_query( - self._configuration, step_context.context, self.telemetry_client - ) - if step_context.result is not None - else BookingDetails() - ) # pylint: disable=bad-continuation - - # In this sample we only have a single Intent we are concerned with. However, - # typically a scenario will have multiple different Intents each corresponding - # to starting a different child Dialog. - - # Run the BookingDialog giving it whatever details we have from the - # model. The dialog will prompt to find out the remaining details. - return await step_context.begin_dialog(BookingDialog.__name__, booking_details) - - async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - """Complete dialog. - At this step, with details from the user, display the completed - flight booking to the user. - """ - # If the child dialog ("BookingDialog") was cancelled or the user failed - # to confirm, the Result here will be null. - if step_context.result is not None: - result = step_context.result - - # Now we have all the booking details call the booking service. - # If the call to the booking service was successful tell the user. - # time_property = Timex(result.travel_date) - # travel_date_msg = time_property.to_natural_language(datetime.now()) - msg = ( - f"I have you booked to {result.destination} from" - f" {result.origin} on {result.travel_date}." - ) - await step_context.context.send_activity(MessageFactory.text(msg)) - else: - await step_context.context.send_activity(MessageFactory.text("Thank you.")) - return await step_context.end_dialog() diff --git a/samples/21.corebot-app-insights/helpers/__init__.py b/samples/21.corebot-app-insights/helpers/__init__.py deleted file mode 100644 index 162eef503..000000000 --- a/samples/21.corebot-app-insights/helpers/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Helpers module.""" - -from . import activity_helper, luis_helper, dialog_helper - -__all__ = ["activity_helper", "dialog_helper", "luis_helper"] diff --git a/samples/21.corebot-app-insights/helpers/activity_helper.py b/samples/21.corebot-app-insights/helpers/activity_helper.py deleted file mode 100644 index bbd0ac848..000000000 --- a/samples/21.corebot-app-insights/helpers/activity_helper.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Helper to create reply object.""" - -from datetime import datetime -from botbuilder.schema import ( - Activity, - ActivityTypes, - ChannelAccount, - ConversationAccount, -) - - -def create_activity_reply(activity: Activity, text: str = None, locale: str = None): - """Helper to create reply object.""" - return Activity( - type=ActivityTypes.message, - timestamp=datetime.utcnow(), - from_property=ChannelAccount( - id=getattr(activity.recipient, "id", None), - name=getattr(activity.recipient, "name", None), - ), - recipient=ChannelAccount( - id=activity.from_property.id, name=activity.from_property.name - ), - reply_to_id=activity.id, - service_url=activity.service_url, - channel_id=activity.channel_id, - conversation=ConversationAccount( - is_group=activity.conversation.is_group, - id=activity.conversation.id, - name=activity.conversation.name, - ), - text=text or "", - locale=locale or "", - attachments=[], - entities=[], - ) diff --git a/samples/21.corebot-app-insights/helpers/dialog_helper.py b/samples/21.corebot-app-insights/helpers/dialog_helper.py deleted file mode 100644 index 56ba5b05f..000000000 --- a/samples/21.corebot-app-insights/helpers/dialog_helper.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Utility to run dialogs.""" -from botbuilder.core import StatePropertyAccessor, TurnContext -from botbuilder.dialogs import Dialog, DialogSet, DialogTurnStatus - - -class DialogHelper: - """Dialog Helper implementation.""" - - @staticmethod - async def run_dialog( - dialog: Dialog, turn_context: TurnContext, accessor: StatePropertyAccessor - ): # pylint: disable=line-too-long - """Run dialog.""" - dialog_set = DialogSet(accessor) - dialog_set.add(dialog) - - dialog_context = await dialog_set.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dialog_context.begin_dialog(dialog.id) diff --git a/samples/21.corebot-app-insights/helpers/luis_helper.py b/samples/21.corebot-app-insights/helpers/luis_helper.py deleted file mode 100644 index 81e28a032..000000000 --- a/samples/21.corebot-app-insights/helpers/luis_helper.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -"""Helper to call LUIS service.""" -from botbuilder.ai.luis import LuisRecognizer, LuisApplication, LuisPredictionOptions -from botbuilder.core import TurnContext, BotTelemetryClient, NullTelemetryClient - -from booking_details import BookingDetails - -# pylint: disable=line-too-long -class LuisHelper: - """LUIS helper implementation.""" - - @staticmethod - async def execute_luis_query( - configuration, - turn_context: TurnContext, - telemetry_client: BotTelemetryClient = None, - ) -> BookingDetails: - """Invoke LUIS service to perform prediction/evaluation of utterance.""" - booking_details = BookingDetails() - - # pylint:disable=broad-except - try: - luis_application = LuisApplication( - configuration.get("LUIS_APP_ID"), - configuration.get("LUIS_API_KEY"), - configuration.get("LUIS_API_HOST_NAME"), - ) - options = LuisPredictionOptions() - options.telemetry_client = ( - telemetry_client - if telemetry_client is not None - else NullTelemetryClient() - ) - recognizer = LuisRecognizer(luis_application, prediction_options=options) - recognizer_result = await recognizer.recognize(turn_context) - print(f"Recognize Result: {recognizer_result}") - - if recognizer_result.intents: - intent = sorted( - recognizer_result.intents, - key=recognizer_result.intents.get, - reverse=True, - )[:1][0] - if intent == "Book_flight": - # We need to get the result from the LUIS JSON which at every level returns an array. - to_entities = recognizer_result.entities.get("$instance", {}).get( - "To", [] - ) - if to_entities: - booking_details.destination = to_entities[0]["text"] - from_entities = recognizer_result.entities.get("$instance", {}).get( - "From", [] - ) - if from_entities: - booking_details.origin = from_entities[0]["text"] - - # This value will be a TIMEX. And we are only interested in a Date so grab the first result and drop the Time part. - # TIMEX is a format that represents DateTime expressions that include some ambiguity. e.g. missing a Year. - date_entities = recognizer_result.entities.get("$instance", {}).get( - "datetime", [] - ) - if date_entities: - booking_details.travel_date = ( - None # Set when we get a timex format - ) - except Exception as exception: - print(exception) - - return booking_details diff --git a/samples/21.corebot-app-insights/requirements.txt b/samples/21.corebot-app-insights/requirements.txt deleted file mode 100644 index ffcf72c6b..000000000 --- a/samples/21.corebot-app-insights/requirements.txt +++ /dev/null @@ -1,13 +0,0 @@ -Flask>=1.0.2 -asyncio>=3.4.3 -requests>=2.18.1 -botframework-connector>=4.4.0.b1 -botbuilder-schema>=4.4.0.b1 -botbuilder-core>=4.4.0.b1 -botbuilder-dialogs>=4.4.0.b1 -botbuilder-ai>=4.4.0.b1 -botbuilder-applicationinsights>=4.4.0.b1 -datatypes-date-time>=1.0.0.a1 -azure-cognitiveservices-language-luis>=0.2.0 -msrest>=0.6.6 - diff --git a/samples/23.facebook-events/README.md b/samples/23.facebook-events/README.md deleted file mode 100644 index 01a0f2619..000000000 --- a/samples/23.facebook-events/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# Facebook events - -Bot Framework v4 facebook events bot sample - -This bot has been created using [Bot Framework](https://dev.botframework.com), is shows how to integrate and consume Facebook specific payloads, such as postbacks, quick replies and optin events. Since Bot Framework supports multiple Facebook pages for a single bot, we also show how to know the page to which the message was sent, so developers can have custom behavior per page. - -More information about configuring a bot for Facebook Messenger can be found here: [Connect a bot to Facebook](https://docs.microsoft.com/en-us/azure/bot-service/bot-service-channel-connect-facebook) - -## Running the sample -- Clone the repository -```bash -git clone https://github.com/Microsoft/botbuilder-python.git -``` -- Activate your desired virtual environment -- Bring up a terminal, navigate to `botbuilder-python\samples\23.facebook-evbents` folder -- In the terminal, type `pip install -r requirements.txt` -- In the terminal, type `python app.py` - -## Testing the bot using Bot Framework Emulator -[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to bot using Bot Framework Emulator -- Launch Bot Framework Emulator -- File -> Open Bot -- Paste this URL in the emulator window - http://localhost:3978/api/messages - -## Further reading - -- [Bot Framework Documentation](https://docs.botframework.com) -- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) -- [Facebook Quick Replies](https://developers.facebook.com/docs/messenger-platform/send-messages/quick-replies/0) -- [Facebook PostBack](https://developers.facebook.com/docs/messenger-platform/reference/webhook-events/messaging_postbacks/) -- [Facebook Opt-in](https://developers.facebook.com/docs/messenger-platform/reference/webhook-events/messaging_optins/) -- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) diff --git a/samples/23.facebook-events/app.py b/samples/23.facebook-events/app.py deleted file mode 100644 index efd359d67..000000000 --- a/samples/23.facebook-events/app.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import asyncio -import sys -from datetime import datetime - -from flask import Flask, request, Response -from botbuilder.core import ( - BotFrameworkAdapterSettings, - TurnContext, - BotFrameworkAdapter, -) -from botbuilder.schema import Activity, ActivityTypes - -from bots import FacebookBot - -# Create the loop and Flask app -LOOP = asyncio.get_event_loop() -app = Flask(__name__, instance_relative_config=True) -app.config.from_object("config.DefaultConfig") - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings(app.config["APP_ID"], app.config["APP_PASSWORD"]) -ADAPTER = BotFrameworkAdapter(SETTINGS) - - -# Catch-all for errors. -async def on_error(context: TurnContext, error: Exception): - # This check writes out errors to console log .vs. app insights. - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - - # Send a message to the user - await context.send_activity("The bot encountered an error or bug.") - await context.send_activity( - "To continue to run this bot, please fix the bot source code." - ) - # Send a trace activity if we're talking to the Bot Framework Emulator - if context.activity.channel_id == "emulator": - # Create a trace activity that contains the error object - trace_activity = Activity( - label="TurnError", - name="on_turn_error Trace", - timestamp=datetime.utcnow(), - type=ActivityTypes.trace, - value=f"{error}", - value_type="https://www.botframework.com/schemas/error", - ) - # Send a trace activity, which will be displayed in Bot Framework Emulator - await context.send_activity(trace_activity) - - -ADAPTER.on_turn_error = on_error - -# Create the Bot -BOT = FacebookBot() - -# Listen for incoming requests on /api/messages -@app.route("/api/messages", methods=["POST"]) -def messages(): - # Main bot message handler. - if "application/json" in request.headers["Content-Type"]: - body = request.json - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = ( - request.headers["Authorization"] if "Authorization" in request.headers else "" - ) - - try: - task = LOOP.create_task( - ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - ) - LOOP.run_until_complete(task) - return Response(status=201) - except Exception as exception: - raise exception - - -if __name__ == "__main__": - try: - app.run(debug=False, port=app.config["PORT"]) # nosec debug - except Exception as exception: - raise exception diff --git a/samples/23.facebook-events/bots/__init__.py b/samples/23.facebook-events/bots/__init__.py deleted file mode 100644 index 7db4bb27c..000000000 --- a/samples/23.facebook-events/bots/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .facebook_bot import FacebookBot - -__all__ = ["FacebookBot"] diff --git a/samples/23.facebook-events/bots/facebook_bot.py b/samples/23.facebook-events/bots/facebook_bot.py deleted file mode 100644 index 7ee4ee609..000000000 --- a/samples/23.facebook-events/bots/facebook_bot.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.dialogs.choices import Choice, ChoiceFactory -from botbuilder.core import ActivityHandler, MessageFactory, TurnContext, CardFactory -from botbuilder.schema import ChannelAccount, CardAction, ActionTypes, HeroCard - -FACEBOOK_PAGEID_OPTION = "Facebook Id" -QUICK_REPLIES_OPTION = "Quick Replies" -POSTBACK_OPTION = "PostBack" - - -class FacebookBot(ActivityHandler): - async def on_members_added_activity( - self, members_added: [ChannelAccount], turn_context: TurnContext - ): - for member in members_added: - if member.id != turn_context.activity.recipient.id: - await turn_context.send_activity("Hello and welcome!") - - async def on_message_activity(self, turn_context: TurnContext): - if not await self._process_facebook_payload( - turn_context, turn_context.activity.channel_data - ): - await self._show_choices(turn_context) - - async def on_event_activity(self, turn_context: TurnContext): - await self._process_facebook_payload(turn_context, turn_context.activity.value) - - async def _show_choices(self, turn_context: TurnContext): - choices = [ - Choice( - value=QUICK_REPLIES_OPTION, - action=CardAction( - title=QUICK_REPLIES_OPTION, - type=ActionTypes.post_back, - value=QUICK_REPLIES_OPTION, - ), - ), - Choice( - value=FACEBOOK_PAGEID_OPTION, - action=CardAction( - title=FACEBOOK_PAGEID_OPTION, - type=ActionTypes.post_back, - value=FACEBOOK_PAGEID_OPTION, - ), - ), - Choice( - value=POSTBACK_OPTION, - action=CardAction( - title=POSTBACK_OPTION, - type=ActionTypes.post_back, - value=POSTBACK_OPTION, - ), - ), - ] - - message = ChoiceFactory.for_channel( - turn_context.activity.channel_id, - choices, - "What Facebook feature would you like to try? Here are some quick replies to choose from!", - ) - await turn_context.send_activity(message) - - async def _process_facebook_payload(self, turn_context: TurnContext, data) -> bool: - if "postback" in data: - await self._on_facebook_postback(turn_context, data["postback"]) - return True - - if "optin" in data: - await self._on_facebook_optin(turn_context, data["optin"]) - return True - - if "message" in data and "quick_reply" in data["message"]: - await self._on_facebook_quick_reply( - turn_context, data["message"]["quick_reply"] - ) - return True - - if "message" in data and data["message"]["is_echo"]: - await self._on_facebook_echo(turn_context, data["message"]) - return True - - async def _on_facebook_postback( - self, turn_context: TurnContext, facebook_postback: dict - ): - # TODO: Your PostBack handling logic here... - - reply = MessageFactory.text(f"Postback: {facebook_postback}") - await turn_context.send_activity(reply) - await self._show_choices(turn_context) - - async def _on_facebook_quick_reply( - self, turn_context: TurnContext, facebook_quick_reply: dict - ): - # TODO: Your quick reply event handling logic here... - - if turn_context.activity.text == FACEBOOK_PAGEID_OPTION: - reply = MessageFactory.text( - f"This message comes from the following Facebook Page: {turn_context.activity.recipient.id}" - ) - await turn_context.send_activity(reply) - await self._show_choices(turn_context) - elif turn_context.activity.text == POSTBACK_OPTION: - card = HeroCard( - text="Is 42 the answer to the ultimate question of Life, the Universe, and Everything?", - buttons=[ - CardAction(title="Yes", type=ActionTypes.post_back, value="Yes"), - CardAction(title="No", type=ActionTypes.post_back, value="No"), - ], - ) - reply = MessageFactory.attachment(CardFactory.hero_card(card)) - await turn_context.send_activity(reply) - else: - print(facebook_quick_reply) - await turn_context.send_activity("Quick Reply") - await self._show_choices(turn_context) - - async def _on_facebook_optin(self, turn_context: TurnContext, facebook_optin: dict): - # TODO: Your optin event handling logic here... - print(facebook_optin) - await turn_context.send_activity("Opt In") - - async def _on_facebook_echo( - self, turn_context: TurnContext, facebook_message: dict - ): - # TODO: Your echo event handling logic here... - print(facebook_message) - await turn_context.send_activity("Echo") diff --git a/samples/23.facebook-events/config.py b/samples/23.facebook-events/config.py deleted file mode 100644 index e007d0fa9..000000000 --- a/samples/23.facebook-events/config.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - -""" Bot Configuration """ - - -class DefaultConfig: - """ Bot Configuration """ - - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") diff --git a/samples/23.facebook-events/deploymentTemplates/template-with-preexisting-rg.json b/samples/23.facebook-events/deploymentTemplates/template-with-preexisting-rg.json deleted file mode 100644 index bff8c096d..000000000 --- a/samples/23.facebook-events/deploymentTemplates/template-with-preexisting-rg.json +++ /dev/null @@ -1,242 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "appId": { - "type": "string", - "metadata": { - "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." - } - }, - "appSecret": { - "type": "string", - "metadata": { - "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." - } - }, - "botId": { - "type": "string", - "metadata": { - "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." - } - }, - "botSku": { - "defaultValue": "F0", - "type": "string", - "metadata": { - "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." - } - }, - "newAppServicePlanName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "The name of the new App Service Plan." - } - }, - "newAppServicePlanSku": { - "type": "object", - "defaultValue": { - "name": "S1", - "tier": "Standard", - "size": "S1", - "family": "S", - "capacity": 1 - }, - "metadata": { - "description": "The SKU of the App Service Plan. Defaults to Standard values." - } - }, - "appServicePlanLocation": { - "type": "string", - "metadata": { - "description": "The location of the App Service Plan." - } - }, - "existingAppServicePlan": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Name of the existing App Service Plan used to create the Web App for the bot." - } - }, - "newWebAppName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." - } - } - }, - "variables": { - "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", - "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", - "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", - "publishingUsername": "[concat('$', parameters('newWebAppName'))]", - "resourcesLocation": "[parameters('appServicePlanLocation')]", - "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", - "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", - "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" - }, - "resources": [ - { - "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", - "type": "Microsoft.Web/serverfarms", - "apiVersion": "2016-09-01", - "name": "[variables('servicePlanName')]", - "location": "[variables('resourcesLocation')]", - "sku": "[parameters('newAppServicePlanSku')]", - "kind": "linux", - "properties": { - "name": "[variables('servicePlanName')]", - "perSiteScaling": false, - "reserved": true, - "targetWorkerCount": 0, - "targetWorkerSizeId": 0 - } - }, - { - "comments": "Create a Web App using a Linux App Service Plan", - "type": "Microsoft.Web/sites", - "apiVersion": "2016-08-01", - "name": "[variables('webAppName')]", - "location": "[variables('resourcesLocation')]", - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]" - ], - "kind": "app,linux", - "properties": { - "enabled": true, - "hostNameSslStates": [ - { - "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", - "sslState": "Disabled", - "hostType": "Standard" - }, - { - "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", - "sslState": "Disabled", - "hostType": "Repository" - } - ], - "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]", - "reserved": true, - "scmSiteAlsoStopped": false, - "clientAffinityEnabled": false, - "clientCertEnabled": false, - "hostNamesDisabled": false, - "containerSize": 0, - "dailyMemoryTimeQuota": 0, - "httpsOnly": false, - "siteConfig": { - "appSettings": [ - { - "name": "MicrosoftAppId", - "value": "[parameters('appId')]" - }, - { - "name": "MicrosoftAppPassword", - "value": "[parameters('appSecret')]" - }, - { - "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", - "value": "true" - } - ], - "cors": { - "allowedOrigins": [ - "https://botservice.hosting.portal.azure.net", - "https://hosting.onecloud.azure-test.net/" - ] - } - } - } - }, - { - "type": "Microsoft.Web/sites/config", - "apiVersion": "2016-08-01", - "name": "[concat(variables('webAppName'), '/web')]", - "location": "[variables('resourcesLocation')]", - "dependsOn": [ - "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" - ], - "properties": { - "numberOfWorkers": 1, - "defaultDocuments": [ - "Default.htm", - "Default.html", - "Default.asp", - "index.htm", - "index.html", - "iisstart.htm", - "default.aspx", - "index.php", - "hostingstart.html" - ], - "netFrameworkVersion": "v4.0", - "phpVersion": "", - "pythonVersion": "", - "nodeVersion": "", - "linuxFxVersion": "PYTHON|3.7", - "requestTracingEnabled": false, - "remoteDebuggingEnabled": false, - "remoteDebuggingVersion": "VS2017", - "httpLoggingEnabled": true, - "logsDirectorySizeLimit": 35, - "detailedErrorLoggingEnabled": false, - "publishingUsername": "[variables('publishingUsername')]", - "scmType": "None", - "use32BitWorkerProcess": true, - "webSocketsEnabled": false, - "alwaysOn": false, - "appCommandLine": "", - "managedPipelineMode": "Integrated", - "virtualApplications": [ - { - "virtualPath": "/", - "physicalPath": "site\\wwwroot", - "preloadEnabled": false, - "virtualDirectories": null - } - ], - "winAuthAdminState": 0, - "winAuthTenantState": 0, - "customAppPoolIdentityAdminState": false, - "customAppPoolIdentityTenantState": false, - "loadBalancing": "LeastRequests", - "routingRules": [], - "experiments": { - "rampUpRules": [] - }, - "autoHealEnabled": false, - "vnetName": "", - "minTlsVersion": "1.2", - "ftpsState": "AllAllowed", - "reservedInstanceCount": 0 - } - }, - { - "apiVersion": "2017-12-01", - "type": "Microsoft.BotService/botServices", - "name": "[parameters('botId')]", - "location": "global", - "kind": "bot", - "sku": { - "name": "[parameters('botSku')]" - }, - "properties": { - "name": "[parameters('botId')]", - "displayName": "[parameters('botId')]", - "endpoint": "[variables('botEndpoint')]", - "msaAppId": "[parameters('appId')]", - "developerAppInsightsApplicationId": null, - "developerAppInsightKey": null, - "publishingCredentials": null, - "storageResourceId": null - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" - ] - } - ] -} \ No newline at end of file diff --git a/samples/23.facebook-events/requirements.txt b/samples/23.facebook-events/requirements.txt deleted file mode 100644 index a69322ec3..000000000 --- a/samples/23.facebook-events/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -jsonpickle==1.2 -botbuilder-core>=4.4.0b1 -flask>=1.0.3 diff --git a/samples/40.timex-resolution/README.md b/samples/40.timex-resolution/README.md deleted file mode 100644 index 2d6b6b0a8..000000000 --- a/samples/40.timex-resolution/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# Timex Resolution - -This sample shows how to use TIMEX expressions. - -## Concepts introduced in this sample - -### What is a TIMEX expression? - -A TIMEX expression is an alpha-numeric expression derived in outline from the standard date-time representation ISO 8601. -The interesting thing about TIMEX expressions is that they can represent various degrees of ambiguity in the date parts. For example, May 29th, is not a -full calendar date because we haven't said which May 29th - it could be this year, last year, any year in fact. -TIMEX has other features such as the ability to represent ranges, date ranges, time ranges and even date-time ranges. - -### Where do TIMEX expressions come from? - -TIMEX expressions are produced as part of the output of running a DateTimeRecognizer against some natural language input. As the same -Recognizers are run in LUIS the result returned in the JSON from a call to LUIS also contains the TIMEX expressions. - -### What can the library do? - -It turns out that TIMEX expressions are not that simple to work with in code. This library attempts to address that. One helpful way to -think about a TIMEX expression is as a partially filled property bag. The properties might be such things as "day of week" or "year." -Basically the more properties we have captured in the expression the less ambiguity we have. - -The library can do various things: - -- Parse TIMEX expressions to give you the properties contained there in. -- Generate TIMEX expressions based on setting raw properties. -- Generate natural language from the TIMEX expression. (This is logically the reverse of the Recognizer.) -- Resolve TIMEX expressions to produce example date-times. (This produces the same result as the Recognizer (and therefore LUIS)). -- Evaluate TIMEX expressions against constraints such that new more precise TIMEX expressions are produced. - -### Where is the source code? - -The TIMEX expression library is contained in the same GitHub repo as the recognizers. Refer to the further reading section below. - -## Running the sample -- Clone the repository -```bash -git clone https://github.com/Microsoft/botbuilder-python.git -``` -- Activate your desired virtual environment -- Bring up a terminal, navigate to `botbuilder-python\samples\40.timex-resolution` folder -- In the terminal, type `pip install -r requirements.txt` -- In the terminal, type `python main.py` - -## Further reading - -- [TIMEX](https://en.wikipedia.org/wiki/TimeML#TIMEX3) -- [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) -- [Recognizers Text](https://github.com/Microsoft/recognizers-text) diff --git a/samples/40.timex-resolution/ambiguity.py b/samples/40.timex-resolution/ambiguity.py deleted file mode 100644 index a412b2f55..000000000 --- a/samples/40.timex-resolution/ambiguity.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from recognizers_date_time import recognize_datetime, Culture - - -class Ambiguity: - """ - TIMEX expressions are designed to represent ambiguous rather than definite dates. For - example: "Monday" could be any Monday ever. "May 5th" could be any one of the possible May - 5th in the past or the future. TIMEX does not represent ambiguous times. So if the natural - language mentioned 4 o'clock it could be either 4AM or 4PM. For that the recognizer (and by - extension LUIS) would return two TIMEX expressions. A TIMEX expression can include a date and - time parts. So ambiguity of date can be combined with multiple results. Code that deals with - TIMEX expressions is frequently dealing with sets of TIMEX expressions. - """ - - @staticmethod - def date_ambiguity(): - # Run the recognizer. - results = recognize_datetime( - "Either Saturday or Sunday would work.", Culture.English - ) - - # We should find two results in this example. - for result in results: - # The resolution includes two example values: going backwards and forwards from NOW in the calendar. - # Each result includes a TIMEX expression that captures the inherent date but not time ambiguity. - # We are interested in the distinct set of TIMEX expressions. - # There is also either a "value" property on each value or "start" and "end". - distinct_timex_expressions = { - value["timex"] - for value in result.resolution["values"] - if "timex" in value - } - print(f"{result.text} ({','.join(distinct_timex_expressions)})") - - @staticmethod - def time_ambiguity(): - # Run the recognizer. - results = recognize_datetime( - "We would like to arrive at 4 o'clock or 5 o'clock.", Culture.English - ) - - # We should find two results in this example. - for result in results: - # The resolution includes two example values: one for AM and one for PM. - # Each result includes a TIMEX expression that captures the inherent date but not time ambiguity. - # We are interested in the distinct set of TIMEX expressions. - distinct_timex_expressions = { - value["timex"] - for value in result.resolution["values"] - if "timex" in value - } - - # TIMEX expressions don't capture time ambiguity so there will be two distinct expressions for each result. - print(f"{result.text} ({','.join(distinct_timex_expressions)})") - - @staticmethod - def date_time_ambiguity(): - # Run the recognizer. - results = recognize_datetime( - "It will be ready Wednesday at 5 o'clock.", Culture.English - ) - - # We should find a single result in this example. - for result in results: - # The resolution includes four example values: backwards and forward in the calendar and then AM and PM. - # Each result includes a TIMEX expression that captures the inherent date but not time ambiguity. - # We are interested in the distinct set of TIMEX expressions. - distinct_timex_expressions = { - value["timex"] - for value in result.resolution["values"] - if "timex" in value - } - - # TIMEX expressions don't capture time ambiguity so there will be two distinct expressions for each result. - print(f"{result.text} ({','.join(distinct_timex_expressions)})") diff --git a/samples/40.timex-resolution/constraints.py b/samples/40.timex-resolution/constraints.py deleted file mode 100644 index 21e8d2190..000000000 --- a/samples/40.timex-resolution/constraints.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import datetime - -from datatypes_timex_expression import TimexRangeResolver, TimexCreator - - -class Constraints: - """ - The TimexRangeResolved can be used in application logic to apply constraints to a set of TIMEX expressions. - The constraints themselves are TIMEX expressions. This is designed to appear a little like a database join, - of course its a little less generic than that because dates can be complicated things. - """ - - @staticmethod - def examples(): - """ - When you give the recognizer the text "Wednesday 4 o'clock" you get these distinct TIMEX values back. - But our bot logic knows that whatever the user says it should be evaluated against the constraints of - a week from today with respect to the date part and in the evening with respect to the time part. - """ - - resolutions = TimexRangeResolver.evaluate( - ["XXXX-WXX-3T04", "XXXX-WXX-3T16"], - [TimexCreator.week_from_today(), TimexCreator.EVENING], - ) - - today = datetime.datetime.now() - for resolution in resolutions: - print(resolution.to_natural_language(today)) diff --git a/samples/40.timex-resolution/language_generation.py b/samples/40.timex-resolution/language_generation.py deleted file mode 100644 index c8b156521..000000000 --- a/samples/40.timex-resolution/language_generation.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import datetime - -from datatypes_timex_expression import Timex - - -class LanguageGeneration: - """ - This language generation capabilities are the logical opposite of what the recognizer does. - As an experiment try feeding the result of language generation back into a recognizer. - You should get back the same TIMEX expression in the result. - """ - - @staticmethod - def examples(): - LanguageGeneration.__describe(Timex("2019-05-29")) - LanguageGeneration.__describe(Timex("XXXX-WXX-6")) - LanguageGeneration.__describe(Timex("XXXX-WXX-6T16")) - LanguageGeneration.__describe(Timex("T12")) - - LanguageGeneration.__describe(Timex.from_date(datetime.datetime.now())) - LanguageGeneration.__describe( - Timex.from_date(datetime.datetime.now() + datetime.timedelta(days=1)) - ) - - @staticmethod - def __describe(timex: Timex): - # Note natural language is often relative, for example the sentence "Yesterday all my troubles seemed so far - # away." Having your bot say something like "next Wednesday" in a response can make it sound more natural. - reference_date = datetime.datetime.now() - print(f"{timex.timex_value()} : {timex.to_natural_language(reference_date)}") diff --git a/samples/40.timex-resolution/main.py b/samples/40.timex-resolution/main.py deleted file mode 100644 index 1079efd7a..000000000 --- a/samples/40.timex-resolution/main.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from ambiguity import Ambiguity -from constraints import Constraints -from language_generation import LanguageGeneration -from parsing import Parsing -from ranges import Ranges -from resolution import Resolution - -if __name__ == "__main__": - # Creating TIMEX expressions from natural language using the Recognizer package. - Ambiguity.date_ambiguity() - Ambiguity.time_ambiguity() - Ambiguity.date_time_ambiguity() - Ranges.date_range() - Ranges.time_range() - - # Manipulating TIMEX expressions in code using the TIMEX Datatype package. - Parsing.examples() - LanguageGeneration.examples() - Resolution.examples() - Constraints.examples() diff --git a/samples/40.timex-resolution/parsing.py b/samples/40.timex-resolution/parsing.py deleted file mode 100644 index 194dc97cc..000000000 --- a/samples/40.timex-resolution/parsing.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from datatypes_timex_expression import Timex, Constants - - -class Parsing: - """ - The Timex class takes a TIMEX expression as a string argument in its constructor. - This pulls all the component parts of the expression into properties on this object. You can - then manipulate the TIMEX expression via those properties. - The "types" property infers a datetimeV2 type from the underlying set of properties. - If you take a TIMEX with date components and add time components you add the - inferred type datetime (its still a date). - Logic can be written against the inferred type, perhaps to have the bot ask the user for - disambiguation. - """ - - @staticmethod - def __describe(timex_pattern: str): - timex = Timex(timex_pattern) - - print(timex.timex_value(), end=" ") - - if Constants.TIMEX_TYPES_DATE in timex.types: - if Constants.TIMEX_TYPES_DEFINITE in timex.types: - print("We have a definite calendar date.", end=" ") - else: - print("We have a date but there is some ambiguity.", end=" ") - - if Constants.TIMEX_TYPES_TIME in timex.types: - print("We have a time.") - else: - print("") - - @staticmethod - def examples(): - """ - Print information an various TimeX expressions. - :return: None - """ - Parsing.__describe("2017-05-29") - Parsing.__describe("XXXX-WXX-6") - Parsing.__describe("XXXX-WXX-6T16") - Parsing.__describe("T12") diff --git a/samples/40.timex-resolution/ranges.py b/samples/40.timex-resolution/ranges.py deleted file mode 100644 index 1bae92ce0..000000000 --- a/samples/40.timex-resolution/ranges.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from recognizers_date_time import recognize_datetime -from recognizers_text import Culture - - -class Ranges: - """ - TIMEX expressions can represent date and time ranges. Here are a couple of examples. - """ - - @staticmethod - def date_range(): - # Run the recognizer. - results = recognize_datetime( - "Some time in the next two weeks.", Culture.English - ) - - # We should find a single result in this example. - for result in results: - # The resolution includes a single value because there is no ambiguity. - # We are interested in the distinct set of TIMEX expressions. - distinct_timex_expressions = { - value["timex"] - for value in result.resolution["values"] - if "timex" in value - } - - # The TIMEX expression can also capture the notion of range. - print(f"{result.text} ({','.join(distinct_timex_expressions)})") - - @staticmethod - def time_range(): - # Run the recognizer. - results = recognize_datetime( - "Some time between 6pm and 6:30pm.", Culture.English - ) - - # We should find a single result in this example. - for result in results: - # The resolution includes a single value because there is no ambiguity. - # We are interested in the distinct set of TIMEX expressions. - distinct_timex_expressions = { - value["timex"] - for value in result.resolution["values"] - if "timex" in value - } - - # The TIMEX expression can also capture the notion of range. - print(f"{result.text} ({','.join(distinct_timex_expressions)})") diff --git a/samples/40.timex-resolution/requirements.txt b/samples/40.timex-resolution/requirements.txt deleted file mode 100644 index 26579538e..000000000 --- a/samples/40.timex-resolution/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -recognizers-text>=1.0.2a2 -datatypes-timex-expression>=1.0.2a2 - diff --git a/samples/40.timex-resolution/resolution.py b/samples/40.timex-resolution/resolution.py deleted file mode 100644 index 4e42f5e88..000000000 --- a/samples/40.timex-resolution/resolution.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import datetime - -from datatypes_timex_expression import TimexResolver - - -class Resolution: - """ - Given the TIMEX expressions it is easy to create the computed example values that the recognizer gives. - """ - - @staticmethod - def examples(): - # When you give the recognizer the text "Wednesday 4 o'clock" you get these distinct TIMEX values back. - - today = datetime.datetime.now() - resolution = TimexResolver.resolve(["XXXX-WXX-3T04", "XXXX-WXX-3T16"], today) - - print(f"Resolution Values: {len(resolution.values)}") - - for value in resolution.values: - print(value.timex) - print(value.type) - print(value.value) diff --git a/samples/42.scaleout/README.md b/samples/42.scaleout/README.md deleted file mode 100644 index e9b8d103c..000000000 --- a/samples/42.scaleout/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# Scale Out - -Bot Framework v4 bot Scale Out sample - -This bot has been created using [Bot Framework](https://dev.botframework.com), is shows how to use a custom storage solution that supports a deployment scaled out across multiple machines. - -The custom storage solution is implemented against memory for testing purposes and against Azure Blob Storage. The sample shows how storage solutions with different policies can be implemented and integrated with the framework. The solution makes use of the standard HTTP ETag/If-Match mechanisms commonly found on cloud storage technologies. - -## Running the sample -- Clone the repository -```bash -git clone https://github.com/Microsoft/botbuilder-python.git -``` -- Activate your desired virtual environment -- Bring up a terminal, navigate to `botbuilder-python\samples\42.scaleout` folder -- In the terminal, type `pip install -r requirements.txt` -- In the terminal, type `python app.py` - -## Testing the bot using Bot Framework Emulator -[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to bot using Bot Framework Emulator -- Launch Bot Framework Emulator -- File -> Open Bot -- Paste this URL in the emulator window - http://localhost:3978/api/messages - -## Further reading - -- [Bot Framework Documentation](https://docs.botframework.com) -- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) -- [Implementing custom storage for you bot](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-custom-storage?view=azure-bot-service-4.0) -- [Bot Storage](https://docs.microsoft.com/en-us/azure/bot-service/dotnet/bot-builder-dotnet-state?view=azure-bot-service-3.0&viewFallbackFrom=azure-bot-service-4.0) -- [HTTP ETag](https://en.wikipedia.org/wiki/HTTP_ETag) -- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) diff --git a/samples/42.scaleout/app.py b/samples/42.scaleout/app.py deleted file mode 100644 index ac780beed..000000000 --- a/samples/42.scaleout/app.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import asyncio -import sys -from datetime import datetime - -from flask import Flask, request, Response -from botbuilder.core import ( - BotFrameworkAdapterSettings, - TurnContext, - BotFrameworkAdapter, -) -from botbuilder.schema import Activity, ActivityTypes - -from bots import ScaleoutBot - -# Create the loop and Flask app -from dialogs import RootDialog -from store import MemoryStore - -LOOP = asyncio.get_event_loop() -app = Flask(__name__, instance_relative_config=True) -app.config.from_object("config.DefaultConfig") - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings(app.config["APP_ID"], app.config["APP_PASSWORD"]) -ADAPTER = BotFrameworkAdapter(SETTINGS) - - -# Catch-all for errors. -async def on_error(context: TurnContext, error: Exception): - # This check writes out errors to console log .vs. app insights. - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - - # Send a message to the user - await context.send_activity("The bot encountered an error or bug.") - await context.send_activity( - "To continue to run this bot, please fix the bot source code." - ) - # Send a trace activity if we're talking to the Bot Framework Emulator - if context.activity.channel_id == "emulator": - # Create a trace activity that contains the error object - trace_activity = Activity( - label="TurnError", - name="on_turn_error Trace", - timestamp=datetime.utcnow(), - type=ActivityTypes.trace, - value=f"{error}", - value_type="https://www.botframework.com/schemas/error", - ) - # Send a trace activity, which will be displayed in Bot Framework Emulator - await context.send_activity(trace_activity) - - -ADAPTER.on_turn_error = on_error - -# Create the Bot -STORAGE = MemoryStore() -# Use BlobStore to test with Azure Blob storage. -# STORAGE = BlobStore(app.config["BLOB_ACCOUNT_NAME"], app.config["BLOB_KEY"], app.config["BLOB_CONTAINER"]) -DIALOG = RootDialog() -BOT = ScaleoutBot(STORAGE, DIALOG) - -# Listen for incoming requests on /api/messages -@app.route("/api/messages", methods=["POST"]) -def messages(): - # Main bot message handler. - if "application/json" in request.headers["Content-Type"]: - body = request.json - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = ( - request.headers["Authorization"] if "Authorization" in request.headers else "" - ) - - try: - task = LOOP.create_task( - ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - ) - LOOP.run_until_complete(task) - return Response(status=201) - except Exception as exception: - raise exception - - -if __name__ == "__main__": - try: - app.run(debug=False, port=app.config["PORT"]) # nosec debug - except Exception as exception: - raise exception diff --git a/samples/42.scaleout/bots/__init__.py b/samples/42.scaleout/bots/__init__.py deleted file mode 100644 index b1886b216..000000000 --- a/samples/42.scaleout/bots/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .scaleout_bot import ScaleoutBot - -__all__ = ["ScaleoutBot"] diff --git a/samples/42.scaleout/bots/scaleout_bot.py b/samples/42.scaleout/bots/scaleout_bot.py deleted file mode 100644 index 83489cd47..000000000 --- a/samples/42.scaleout/bots/scaleout_bot.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import ActivityHandler, TurnContext -from botbuilder.dialogs import Dialog - -from host import DialogHost -from store import Store - - -class ScaleoutBot(ActivityHandler): - """ - This bot runs Dialogs that send message Activities in a way that can be scaled out with a multi-machine deployment. - The bot logic makes use of the standard HTTP ETag/If-Match mechanism for optimistic locking. This mechanism - is commonly supported on cloud storage technologies from multiple vendors including teh Azure Blob Storage - service. A full implementation against Azure Blob Storage is included in this sample. - """ - - def __init__(self, store: Store, dialog: Dialog): - self.store = store - self.dialog = dialog - - async def on_message_activity(self, turn_context: TurnContext): - # Create the storage key for this conversation. - key = f"{turn_context.activity.channel_id}/conversations/{turn_context.activity.conversation.id}" - - # The execution sits in a loop because there might be a retry if the save operation fails. - while True: - # Load any existing state associated with this key - old_state, e_tag = await self.store.load(key) - - # Run the dialog system with the old state and inbound activity, the result is a new state and outbound - # activities. - activities, new_state = await DialogHost.run( - self.dialog, turn_context.activity, old_state - ) - - # Save the updated state associated with this key. - success = await self.store.save(key, new_state, e_tag) - if success: - if activities: - # This is an actual send on the TurnContext we were given and so will actual do a send this time. - await turn_context.send_activities(activities) - - break diff --git a/samples/42.scaleout/config.py b/samples/42.scaleout/config.py deleted file mode 100644 index 5737815c9..000000000 --- a/samples/42.scaleout/config.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - -""" Bot Configuration """ - - -class DefaultConfig: - """ Bot Configuration """ - - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") - BLOB_ACCOUNT_NAME = "tboehrestorage" - BLOB_KEY = "A7tc3c9T/n67iDYO7Lx19sTjnA+DD3bR/HQ4yPhJuyVXO1yJ8mYzDOXsBhJrjldh7zKMjE9Wc6PrM1It4nlGPw==" - BLOB_CONTAINER = "dialogs" diff --git a/samples/42.scaleout/deploymentTemplates/template-with-preexisting-rg.json b/samples/42.scaleout/deploymentTemplates/template-with-preexisting-rg.json deleted file mode 100644 index bff8c096d..000000000 --- a/samples/42.scaleout/deploymentTemplates/template-with-preexisting-rg.json +++ /dev/null @@ -1,242 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "appId": { - "type": "string", - "metadata": { - "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." - } - }, - "appSecret": { - "type": "string", - "metadata": { - "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." - } - }, - "botId": { - "type": "string", - "metadata": { - "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." - } - }, - "botSku": { - "defaultValue": "F0", - "type": "string", - "metadata": { - "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." - } - }, - "newAppServicePlanName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "The name of the new App Service Plan." - } - }, - "newAppServicePlanSku": { - "type": "object", - "defaultValue": { - "name": "S1", - "tier": "Standard", - "size": "S1", - "family": "S", - "capacity": 1 - }, - "metadata": { - "description": "The SKU of the App Service Plan. Defaults to Standard values." - } - }, - "appServicePlanLocation": { - "type": "string", - "metadata": { - "description": "The location of the App Service Plan." - } - }, - "existingAppServicePlan": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Name of the existing App Service Plan used to create the Web App for the bot." - } - }, - "newWebAppName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." - } - } - }, - "variables": { - "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", - "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", - "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", - "publishingUsername": "[concat('$', parameters('newWebAppName'))]", - "resourcesLocation": "[parameters('appServicePlanLocation')]", - "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", - "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", - "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" - }, - "resources": [ - { - "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", - "type": "Microsoft.Web/serverfarms", - "apiVersion": "2016-09-01", - "name": "[variables('servicePlanName')]", - "location": "[variables('resourcesLocation')]", - "sku": "[parameters('newAppServicePlanSku')]", - "kind": "linux", - "properties": { - "name": "[variables('servicePlanName')]", - "perSiteScaling": false, - "reserved": true, - "targetWorkerCount": 0, - "targetWorkerSizeId": 0 - } - }, - { - "comments": "Create a Web App using a Linux App Service Plan", - "type": "Microsoft.Web/sites", - "apiVersion": "2016-08-01", - "name": "[variables('webAppName')]", - "location": "[variables('resourcesLocation')]", - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]" - ], - "kind": "app,linux", - "properties": { - "enabled": true, - "hostNameSslStates": [ - { - "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", - "sslState": "Disabled", - "hostType": "Standard" - }, - { - "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", - "sslState": "Disabled", - "hostType": "Repository" - } - ], - "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]", - "reserved": true, - "scmSiteAlsoStopped": false, - "clientAffinityEnabled": false, - "clientCertEnabled": false, - "hostNamesDisabled": false, - "containerSize": 0, - "dailyMemoryTimeQuota": 0, - "httpsOnly": false, - "siteConfig": { - "appSettings": [ - { - "name": "MicrosoftAppId", - "value": "[parameters('appId')]" - }, - { - "name": "MicrosoftAppPassword", - "value": "[parameters('appSecret')]" - }, - { - "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", - "value": "true" - } - ], - "cors": { - "allowedOrigins": [ - "https://botservice.hosting.portal.azure.net", - "https://hosting.onecloud.azure-test.net/" - ] - } - } - } - }, - { - "type": "Microsoft.Web/sites/config", - "apiVersion": "2016-08-01", - "name": "[concat(variables('webAppName'), '/web')]", - "location": "[variables('resourcesLocation')]", - "dependsOn": [ - "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" - ], - "properties": { - "numberOfWorkers": 1, - "defaultDocuments": [ - "Default.htm", - "Default.html", - "Default.asp", - "index.htm", - "index.html", - "iisstart.htm", - "default.aspx", - "index.php", - "hostingstart.html" - ], - "netFrameworkVersion": "v4.0", - "phpVersion": "", - "pythonVersion": "", - "nodeVersion": "", - "linuxFxVersion": "PYTHON|3.7", - "requestTracingEnabled": false, - "remoteDebuggingEnabled": false, - "remoteDebuggingVersion": "VS2017", - "httpLoggingEnabled": true, - "logsDirectorySizeLimit": 35, - "detailedErrorLoggingEnabled": false, - "publishingUsername": "[variables('publishingUsername')]", - "scmType": "None", - "use32BitWorkerProcess": true, - "webSocketsEnabled": false, - "alwaysOn": false, - "appCommandLine": "", - "managedPipelineMode": "Integrated", - "virtualApplications": [ - { - "virtualPath": "/", - "physicalPath": "site\\wwwroot", - "preloadEnabled": false, - "virtualDirectories": null - } - ], - "winAuthAdminState": 0, - "winAuthTenantState": 0, - "customAppPoolIdentityAdminState": false, - "customAppPoolIdentityTenantState": false, - "loadBalancing": "LeastRequests", - "routingRules": [], - "experiments": { - "rampUpRules": [] - }, - "autoHealEnabled": false, - "vnetName": "", - "minTlsVersion": "1.2", - "ftpsState": "AllAllowed", - "reservedInstanceCount": 0 - } - }, - { - "apiVersion": "2017-12-01", - "type": "Microsoft.BotService/botServices", - "name": "[parameters('botId')]", - "location": "global", - "kind": "bot", - "sku": { - "name": "[parameters('botSku')]" - }, - "properties": { - "name": "[parameters('botId')]", - "displayName": "[parameters('botId')]", - "endpoint": "[variables('botEndpoint')]", - "msaAppId": "[parameters('appId')]", - "developerAppInsightsApplicationId": null, - "developerAppInsightKey": null, - "publishingCredentials": null, - "storageResourceId": null - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" - ] - } - ] -} \ No newline at end of file diff --git a/samples/42.scaleout/dialogs/__init__.py b/samples/42.scaleout/dialogs/__init__.py deleted file mode 100644 index d97c50169..000000000 --- a/samples/42.scaleout/dialogs/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .root_dialog import RootDialog - -__all__ = ["RootDialog"] diff --git a/samples/42.scaleout/dialogs/root_dialog.py b/samples/42.scaleout/dialogs/root_dialog.py deleted file mode 100644 index e849ba02b..000000000 --- a/samples/42.scaleout/dialogs/root_dialog.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import MessageFactory -from botbuilder.dialogs import ( - ComponentDialog, - WaterfallDialog, - WaterfallStepContext, - DialogTurnResult, - NumberPrompt, - PromptOptions, -) - - -class RootDialog(ComponentDialog): - def __init__(self): - super(RootDialog, self).__init__(RootDialog.__name__) - - self.add_dialog(self.__create_waterfall()) - self.add_dialog(NumberPrompt("number")) - - self.initial_dialog_id = "waterfall" - - def __create_waterfall(self) -> WaterfallDialog: - return WaterfallDialog("waterfall", [self.__step1, self.__step2, self.__step3]) - - async def __step1(self, step_context: WaterfallStepContext) -> DialogTurnResult: - return await step_context.prompt( - "number", PromptOptions(prompt=MessageFactory.text("Enter a number.")) - ) - - async def __step2(self, step_context: WaterfallStepContext) -> DialogTurnResult: - first: int = step_context.result - step_context.values["first"] = first - - return await step_context.prompt( - "number", - PromptOptions( - prompt=MessageFactory.text(f"I have {first}, now enter another number") - ), - ) - - async def __step3(self, step_context: WaterfallStepContext) -> DialogTurnResult: - first: int = step_context.values["first"] - second: int = step_context.result - - await step_context.prompt( - "number", - PromptOptions( - prompt=MessageFactory.text( - f"The result of the first minus the second is {first - second}." - ) - ), - ) - - return await step_context.end_dialog() diff --git a/samples/42.scaleout/helpers/__init__.py b/samples/42.scaleout/helpers/__init__.py deleted file mode 100644 index a824eb8f4..000000000 --- a/samples/42.scaleout/helpers/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from . import dialog_helper - -__all__ = ["dialog_helper"] diff --git a/samples/42.scaleout/helpers/dialog_helper.py b/samples/42.scaleout/helpers/dialog_helper.py deleted file mode 100644 index 6b2646b0b..000000000 --- a/samples/42.scaleout/helpers/dialog_helper.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import StatePropertyAccessor, TurnContext -from botbuilder.dialogs import Dialog, DialogSet, DialogTurnStatus - - -class DialogHelper: - @staticmethod - async def run_dialog( - dialog: Dialog, turn_context: TurnContext, accessor: StatePropertyAccessor - ): - dialog_set = DialogSet(accessor) - dialog_set.add(dialog) - - dialog_context = await dialog_set.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dialog_context.begin_dialog(dialog.id) diff --git a/samples/42.scaleout/host/__init__.py b/samples/42.scaleout/host/__init__.py deleted file mode 100644 index 3ce168e54..000000000 --- a/samples/42.scaleout/host/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .dialog_host import DialogHost -from .dialog_host_adapter import DialogHostAdapter - -__all__ = ["DialogHost", "DialogHostAdapter"] diff --git a/samples/42.scaleout/host/dialog_host.py b/samples/42.scaleout/host/dialog_host.py deleted file mode 100644 index b7cfe1692..000000000 --- a/samples/42.scaleout/host/dialog_host.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import json - -from jsonpickle import encode -from jsonpickle.unpickler import Unpickler - -from botbuilder.core import TurnContext -from botbuilder.dialogs import Dialog, ComponentDialog -from botbuilder.schema import Activity - -from helpers.dialog_helper import DialogHelper -from host.dialog_host_adapter import DialogHostAdapter -from store import RefAccessor - - -class DialogHost: - """ - The essential code for running a dialog. The execution of the dialog is treated here as a pure function call. - The input being the existing (or old) state and the inbound Activity and the result being the updated (or new) - state and the Activities that should be sent. The assumption is that this code can be re-run without causing any - unintended or harmful side-effects, for example, any outbound service calls made directly from the - dialog implementation should be idempotent. - """ - - @staticmethod - async def run(dialog: Dialog, activity: Activity, old_state) -> (): - """ - A function to run a dialog while buffering the outbound Activities. - """ - - # A custom adapter and corresponding TurnContext that buffers any messages sent. - adapter = DialogHostAdapter() - turn_context = TurnContext(adapter, activity) - - # Run the dialog using this TurnContext with the existing state. - new_state = await DialogHost.__run_turn(dialog, turn_context, old_state) - - # The result is a set of activities to send and a replacement state. - return adapter.activities, new_state - - @staticmethod - async def __run_turn(dialog: Dialog, turn_context: TurnContext, state): - """ - Execute the turn of the bot. The functionality here closely resembles that which is found in the - Bot.on_turn method in an implementation that is using the regular BotFrameworkAdapter. - Also here in this example the focus is explicitly on Dialogs but the pattern could be adapted - to other conversation modeling abstractions. - """ - # If we have some state, deserialize it. (This mimics the shape produced by BotState.cs.) - dialog_state_property = ( - state[ComponentDialog.persisted_dialog_state] if state else None - ) - dialog_state = ( - None - if not dialog_state_property - else Unpickler().restore(json.loads(dialog_state_property)) - ) - - # A custom accessor is used to pass a handle on the state to the dialog system. - accessor = RefAccessor(dialog_state) - - # Run the dialog. - await DialogHelper.run_dialog(dialog, turn_context, accessor) - - # Serialize the result (available as Value on the accessor), and put its value back into a new json object. - return { - ComponentDialog.persisted_dialog_state: None - if not accessor.value - else encode(accessor.value) - } diff --git a/samples/42.scaleout/host/dialog_host_adapter.py b/samples/42.scaleout/host/dialog_host_adapter.py deleted file mode 100644 index ab7151c0f..000000000 --- a/samples/42.scaleout/host/dialog_host_adapter.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from typing import List - -from botbuilder.core import BotAdapter, TurnContext -from botbuilder.schema import Activity, ConversationReference - - -class DialogHostAdapter(BotAdapter): - """ - This custom BotAdapter supports scenarios that only Send Activities. Update and Delete Activity - are not supported. - Rather than sending the outbound Activities directly as the BotFrameworkAdapter does this class - buffers them in a list. The list is exposed as a public property. - """ - - def __init__(self): - super(DialogHostAdapter, self).__init__() - self.activities = [] - - async def send_activities(self, context: TurnContext, activities: List[Activity]): - self.activities.extend(activities) - return [] - - async def update_activity(self, context: TurnContext, activity: Activity): - raise NotImplementedError - - async def delete_activity( - self, context: TurnContext, reference: ConversationReference - ): - raise NotImplementedError diff --git a/samples/42.scaleout/requirements.txt b/samples/42.scaleout/requirements.txt deleted file mode 100644 index 4760c7682..000000000 --- a/samples/42.scaleout/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -jsonpickle -botbuilder-core>=4.4.0b1 -azure>=4.0.0 -flask>=1.0.3 diff --git a/samples/42.scaleout/store/__init__.py b/samples/42.scaleout/store/__init__.py deleted file mode 100644 index 0aaa4235a..000000000 --- a/samples/42.scaleout/store/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .store import Store -from .memory_store import MemoryStore -from .blob_store import BlobStore -from .ref_accessor import RefAccessor - -__all__ = ["Store", "MemoryStore", "BlobStore", "RefAccessor"] diff --git a/samples/42.scaleout/store/blob_store.py b/samples/42.scaleout/store/blob_store.py deleted file mode 100644 index c17ebd2c6..000000000 --- a/samples/42.scaleout/store/blob_store.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import json - -from azure.storage.blob import BlockBlobService, PublicAccess -from jsonpickle import encode -from jsonpickle.unpickler import Unpickler - -from store.store import Store - - -class BlobStore(Store): - """ - An implementation of the ETag aware Store interface against Azure Blob Storage. - """ - - def __init__(self, account_name: str, account_key: str, container_name: str): - self.container_name = container_name - self.client = BlockBlobService( - account_name=account_name, account_key=account_key - ) - - async def load(self, key: str) -> (): - self.client.create_container(self.container_name) - self.client.set_container_acl( - self.container_name, public_access=PublicAccess.Container - ) - - if not self.client.exists(container_name=self.container_name, blob_name=key): - return None, None - - blob = self.client.get_blob_to_text( - container_name=self.container_name, blob_name=key - ) - return Unpickler().restore(json.loads(blob.content)), blob.properties.etag - - async def save(self, key: str, content, e_tag: str): - self.client.create_container(self.container_name) - self.client.set_container_acl( - self.container_name, public_access=PublicAccess.Container - ) - - self.client.create_blob_from_text( - container_name=self.container_name, - blob_name=key, - text=encode(content), - if_match=e_tag, - ) - - return True diff --git a/samples/42.scaleout/store/memory_store.py b/samples/42.scaleout/store/memory_store.py deleted file mode 100644 index d72293422..000000000 --- a/samples/42.scaleout/store/memory_store.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import uuid -from typing import Tuple - -from store.store import Store - - -class MemoryStore(Store): - """ - Implementation of the IStore abstraction intended for testing. - """ - - def __init__(self): - # dict of Tuples - self.store = {} - - async def load(self, key: str) -> (): - return self.store[key] if key in self.store else (None, None) - - async def save(self, key: str, content, e_tag: str) -> bool: - if e_tag: - value: Tuple = self.store[key] - if value and value[1] != e_tag: - return False - - self.store[key] = (content, str(uuid.uuid4())) - return True diff --git a/samples/42.scaleout/store/ref_accessor.py b/samples/42.scaleout/store/ref_accessor.py deleted file mode 100644 index 45bb5d4a4..000000000 --- a/samples/42.scaleout/store/ref_accessor.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import StatePropertyAccessor, TurnContext - - -class RefAccessor(StatePropertyAccessor): - """ - This is an accessor for any object. By definition objects (as opposed to values) - are returned by reference in the GetAsync call on the accessor. As such the SetAsync - call is never used. The actual act of saving any state to an external store therefore - cannot be encapsulated in the Accessor implementation itself. And so to facilitate this - the state itself is available as a public property on this class. The reason its here is - because the caller of the constructor could pass in null for the state, in which case - the factory provided on the GetAsync call will be used. - """ - - def __init__(self, value): - self.value = value - self.name = type(value).__name__ - - async def get( - self, turn_context: TurnContext, default_value_or_factory=None - ) -> object: - if not self.value: - if not default_value_or_factory: - raise Exception("key not found") - - self.value = default_value_or_factory() - - return self.value - - async def delete(self, turn_context: TurnContext) -> None: - pass - - async def set(self, turn_context: TurnContext, value) -> None: - pass diff --git a/samples/42.scaleout/store/store.py b/samples/42.scaleout/store/store.py deleted file mode 100644 index 4d13e0889..000000000 --- a/samples/42.scaleout/store/store.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from abc import ABC, abstractmethod - - -class Store(ABC): - """ - An ETag aware store definition. - The interface is defined in terms of JObject to move serialization out of the storage layer - while still indicating it is JSON, a fact the store may choose to make use of. - """ - - @abstractmethod - async def load(self, key: str) -> (): - """ - Loads a value from the Store. - :param key: - :return: (object, etag) - """ - raise NotImplementedError - - @abstractmethod - async def save(self, key: str, content, e_tag: str) -> bool: - """ - Saves a values to the Store if the etag matches. - :param key: - :param content: - :param e_tag: - :return: True if the content was saved. - """ - raise NotImplementedError diff --git a/samples/43.complex-dialog/README.md b/samples/43.complex-dialog/README.md deleted file mode 100644 index 1605fcce5..000000000 --- a/samples/43.complex-dialog/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# Complex dialog sample - -This sample creates a complex conversation with dialogs. - -## Running the sample -- Clone the repository -```bash -git clone https://github.com/Microsoft/botbuilder-python.git -``` -- Activate your desired virtual environment -- Bring up a terminal, navigate to `botbuilder-python\samples\43.complex-dialog` folder -- In the terminal, type `pip install -r requirements.txt` -- In the terminal, type `python app.py` - -## Testing the bot using Bot Framework Emulator -[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to bot using Bot Framework Emulator -- Launch Bot Framework Emulator -- File -> Open Bot -- Paste this URL in the emulator window - http://localhost:3978/api/messages - - -# Further reading - -- [Bot Framework Documentation](https://docs.botframework.com) -- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) -- [Dialogs](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0) \ No newline at end of file diff --git a/samples/43.complex-dialog/app.py b/samples/43.complex-dialog/app.py deleted file mode 100644 index f18a309d1..000000000 --- a/samples/43.complex-dialog/app.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import asyncio -import sys -from datetime import datetime - -from flask import Flask, request, Response -from botbuilder.core import ( - BotFrameworkAdapter, - BotFrameworkAdapterSettings, - ConversationState, - MemoryStorage, - TurnContext, - UserState, -) -from botbuilder.schema import Activity, ActivityTypes - -from bots import DialogAndWelcomeBot - -# Create the loop and Flask app -from dialogs import MainDialog - -LOOP = asyncio.get_event_loop() -APP = Flask(__name__, instance_relative_config=True) -APP.config.from_object("config.DefaultConfig") - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"]) -ADAPTER = BotFrameworkAdapter(SETTINGS) - - -# Catch-all for errors. -async def on_error(context: TurnContext, error: Exception): - # This check writes out errors to console log .vs. app insights. - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - - # Send a message to the user - await context.send_activity("The bot encountered an error or bug.") - await context.send_activity( - "To continue to run this bot, please fix the bot source code." - ) - # Send a trace activity if we're talking to the Bot Framework Emulator - if context.activity.channel_id == "emulator": - # Create a trace activity that contains the error object - trace_activity = Activity( - label="TurnError", - name="on_turn_error Trace", - timestamp=datetime.utcnow(), - type=ActivityTypes.trace, - value=f"{error}", - value_type="https://www.botframework.com/schemas/error", - ) - # Send a trace activity, which will be displayed in Bot Framework Emulator - await context.send_activity(trace_activity) - - # Clear out state - await CONVERSATION_STATE.delete(context) - - -# Set the error handler on the Adapter. -# In this case, we want an unbound function, so MethodType is not needed. -ADAPTER.on_turn_error = on_error - -# Create MemoryStorage and state -MEMORY = MemoryStorage() -USER_STATE = UserState(MEMORY) -CONVERSATION_STATE = ConversationState(MEMORY) - -# Create Dialog and Bot -DIALOG = MainDialog(USER_STATE) -BOT = DialogAndWelcomeBot(CONVERSATION_STATE, USER_STATE, DIALOG) - - -# Listen for incoming requests on /api/messages. -@APP.route("/api/messages", methods=["POST"]) -def messages(): - # Main bot message handler. - if "application/json" in request.headers["Content-Type"]: - body = request.json - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = ( - request.headers["Authorization"] if "Authorization" in request.headers else "" - ) - - try: - task = LOOP.create_task( - ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - ) - LOOP.run_until_complete(task) - return Response(status=201) - except Exception as exception: - raise exception - - -if __name__ == "__main__": - try: - APP.run(debug=False, port=APP.config["PORT"]) # nosec debug - except Exception as exception: - raise exception diff --git a/samples/43.complex-dialog/bots/__init__.py b/samples/43.complex-dialog/bots/__init__.py deleted file mode 100644 index 6925db302..000000000 --- a/samples/43.complex-dialog/bots/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .dialog_bot import DialogBot -from .dialog_and_welcome_bot import DialogAndWelcomeBot - -__all__ = ["DialogBot", "DialogAndWelcomeBot"] diff --git a/samples/43.complex-dialog/bots/dialog_and_welcome_bot.py b/samples/43.complex-dialog/bots/dialog_and_welcome_bot.py deleted file mode 100644 index 68c3c9a30..000000000 --- a/samples/43.complex-dialog/bots/dialog_and_welcome_bot.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from typing import List -from botbuilder.core import ( - ConversationState, - MessageFactory, - UserState, - TurnContext, -) -from botbuilder.dialogs import Dialog -from botbuilder.schema import ChannelAccount - -from .dialog_bot import DialogBot - - -class DialogAndWelcomeBot(DialogBot): - def __init__( - self, - conversation_state: ConversationState, - user_state: UserState, - dialog: Dialog, - ): - super(DialogAndWelcomeBot, self).__init__( - conversation_state, user_state, dialog - ) - - async def on_members_added_activity( - self, members_added: List[ChannelAccount], turn_context: TurnContext - ): - for member in members_added: - # Greet anyone that was not the target (recipient) of this message. - if member.id != turn_context.activity.recipient.id: - await turn_context.send_activity( - MessageFactory.text( - f"Welcome to Complex Dialog Bot {member.name}. This bot provides a complex conversation, with " - f"multiple dialogs. Type anything to get started. " - ) - ) diff --git a/samples/43.complex-dialog/bots/dialog_bot.py b/samples/43.complex-dialog/bots/dialog_bot.py deleted file mode 100644 index eb560a1be..000000000 --- a/samples/43.complex-dialog/bots/dialog_bot.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import ActivityHandler, ConversationState, UserState, TurnContext -from botbuilder.dialogs import Dialog -from helpers.dialog_helper import DialogHelper - - -class DialogBot(ActivityHandler): - def __init__( - self, - conversation_state: ConversationState, - user_state: UserState, - dialog: Dialog, - ): - if conversation_state is None: - raise Exception( - "[DialogBot]: Missing parameter. conversation_state is required" - ) - if user_state is None: - raise Exception("[DialogBot]: Missing parameter. user_state is required") - if dialog is None: - raise Exception("[DialogBot]: Missing parameter. dialog is required") - - self.conversation_state = conversation_state - self.user_state = user_state - self.dialog = dialog - - async def on_turn(self, turn_context: TurnContext): - await super().on_turn(turn_context) - - # Save any state changes that might have occurred during the turn. - await self.conversation_state.save_changes(turn_context, False) - await self.user_state.save_changes(turn_context, False) - - async def on_message_activity(self, turn_context: TurnContext): - await DialogHelper.run_dialog( - self.dialog, - turn_context, - self.conversation_state.create_property("DialogState"), - ) diff --git a/samples/43.complex-dialog/config.py b/samples/43.complex-dialog/config.py deleted file mode 100644 index e007d0fa9..000000000 --- a/samples/43.complex-dialog/config.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - -""" Bot Configuration """ - - -class DefaultConfig: - """ Bot Configuration """ - - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") diff --git a/samples/43.complex-dialog/data_models/__init__.py b/samples/43.complex-dialog/data_models/__init__.py deleted file mode 100644 index 35a5934d4..000000000 --- a/samples/43.complex-dialog/data_models/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .user_profile import UserProfile - -__all__ = ["UserProfile"] diff --git a/samples/43.complex-dialog/data_models/user_profile.py b/samples/43.complex-dialog/data_models/user_profile.py deleted file mode 100644 index 0267721d4..000000000 --- a/samples/43.complex-dialog/data_models/user_profile.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from typing import List - - -class UserProfile: - def __init__( - self, name: str = None, age: int = 0, companies_to_review: List[str] = None - ): - self.name: str = name - self.age: int = age - self.companies_to_review: List[str] = companies_to_review diff --git a/samples/43.complex-dialog/dialogs/__init__.py b/samples/43.complex-dialog/dialogs/__init__.py deleted file mode 100644 index cde97fd80..000000000 --- a/samples/43.complex-dialog/dialogs/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .main_dialog import MainDialog -from .review_selection_dialog import ReviewSelectionDialog -from .top_level_dialog import TopLevelDialog - -__all__ = ["MainDialog", "ReviewSelectionDialog", "TopLevelDialog"] diff --git a/samples/43.complex-dialog/dialogs/main_dialog.py b/samples/43.complex-dialog/dialogs/main_dialog.py deleted file mode 100644 index 8b3fcd82d..000000000 --- a/samples/43.complex-dialog/dialogs/main_dialog.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.dialogs import ( - ComponentDialog, - WaterfallDialog, - WaterfallStepContext, - DialogTurnResult, -) -from botbuilder.core import MessageFactory, UserState - -from data_models import UserProfile -from dialogs.top_level_dialog import TopLevelDialog - - -class MainDialog(ComponentDialog): - def __init__(self, user_state: UserState): - super(MainDialog, self).__init__(MainDialog.__name__) - - self.user_state = user_state - - self.add_dialog(TopLevelDialog(TopLevelDialog.__name__)) - self.add_dialog( - WaterfallDialog("WFDialog", [self.initial_step, self.final_step]) - ) - - self.initial_dialog_id = "WFDialog" - - async def initial_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - return await step_context.begin_dialog(TopLevelDialog.__name__) - - async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - user_info: UserProfile = step_context.result - - companies = ( - "no companies" - if len(user_info.companies_to_review) == 0 - else " and ".join(user_info.companies_to_review) - ) - status = f"You are signed up to review {companies}." - - await step_context.context.send_activity(MessageFactory.text(status)) - - # store the UserProfile - accessor = self.user_state.create_property("UserProfile") - await accessor.set(step_context.context, user_info) - - return await step_context.end_dialog() diff --git a/samples/43.complex-dialog/dialogs/review_selection_dialog.py b/samples/43.complex-dialog/dialogs/review_selection_dialog.py deleted file mode 100644 index 2119068bb..000000000 --- a/samples/43.complex-dialog/dialogs/review_selection_dialog.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from typing import List - -from botbuilder.dialogs import ( - WaterfallDialog, - WaterfallStepContext, - DialogTurnResult, - ComponentDialog, -) -from botbuilder.dialogs.prompts import ChoicePrompt, PromptOptions -from botbuilder.dialogs.choices import Choice, FoundChoice -from botbuilder.core import MessageFactory - - -class ReviewSelectionDialog(ComponentDialog): - def __init__(self, dialog_id: str = None): - super(ReviewSelectionDialog, self).__init__( - dialog_id or ReviewSelectionDialog.__name__ - ) - - self.COMPANIES_SELECTED = "value-companiesSelected" - self.DONE_OPTION = "done" - - self.company_options = [ - "Adatum Corporation", - "Contoso Suites", - "Graphic Design Institute", - "Wide World Importers", - ] - - self.add_dialog(ChoicePrompt(ChoicePrompt.__name__)) - self.add_dialog( - WaterfallDialog( - WaterfallDialog.__name__, [self.selection_step, self.loop_step] - ) - ) - - self.initial_dialog_id = WaterfallDialog.__name__ - - async def selection_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - # step_context.options will contains the value passed in begin_dialog or replace_dialog. - # if this value wasn't provided then start with an emtpy selection list. This list will - # eventually be returned to the parent via end_dialog. - selected: [ - str - ] = step_context.options if step_context.options is not None else [] - step_context.values[self.COMPANIES_SELECTED] = selected - - if len(selected) == 0: - message = ( - f"Please choose a company to review, or `{self.DONE_OPTION}` to finish." - ) - else: - message = ( - f"You have selected **{selected[0]}**. You can review an additional company, " - f"or choose `{self.DONE_OPTION}` to finish. " - ) - - # create a list of options to choose, with already selected items removed. - options = self.company_options.copy() - options.append(self.DONE_OPTION) - if len(selected) > 0: - options.remove(selected[0]) - - # prompt with the list of choices - prompt_options = PromptOptions( - prompt=MessageFactory.text(message), - retry_prompt=MessageFactory.text("Please choose an option from the list."), - choices=self._to_choices(options), - ) - return await step_context.prompt(ChoicePrompt.__name__, prompt_options) - - def _to_choices(self, choices: [str]) -> List[Choice]: - choice_list: List[Choice] = [] - for choice in choices: - choice_list.append(Choice(value=choice)) - return choice_list - - async def loop_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - selected: List[str] = step_context.values[self.COMPANIES_SELECTED] - choice: FoundChoice = step_context.result - done = choice.value == self.DONE_OPTION - - # If they chose a company, add it to the list. - if not done: - selected.append(choice.value) - - # If they're done, exit and return their list. - if done or len(selected) >= 2: - return await step_context.end_dialog(selected) - - # Otherwise, repeat this dialog, passing in the selections from this iteration. - return await step_context.replace_dialog( - ReviewSelectionDialog.__name__, selected - ) diff --git a/samples/43.complex-dialog/dialogs/top_level_dialog.py b/samples/43.complex-dialog/dialogs/top_level_dialog.py deleted file mode 100644 index 4342e668f..000000000 --- a/samples/43.complex-dialog/dialogs/top_level_dialog.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import MessageFactory -from botbuilder.dialogs import ( - WaterfallDialog, - DialogTurnResult, - WaterfallStepContext, - ComponentDialog, -) -from botbuilder.dialogs.prompts import PromptOptions, TextPrompt, NumberPrompt - -from data_models import UserProfile -from dialogs.review_selection_dialog import ReviewSelectionDialog - - -class TopLevelDialog(ComponentDialog): - def __init__(self, dialog_id: str = None): - super(TopLevelDialog, self).__init__(dialog_id or TopLevelDialog.__name__) - - # Key name to store this dialogs state info in the StepContext - self.USER_INFO = "value-userInfo" - - self.add_dialog(TextPrompt(TextPrompt.__name__)) - self.add_dialog(NumberPrompt(NumberPrompt.__name__)) - - self.add_dialog(ReviewSelectionDialog(ReviewSelectionDialog.__name__)) - - self.add_dialog( - WaterfallDialog( - "WFDialog", - [ - self.name_step, - self.age_step, - self.start_selection_step, - self.acknowledgement_step, - ], - ) - ) - - self.initial_dialog_id = "WFDialog" - - async def name_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - # Create an object in which to collect the user's information within the dialog. - step_context.values[self.USER_INFO] = UserProfile() - - # Ask the user to enter their name. - prompt_options = PromptOptions( - prompt=MessageFactory.text("Please enter your name.") - ) - return await step_context.prompt(TextPrompt.__name__, prompt_options) - - async def age_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - # Set the user's name to what they entered in response to the name prompt. - user_profile = step_context.values[self.USER_INFO] - user_profile.name = step_context.result - - # Ask the user to enter their age. - prompt_options = PromptOptions( - prompt=MessageFactory.text("Please enter your age.") - ) - return await step_context.prompt(NumberPrompt.__name__, prompt_options) - - async def start_selection_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - # Set the user's age to what they entered in response to the age prompt. - user_profile: UserProfile = step_context.values[self.USER_INFO] - user_profile.age = step_context.result - - if user_profile.age < 25: - # If they are too young, skip the review selection dialog, and pass an empty list to the next step. - await step_context.context.send_activity( - MessageFactory.text("You must be 25 or older to participate.") - ) - - return await step_context.next([]) - - # Otherwise, start the review selection dialog. - return await step_context.begin_dialog(ReviewSelectionDialog.__name__) - - async def acknowledgement_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - # Set the user's company selection to what they entered in the review-selection dialog. - user_profile: UserProfile = step_context.values[self.USER_INFO] - user_profile.companies_to_review = step_context.result - - # Thank them for participating. - await step_context.context.send_activity( - MessageFactory.text(f"Thanks for participating, {user_profile.name}.") - ) - - # Exit the dialog, returning the collected user information. - return await step_context.end_dialog(user_profile) diff --git a/samples/43.complex-dialog/helpers/__init__.py b/samples/43.complex-dialog/helpers/__init__.py deleted file mode 100644 index a824eb8f4..000000000 --- a/samples/43.complex-dialog/helpers/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from . import dialog_helper - -__all__ = ["dialog_helper"] diff --git a/samples/43.complex-dialog/helpers/dialog_helper.py b/samples/43.complex-dialog/helpers/dialog_helper.py deleted file mode 100644 index 6b2646b0b..000000000 --- a/samples/43.complex-dialog/helpers/dialog_helper.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import StatePropertyAccessor, TurnContext -from botbuilder.dialogs import Dialog, DialogSet, DialogTurnStatus - - -class DialogHelper: - @staticmethod - async def run_dialog( - dialog: Dialog, turn_context: TurnContext, accessor: StatePropertyAccessor - ): - dialog_set = DialogSet(accessor) - dialog_set.add(dialog) - - dialog_context = await dialog_set.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dialog_context.begin_dialog(dialog.id) diff --git a/samples/43.complex-dialog/requirements.txt b/samples/43.complex-dialog/requirements.txt deleted file mode 100644 index 7e54b62ec..000000000 --- a/samples/43.complex-dialog/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -botbuilder-core>=4.4.0b1 -flask>=1.0.3 diff --git a/samples/44.prompt-users-for-input/README.md b/samples/44.prompt-users-for-input/README.md deleted file mode 100644 index 527bb8a82..000000000 --- a/samples/44.prompt-users-for-input/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# Prompt users for input - -This sample demonstrates how to create your own prompts with the Python Bot Framework. -The bot maintains conversation state to track and direct the conversation and ask the user questions. -The bot maintains user state to track the user's answers. - -## Running the sample -- Clone the repository -```bash -git clone https://github.com/Microsoft/botbuilder-python.git -``` -- Bring up a terminal, navigate to `botbuilder-python\samples\44.prompt-user-for-input` folder -- In the terminal, type `pip install -r requirements.txt` -- In the terminal, type `python app.py` - -## Testing the bot using Bot Framework Emulator -[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to bot using Bot Framework Emulator -- Launch Bot Framework Emulator -- File -> Open Bot -- Paste this URL in the emulator window - http://localhost:3978/api/messages - - -## Bot State - -A key to good bot design is to track the context of a conversation, so that your bot remembers things like the answers to previous questions. Depending on what your bot is used for, you may even need to keep track of state or store information for longer than the lifetime of the conversation. A bot's state is information it remembers in order to respond appropriately to incoming messages. The Bot Builder SDK provides classes for storing and retrieving state data as an object associated with a user or a conversation. - -# Further reading - -- [Azure Bot Service Introduction](https://docs.microsoft.com/en-us/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) -- [Bot State](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-storage-concept?view=azure-bot-service-4.0) -- [Write directly to storage](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-v4-storage?view=azure-bot-service-4.0&tabs=csharpechorproperty%2Ccsetagoverwrite%2Ccsetag) -- [Managing conversation and user state](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-v4-state?view=azure-bot-service-4.0) -- [Microsoft Recognizers-Text](https://github.com/Microsoft/Recognizers-Text/tree/master/Python) \ No newline at end of file diff --git a/samples/44.prompt-users-for-input/app.py b/samples/44.prompt-users-for-input/app.py deleted file mode 100644 index 34633b1fe..000000000 --- a/samples/44.prompt-users-for-input/app.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import asyncio -import sys -from datetime import datetime - -from flask import Flask, request, Response -from botbuilder.core import ( - BotFrameworkAdapter, - BotFrameworkAdapterSettings, - ConversationState, - MemoryStorage, - TurnContext, - UserState, -) -from botbuilder.schema import Activity, ActivityTypes - -from bots import CustomPromptBot - -# Create the loop and Flask app -LOOP = asyncio.get_event_loop() -APP = Flask(__name__, instance_relative_config=True) -APP.config.from_object("config.DefaultConfig") - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"]) -ADAPTER = BotFrameworkAdapter(SETTINGS) - - -# Catch-all for errors. -async def on_error(context: TurnContext, error: Exception): - # This check writes out errors to console log .vs. app insights. - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - - # Send a message to the user - await context.send_activity("The bot encountered an error or bug.") - await context.send_activity( - "To continue to run this bot, please fix the bot source code." - ) - # Send a trace activity if we're talking to the Bot Framework Emulator - if context.activity.channel_id == "emulator": - # Create a trace activity that contains the error object - trace_activity = Activity( - label="TurnError", - name="on_turn_error Trace", - timestamp=datetime.utcnow(), - type=ActivityTypes.trace, - value=f"{error}", - value_type="https://www.botframework.com/schemas/error", - ) - # Send a trace activity, which will be displayed in Bot Framework Emulator - await context.send_activity(trace_activity) - - # Clear out state - await CONVERSATION_STATE.delete(context) - - -# Set the error handler on the Adapter. -# In this case, we want an unbound method, so MethodType is not needed. -ADAPTER.on_turn_error = on_error - -# Create MemoryStorage and state -MEMORY = MemoryStorage() -USER_STATE = UserState(MEMORY) -CONVERSATION_STATE = ConversationState(MEMORY) - -# Create Bot -BOT = CustomPromptBot(CONVERSATION_STATE, USER_STATE) - - -# Listen for incoming requests on /api/messages. -@APP.route("/api/messages", methods=["POST"]) -def messages(): - # Main bot message handler. - if "application/json" in request.headers["Content-Type"]: - body = request.json - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = ( - request.headers["Authorization"] if "Authorization" in request.headers else "" - ) - - try: - task = LOOP.create_task( - ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - ) - LOOP.run_until_complete(task) - return Response(status=201) - except Exception as exception: - raise exception - - -if __name__ == "__main__": - try: - APP.run(debug=False, port=APP.config["PORT"]) # nosec debug - except Exception as exception: - raise exception diff --git a/samples/44.prompt-users-for-input/bots/__init__.py b/samples/44.prompt-users-for-input/bots/__init__.py deleted file mode 100644 index 87a52e887..000000000 --- a/samples/44.prompt-users-for-input/bots/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .custom_prompt_bot import CustomPromptBot - -__all__ = ["CustomPromptBot"] diff --git a/samples/44.prompt-users-for-input/bots/custom_prompt_bot.py b/samples/44.prompt-users-for-input/bots/custom_prompt_bot.py deleted file mode 100644 index 693eee92a..000000000 --- a/samples/44.prompt-users-for-input/bots/custom_prompt_bot.py +++ /dev/null @@ -1,189 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from datetime import datetime - -from recognizers_number import recognize_number, Culture -from recognizers_date_time import recognize_datetime - -from botbuilder.core import ( - ActivityHandler, - ConversationState, - TurnContext, - UserState, - MessageFactory, -) - -from data_models import ConversationFlow, Question, UserProfile - - -class ValidationResult: - def __init__( - self, is_valid: bool = False, value: object = None, message: str = None - ): - self.is_valid = is_valid - self.value = value - self.message = message - - -class CustomPromptBot(ActivityHandler): - def __init__(self, conversation_state: ConversationState, user_state: UserState): - if conversation_state is None: - raise TypeError( - "[CustomPromptBot]: Missing parameter. conversation_state is required but None was given" - ) - if user_state is None: - raise TypeError( - "[CustomPromptBot]: Missing parameter. user_state is required but None was given" - ) - - self.conversation_state = conversation_state - self.user_state = user_state - - self.flow_accessor = self.conversation_state.create_property("ConversationFlow") - self.profile_accessor = self.conversation_state.create_property("UserProfile") - - async def on_message_activity(self, turn_context: TurnContext): - # Get the state properties from the turn context. - profile = await self.profile_accessor.get(turn_context, UserProfile) - flow = await self.flow_accessor.get(turn_context, ConversationFlow) - - await self._fill_out_user_profile(flow, profile, turn_context) - - # Save changes to UserState and ConversationState - await self.conversation_state.save_changes(turn_context) - await self.user_state.save_changes(turn_context) - - async def _fill_out_user_profile( - self, flow: ConversationFlow, profile: UserProfile, turn_context: TurnContext - ): - user_input = turn_context.activity.text.strip() - - # ask for name - if flow.last_question_asked == Question.NONE: - await turn_context.send_activity( - MessageFactory.text("Let's get started. What is your name?") - ) - flow.last_question_asked = Question.NAME - - # validate name then ask for age - elif flow.last_question_asked == Question.NAME: - validate_result = self._validate_name(user_input) - if not validate_result.is_valid: - await turn_context.send_activity( - MessageFactory.text(validate_result.message) - ) - else: - profile.name = validate_result.value - await turn_context.send_activity( - MessageFactory.text(f"Hi {profile.name}") - ) - await turn_context.send_activity( - MessageFactory.text("How old are you?") - ) - flow.last_question_asked = Question.AGE - - # validate age then ask for date - elif flow.last_question_asked == Question.AGE: - validate_result = self._validate_age(user_input) - if not validate_result.is_valid: - await turn_context.send_activity( - MessageFactory.text(validate_result.message) - ) - else: - profile.age = validate_result.value - await turn_context.send_activity( - MessageFactory.text(f"I have your age as {profile.age}.") - ) - await turn_context.send_activity( - MessageFactory.text("When is your flight?") - ) - flow.last_question_asked = Question.DATE - - # validate date and wrap it up - elif flow.last_question_asked == Question.DATE: - validate_result = self._validate_date(user_input) - if not validate_result.is_valid: - await turn_context.send_activity( - MessageFactory.text(validate_result.message) - ) - else: - profile.date = validate_result.value - await turn_context.send_activity( - MessageFactory.text( - f"Your cab ride to the airport is scheduled for {profile.date}." - ) - ) - await turn_context.send_activity( - MessageFactory.text( - f"Thanks for completing the booking {profile.name}." - ) - ) - await turn_context.send_activity( - MessageFactory.text("Type anything to run the bot again.") - ) - flow.last_question_asked = Question.NONE - - def _validate_name(self, user_input: str) -> ValidationResult: - if not user_input: - return ValidationResult( - is_valid=False, - message="Please enter a name that contains at least one character.", - ) - - return ValidationResult(is_valid=True, value=user_input) - - def _validate_age(self, user_input: str) -> ValidationResult: - # Attempt to convert the Recognizer result to an integer. This works for "a dozen", "twelve", "12", and so on. - # The recognizer returns a list of potential recognition results, if any. - results = recognize_number(user_input, Culture.English) - for result in results: - if "value" in result.resolution: - age = int(result.resolution["value"]) - if 18 <= age <= 120: - return ValidationResult(is_valid=True, value=age) - - return ValidationResult( - is_valid=False, message="Please enter an age between 18 and 120." - ) - - def _validate_date(self, user_input: str) -> ValidationResult: - try: - # Try to recognize the input as a date-time. This works for responses such as "11/14/2018", "9pm", - # "tomorrow", "Sunday at 5pm", and so on. The recognizer returns a list of potential recognition results, - # if any. - results = recognize_datetime(user_input, Culture.English) - for result in results: - for resolution in result.resolution["values"]: - if "value" in resolution: - now = datetime.now() - - value = resolution["value"] - if resolution["type"] == "date": - candidate = datetime.strptime(value, "%Y-%m-%d") - elif resolution["type"] == "time": - candidate = datetime.strptime(value, "%H:%M:%S") - candidate = candidate.replace( - year=now.year, month=now.month, day=now.day - ) - else: - candidate = datetime.strptime(value, "%Y-%m-%d %H:%M:%S") - - # user response must be more than an hour out - diff = candidate - now - if diff.total_seconds() >= 3600: - return ValidationResult( - is_valid=True, - value=candidate.strftime("%m/%d/%y @ %H:%M"), - ) - - return ValidationResult( - is_valid=False, - message="I'm sorry, please enter a date at least an hour out.", - ) - except ValueError: - return ValidationResult( - is_valid=False, - message="I'm sorry, I could not interpret that as an appropriate " - "date. Please enter a date at least an hour out.", - ) diff --git a/samples/44.prompt-users-for-input/config.py b/samples/44.prompt-users-for-input/config.py deleted file mode 100644 index e007d0fa9..000000000 --- a/samples/44.prompt-users-for-input/config.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - -""" Bot Configuration """ - - -class DefaultConfig: - """ Bot Configuration """ - - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") diff --git a/samples/44.prompt-users-for-input/data_models/__init__.py b/samples/44.prompt-users-for-input/data_models/__init__.py deleted file mode 100644 index 1ca181322..000000000 --- a/samples/44.prompt-users-for-input/data_models/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .conversation_flow import ConversationFlow, Question -from .user_profile import UserProfile - -__all__ = ["ConversationFlow", "Question", "UserProfile"] diff --git a/samples/44.prompt-users-for-input/data_models/conversation_flow.py b/samples/44.prompt-users-for-input/data_models/conversation_flow.py deleted file mode 100644 index f848db64f..000000000 --- a/samples/44.prompt-users-for-input/data_models/conversation_flow.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from enum import Enum - - -class Question(Enum): - NAME = 1 - AGE = 2 - DATE = 3 - NONE = 4 - - -class ConversationFlow: - def __init__( - self, last_question_asked: Question = Question.NONE, - ): - self.last_question_asked = last_question_asked diff --git a/samples/44.prompt-users-for-input/data_models/user_profile.py b/samples/44.prompt-users-for-input/data_models/user_profile.py deleted file mode 100644 index b1c40978e..000000000 --- a/samples/44.prompt-users-for-input/data_models/user_profile.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -class UserProfile: - def __init__(self, name: str = None, age: int = 0, date: str = None): - self.name = name - self.age = age - self.date = date diff --git a/samples/44.prompt-users-for-input/requirements.txt b/samples/44.prompt-users-for-input/requirements.txt deleted file mode 100644 index 5a3de5833..000000000 --- a/samples/44.prompt-users-for-input/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -botbuilder-core>=4.4.0b1 -flask>=1.0.3 -recognizers-text>=1.0.2a1 diff --git a/samples/45.state-management/README.md b/samples/45.state-management/README.md deleted file mode 100644 index f6ca355a4..000000000 --- a/samples/45.state-management/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# Save user and conversation data - -This sample demonstrates how to save user and conversation data in a Python bot. -The bot maintains conversation state to track and direct the conversation and ask the user questions. -The bot maintains user state to track the user's answers. - -## Running the sample -- Clone the repository -```bash -git clone https://github.com/Microsoft/botbuilder-python.git -``` -- Activate your desired virtual environment -- Bring up a terminal, navigate to `botbuilder-python\samples\45.state-management` folder -- In the terminal, type `pip install -r requirements.txt` -- In the terminal, type `python app.py` - -## Testing the bot using Bot Framework Emulator -[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to bot using Bot Framework Emulator -- Launch Bot Framework Emulator -- Paste this URL in the emulator window - http://localhost:3978/api/messages - - -## Bot State - -A key to good bot design is to track the context of a conversation, so that your bot remembers things like the answers to previous questions. Depending on what your bot is used for, you may even need to keep track of state or store information for longer than the lifetime of the conversation. A bot's state is information it remembers in order to respond appropriately to incoming messages. The Bot Builder SDK provides classes for storing and retrieving state data as an object associated with a user or a conversation. - -# Further reading - -- [Azure Bot Service Introduction](https://docs.microsoft.com/en-us/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) -- [Bot State](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-storage-concept?view=azure-bot-service-4.0) -- [Write directly to storage](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-v4-storage?view=azure-bot-service-4.0&tabs=csharpechorproperty%2Ccsetagoverwrite%2Ccsetag) -- [Managing conversation and user state](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-v4-state?view=azure-bot-service-4.0) \ No newline at end of file diff --git a/samples/45.state-management/app.py b/samples/45.state-management/app.py deleted file mode 100644 index 4609c2881..000000000 --- a/samples/45.state-management/app.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import asyncio -import sys -from datetime import datetime - -from flask import Flask, request, Response -from botbuilder.core import ( - BotFrameworkAdapter, - BotFrameworkAdapterSettings, - ConversationState, - MemoryStorage, - TurnContext, - UserState, -) -from botbuilder.schema import Activity, ActivityTypes - -from bots import StateManagementBot - -# Create the loop and Flask app -LOOP = asyncio.get_event_loop() -APP = Flask(__name__, instance_relative_config=True) -APP.config.from_object("config.DefaultConfig") - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"]) -ADAPTER = BotFrameworkAdapter(SETTINGS) - - -# Catch-all for errors. -async def on_error(context: TurnContext, error: Exception): - # This check writes out errors to console log .vs. app insights. - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - - # Send a message to the user - await context.send_activity("The bot encountered an error or bug.") - await context.send_activity("To continue to run this bot, please fix the bot source code.") - # Send a trace activity if we're talking to the Bot Framework Emulator - if context.activity.channel_id == 'emulator': - # Create a trace activity that contains the error object - trace_activity = Activity( - label="TurnError", - name="on_turn_error Trace", - timestamp=datetime.utcnow(), - type=ActivityTypes.trace, - value=f"{error}", - value_type="https://www.botframework.com/schemas/error" - ) - # Send a trace activity, which will be displayed in Bot Framework Emulator - await context.send_activity(trace_activity) - - # Clear out state - await CONVERSATION_STATE.delete(context) - -# Set the error handler on the Adapter. -# In this case, we want an unbound method, so MethodType is not needed. -ADAPTER.on_turn_error = on_error - -# Create MemoryStorage and state -MEMORY = MemoryStorage() -USER_STATE = UserState(MEMORY) -CONVERSATION_STATE = ConversationState(MEMORY) - -# Create Bot -BOT = StateManagementBot(CONVERSATION_STATE, USER_STATE) - - -# Listen for incoming requests on /api/messages. -@APP.route("/api/messages", methods=["POST"]) -def messages(): - # Main bot message handler. - if "application/json" in request.headers["Content-Type"]: - body = request.json - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = ( - request.headers["Authorization"] if "Authorization" in request.headers else "" - ) - - try: - task = LOOP.create_task( - ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - ) - LOOP.run_until_complete(task) - return Response(status=201) - except Exception as exception: - raise exception - - -if __name__ == "__main__": - try: - APP.run(debug=False, port=APP.config["PORT"]) # nosec debug - except Exception as exception: - raise exception diff --git a/samples/45.state-management/bots/__init__.py b/samples/45.state-management/bots/__init__.py deleted file mode 100644 index 535957236..000000000 --- a/samples/45.state-management/bots/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .state_management_bot import StateManagementBot - -__all__ = ["StateManagementBot"] diff --git a/samples/45.state-management/bots/state_management_bot.py b/samples/45.state-management/bots/state_management_bot.py deleted file mode 100644 index 47b8b21f8..000000000 --- a/samples/45.state-management/bots/state_management_bot.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import time -from datetime import datetime - -from botbuilder.core import ActivityHandler, ConversationState, TurnContext, UserState -from botbuilder.schema import ChannelAccount - -from data_models import ConversationData, UserProfile - - -class StateManagementBot(ActivityHandler): - def __init__(self, conversation_state: ConversationState, user_state: UserState): - if conversation_state is None: - raise TypeError( - "[StateManagementBot]: Missing parameter. conversation_state is required but None was given" - ) - if user_state is None: - raise TypeError( - "[StateManagementBot]: Missing parameter. user_state is required but None was given" - ) - - self.conversation_state = conversation_state - self.user_state = user_state - - self.conversation_data_accessor = self.conversation_state.create_property( - "ConversationData" - ) - self.user_profile_accessor = self.user_state.create_property("UserProfile") - - async def on_turn(self, turn_context: TurnContext): - await super().on_turn(turn_context) - - await self.conversation_state.save_changes(turn_context) - await self.user_state.save_changes(turn_context) - - async def on_members_added_activity( - self, members_added: [ChannelAccount], turn_context: TurnContext - ): - for member in members_added: - if member.id != turn_context.activity.recipient.id: - await turn_context.send_activity( - "Welcome to State Bot Sample. Type anything to get started." - ) - - async def on_message_activity(self, turn_context: TurnContext): - # Get the state properties from the turn context. - user_profile = await self.user_profile_accessor.get(turn_context, UserProfile) - conversation_data = await self.conversation_data_accessor.get( - turn_context, ConversationData - ) - - if user_profile.name is None: - # First time around this is undefined, so we will prompt user for name. - if conversation_data.prompted_for_user_name: - # Set the name to what the user provided. - user_profile.name = turn_context.activity.text - - # Acknowledge that we got their name. - await turn_context.send_activity( - f"Thanks { user_profile.name }. To see conversation data, type anything." - ) - - # Reset the flag to allow the bot to go though the cycle again. - conversation_data.prompted_for_user_name = False - else: - # Prompt the user for their name. - await turn_context.send_activity("What is your name?") - - # Set the flag to true, so we don't prompt in the next turn. - conversation_data.prompted_for_user_name = True - else: - # Add message details to the conversation data. - conversation_data.timestamp = self.__datetime_from_utc_to_local( - turn_context.activity.timestamp - ) - conversation_data.channel_id = turn_context.activity.channel_id - - # Display state data. - await turn_context.send_activity( - f"{ user_profile.name } sent: { turn_context.activity.text }" - ) - await turn_context.send_activity( - f"Message received at: { conversation_data.timestamp }" - ) - await turn_context.send_activity( - f"Message received from: { conversation_data.channel_id }" - ) - - def __datetime_from_utc_to_local(self, utc_datetime): - now_timestamp = time.time() - offset = datetime.fromtimestamp(now_timestamp) - datetime.utcfromtimestamp( - now_timestamp - ) - result = utc_datetime + offset - return result.strftime("%I:%M:%S %p, %A, %B %d of %Y") diff --git a/samples/45.state-management/config.py b/samples/45.state-management/config.py deleted file mode 100644 index e007d0fa9..000000000 --- a/samples/45.state-management/config.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - -""" Bot Configuration """ - - -class DefaultConfig: - """ Bot Configuration """ - - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") diff --git a/samples/45.state-management/data_models/__init__.py b/samples/45.state-management/data_models/__init__.py deleted file mode 100644 index 4e69f286b..000000000 --- a/samples/45.state-management/data_models/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .conversation_data import ConversationData -from .user_profile import UserProfile - -__all__ = ["ConversationData", "UserProfile"] diff --git a/samples/45.state-management/data_models/conversation_data.py b/samples/45.state-management/data_models/conversation_data.py deleted file mode 100644 index 4b2757e43..000000000 --- a/samples/45.state-management/data_models/conversation_data.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -class ConversationData: - def __init__( - self, - timestamp: str = None, - channel_id: str = None, - prompted_for_user_name: bool = False, - ): - self.timestamp = timestamp - self.channel_id = channel_id - self.prompted_for_user_name = prompted_for_user_name diff --git a/samples/45.state-management/data_models/user_profile.py b/samples/45.state-management/data_models/user_profile.py deleted file mode 100644 index 36add1ea1..000000000 --- a/samples/45.state-management/data_models/user_profile.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -class UserProfile: - def __init__(self, name: str = None): - self.name = name diff --git a/samples/45.state-management/requirements.txt b/samples/45.state-management/requirements.txt deleted file mode 100644 index 7e54b62ec..000000000 --- a/samples/45.state-management/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -botbuilder-core>=4.4.0b1 -flask>=1.0.3 diff --git a/samples/47.inspection/README.md b/samples/47.inspection/README.md deleted file mode 100644 index 6e2c42a08..000000000 --- a/samples/47.inspection/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# Inspection Bot - -Bot Framework v4 Inspection Middleware sample. - -This bot demonstrates a feature called Inspection. This feature allows the Bot Framework Emulator to debug traffic into and out of the bot in addition to looking at the current state of the bot. This is done by having this data sent to the emulator using trace messages. - -This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back. Included in this sample are two counters maintained in User and Conversation state to demonstrate the ability to look at state. - -This runtime behavior is achieved by simply adding a middleware to the Adapter. In this sample you can find that being done in `app.py`. - -More details are available [here](https://github.com/microsoft/BotFramework-Emulator/blob/master/content/CHANNELS.md) - -## Running the sample -- Clone the repository -```bash -git clone https://github.com/Microsoft/botbuilder-python.git -``` -- Bring up a terminal, navigate to `botbuilder-python\samples\47.inspection` folder -- In the terminal, type `pip install -r requirements.txt` -- In the terminal, type `python app.py` - -## Testing the bot using Bot Framework Emulator -[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework Emulator version 4.5.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to the bot using Bot Framework Emulator - -- Launch Bot Framework Emulator -- File -> Open Bot -- Enter a Bot URL of `http://localhost:3978/api/messages` - -### Special Instructions for Running Inspection - -- In the emulator, select Debug -> Start Debugging. -- Enter the endpoint url (http://localhost:8080)/api/messages, and select Connect. -- The result is a trace activity which contains a statement that looks like /INSPECT attach < identifier > -- Right click and copy that response. -- In the original Live Chat session paste the value. -- Now all the traffic will be replicated (as trace activities) to the Emulator Debug tab. - -# Further reading - -- [Getting started with the Bot Inspector](https://github.com/microsoft/BotFramework-Emulator/blob/master/content/CHANNELS.md) -- [Azure Bot Service Introduction](https://docs.microsoft.com/en-us/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) -- [Bot State](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-storage-concept?view=azure-bot-service-4.0) diff --git a/samples/47.inspection/app.py b/samples/47.inspection/app.py deleted file mode 100644 index c699450c5..000000000 --- a/samples/47.inspection/app.py +++ /dev/null @@ -1,117 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import asyncio -import sys -from datetime import datetime - -from flask import Flask, request, Response - -from botbuilder.core import ( - BotFrameworkAdapter, - BotFrameworkAdapterSettings, - ConversationState, - MemoryStorage, - TurnContext, - UserState -) -from botbuilder.core.inspection import InspectionMiddleware, InspectionState -from botbuilder.schema import Activity, ActivityTypes -from botframework.connector.auth import MicrosoftAppCredentials - -from bots import EchoBot - -# Create the loop and Flask app -LOOP = asyncio.get_event_loop() -APP = Flask(__name__, instance_relative_config=True) -APP.config.from_object("config.DefaultConfig") - -# Create adapter. -# See https://aka.ms/about-bot-adapter to learn more about how bots work. -SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"]) -ADAPTER = BotFrameworkAdapter(SETTINGS) - - -# Catch-all for errors. -async def on_error(context: TurnContext, error: Exception): - # This check writes out errors to console log .vs. app insights. - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) - - # Send a message to the user - await context.send_activity("The bot encountered an error or bug.") - await context.send_activity( - "To continue to run this bot, please fix the bot source code." - ) - # Send a trace activity if we're talking to the Bot Framework Emulator - if context.activity.channel_id == "emulator": - # Create a trace activity that contains the error object - trace_activity = Activity( - label="TurnError", - name="on_turn_error Trace", - timestamp=datetime.utcnow(), - type=ActivityTypes.trace, - value=f"{error}", - value_type="https://www.botframework.com/schemas/error", - ) - # Send a trace activity, which will be displayed in Bot Framework Emulator - await context.send_activity(trace_activity) - - # Clear out state - await CONVERSATION_STATE.delete(context) - - -# Set the error handler on the Adapter. -# In this case, we want an unbound method, so MethodType is not needed. -ADAPTER.on_turn_error = on_error - -# Create MemoryStorage and state -MEMORY = MemoryStorage() -USER_STATE = UserState(MEMORY) -CONVERSATION_STATE = ConversationState(MEMORY) - -# Create InspectionMiddleware -INSPECTION_MIDDLEWARE = InspectionMiddleware( - inspection_state=InspectionState(MEMORY), - user_state=USER_STATE, - conversation_state=CONVERSATION_STATE, - credentials=MicrosoftAppCredentials( - app_id=APP.config["APP_ID"], password=APP.config["APP_PASSWORD"] - ), -) -ADAPTER.use(INSPECTION_MIDDLEWARE) - -# Create Bot -BOT = EchoBot(CONVERSATION_STATE, USER_STATE) - - -# Listen for incoming requests on /api/messages. -@APP.route("/api/messages", methods=["POST"]) -def messages(): - # Main bot message handler. - if "application/json" in request.headers["Content-Type"]: - body = request.json - else: - return Response(status=415) - - activity = Activity().deserialize(body) - auth_header = ( - request.headers["Authorization"] if "Authorization" in request.headers else "" - ) - - try: - task = LOOP.create_task( - ADAPTER.process_activity(activity, auth_header, BOT.on_turn) - ) - LOOP.run_until_complete(task) - return Response(status=201) - except Exception as exception: - raise exception - - -if __name__ == "__main__": - try: - APP.run(debug=False, port=APP.config["PORT"]) # nosec debug - except Exception as exception: - raise exception diff --git a/samples/47.inspection/bots/__init__.py b/samples/47.inspection/bots/__init__.py deleted file mode 100644 index f95fbbbad..000000000 --- a/samples/47.inspection/bots/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .echo_bot import EchoBot - -__all__ = ["EchoBot"] diff --git a/samples/47.inspection/bots/echo_bot.py b/samples/47.inspection/bots/echo_bot.py deleted file mode 100644 index 21a99aa9d..000000000 --- a/samples/47.inspection/bots/echo_bot.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from botbuilder.core import ( - ActivityHandler, - ConversationState, - TurnContext, - UserState, - MessageFactory, -) -from botbuilder.schema import ChannelAccount - -from data_models import CustomState - - -class EchoBot(ActivityHandler): - def __init__(self, conversation_state: ConversationState, user_state: UserState): - if conversation_state is None: - raise TypeError( - "[EchoBot]: Missing parameter. conversation_state is required but None was given" - ) - if user_state is None: - raise TypeError( - "[EchoBot]: Missing parameter. user_state is required but None was given" - ) - - self.conversation_state = conversation_state - self.user_state = user_state - - self.conversation_state_accessor = self.conversation_state.create_property( - "CustomState" - ) - self.user_state_accessor = self.user_state.create_property("CustomState") - - async def on_turn(self, turn_context: TurnContext): - await super().on_turn(turn_context) - - await self.conversation_state.save_changes(turn_context) - await self.user_state.save_changes(turn_context) - - async def on_members_added_activity( - self, members_added: [ChannelAccount], turn_context: TurnContext - ): - for member in members_added: - if member.id != turn_context.activity.recipient.id: - await turn_context.send_activity("Hello and welcome!") - - async def on_message_activity(self, turn_context: TurnContext): - # Get the state properties from the turn context. - user_data = await self.user_state_accessor.get(turn_context, CustomState) - conversation_data = await self.conversation_state_accessor.get( - turn_context, CustomState - ) - - await turn_context.send_activity( - MessageFactory.text( - f"Echo: {turn_context.activity.text}, " - f"conversation state: {conversation_data.value}, " - f"user state: {user_data.value}" - ) - ) - - user_data.value = user_data.value + 1 - conversation_data.value = conversation_data.value + 1 diff --git a/samples/47.inspection/config.py b/samples/47.inspection/config.py deleted file mode 100644 index e007d0fa9..000000000 --- a/samples/47.inspection/config.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - -""" Bot Configuration """ - - -class DefaultConfig: - """ Bot Configuration """ - - PORT = 3978 - APP_ID = os.environ.get("MicrosoftAppId", "") - APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") diff --git a/samples/47.inspection/data_models/__init__.py b/samples/47.inspection/data_models/__init__.py deleted file mode 100644 index f84d31d7b..000000000 --- a/samples/47.inspection/data_models/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .custom_state import CustomState - -__all__ = ["CustomState"] diff --git a/samples/47.inspection/data_models/custom_state.py b/samples/47.inspection/data_models/custom_state.py deleted file mode 100644 index 96a405cd4..000000000 --- a/samples/47.inspection/data_models/custom_state.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -class CustomState: - def __init__(self, value: int = 0): - self.value = value diff --git a/samples/47.inspection/requirements.txt b/samples/47.inspection/requirements.txt deleted file mode 100644 index 7e54b62ec..000000000 --- a/samples/47.inspection/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -botbuilder-core>=4.4.0b1 -flask>=1.0.3 diff --git a/samples/README.md b/samples/README.md deleted file mode 100644 index 72f1506a9..000000000 --- a/samples/README.md +++ /dev/null @@ -1,14 +0,0 @@ - -# Contributing - -This project welcomes contributions and suggestions. Most contributions require you to agree to a -Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit https://cla.microsoft.com. - -When you submit a pull request, a CLA-bot will automatically determine whether you need to provide -a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions -provided by the bot. You will only need to do this once across all repos using our CLA. - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/samples/python_django/13.core-bot/README-LUIS.md b/samples/python_django/13.core-bot/README-LUIS.md deleted file mode 100644 index b6b9b925f..000000000 --- a/samples/python_django/13.core-bot/README-LUIS.md +++ /dev/null @@ -1,216 +0,0 @@ -# Setting up LUIS via CLI: - -This README contains information on how to create and deploy a LUIS application. When the bot is ready to be deployed to production, we recommend creating a LUIS Endpoint Resource for usage with your LUIS App. - -> _For instructions on how to create a LUIS Application via the LUIS portal, see these Quickstart steps:_ -> 1. _[Quickstart: Create a new app in the LUIS portal][Quickstart-create]_ -> 2. _[Quickstart: Deploy an app in the LUIS portal][Quickstart-deploy]_ - - [Quickstart-create]: https://docs.microsoft.com/azure/cognitive-services/luis/get-started-portal-build-app - [Quickstart-deploy]:https://docs.microsoft.com/azure/cognitive-services/luis/get-started-portal-deploy-app - -## Table of Contents: - -- [Prerequisites](#Prerequisites) -- [Import a new LUIS Application using a local LUIS application](#Import-a-new-LUIS-Application-using-a-local-LUIS-application) -- [How to create a LUIS Endpoint resource in Azure and pair it with a LUIS Application](#How-to-create-a-LUIS-Endpoint-resource-in-Azure-and-pair-it-with-a-LUIS-Application) - -___ - -## [Prerequisites](#Table-of-Contents): - -#### Install Azure CLI >=2.0.61: - -Visit the following page to find the correct installer for your OS: -- https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest - -#### Install LUIS CLI >=2.4.0: - -Open a CLI of your choice and type the following: - -```bash -npm i -g luis-apis@^2.4.0 -``` - -#### LUIS portal account: - -You should already have a LUIS account with either https://luis.ai, https://eu.luis.ai, or https://au.luis.ai. To determine where to create a LUIS account, consider where you will deploy your LUIS applications, and then place them in [the corresponding region][LUIS-Authoring-Regions]. - -After you've created your account, you need your [Authoring Key][LUIS-AKey] and a LUIS application ID. - - [LUIS-Authoring-Regions]: https://docs.microsoft.com/azure/cognitive-services/luis/luis-reference-regions#luis-authoring-regions] - [LUIS-AKey]: https://docs.microsoft.com/azure/cognitive-services/luis/luis-concept-keys#authoring-key - -___ - -## [Import a new LUIS Application using a local LUIS application](#Table-of-Contents) - -### 1. Import the local LUIS application to luis.ai - -```bash -luis import application --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appName "FlightBooking" --in "./cognitiveModels/FlightBooking.json" -``` - -Outputs the following JSON: - -```json -{ - "id": "########-####-####-####-############", - "name": "FlightBooking", - "description": "A LUIS model that uses intent and entities.", - "culture": "en-us", - "usageScenario": "", - "domain": "", - "versionsCount": 1, - "createdDateTime": "2019-03-29T18:32:02Z", - "endpoints": {}, - "endpointHitsCount": 0, - "activeVersion": "0.1", - "ownerEmail": "bot@contoso.com", - "tokenizerVersion": "1.0.0" -} -``` - -For the next step, you'll need the `"id"` value for `--appId` and the `"activeVersion"` value for `--versionId`. - -### 2. Train the LUIS Application - -```bash -luis train version --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appId "LuisAppId" --versionId "LuisAppversion" --wait -``` - -### 3. Publish the LUIS Application - -```bash -luis publish version --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appId "LuisAppId" --versionId "LuisAppversion" --publishRegion "LuisAppPublishRegion" -``` - -> `--region` corresponds to the region you _author_ your application in. The regions available for this are "westus", "westeurope" and "australiaeast".
-> These regions correspond to the three available portals, https://luis.ai, https://eu.luis.ai, or https://au.luis.ai.
-> `--publishRegion` corresponds to the region of the endpoint you're publishing to, (e.g. "westus", "southeastasia", "westeurope", "brazilsouth").
-> See the [reference docs][Endpoint-API] for a list of available publish/endpoint regions. - - [Endpoint-API]: https://westus.dev.cognitive.microsoft.com/docs/services/5819c76f40a6350ce09de1ac/operations/5819c77140a63516d81aee78 - -Outputs the following: - -```json - { - "versionId": "0.1", - "isStaging": false, - "endpointUrl": "https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/########-####-####-####-############", - "region": "westus", - "assignedEndpointKey": null, - "endpointRegion": "westus", - "failedRegions": "", - "publishedDateTime": "2019-03-29T18:40:32Z", - "directVersionPublish": false -} -``` - -To see how to create an LUIS Cognitive Service Resource in Azure, please see [the next README][README-LUIS]. This Resource should be used when you want to move your bot to production. The instructions will show you how to create and pair the resource with a LUIS Application. - - [README-LUIS]: ./README-LUIS.md - -___ - -## [How to create a LUIS Endpoint resource in Azure and pair it with a LUIS Application](#Table-of-Contents) - -### 1. Create a new LUIS Cognitive Services resource on Azure via Azure CLI - -> _Note:_
-> _If you don't have a Resource Group in your Azure subscription, you can create one through the Azure portal or through using:_ -> ```bash -> az group create --subscription "AzureSubscriptionGuid" --location "westus" --name "ResourceGroupName" -> ``` -> _To see a list of valid locations, use `az account list-locations`_ - - -```bash -# Use Azure CLI to create the LUIS Key resource on Azure -az cognitiveservices account create --kind "luis" --name "NewLuisResourceName" --sku "S0" --location "westus" --subscription "AzureSubscriptionGuid" -g "ResourceGroupName" -``` - -The command will output a response similar to the JSON below: - -```json -{ - "endpoint": "https://westus.api.cognitive.microsoft.com/luis/v2.0", - "etag": "\"########-####-####-####-############\"", - "id": "/subscriptions/########-####-####-####-############/resourceGroups/ResourceGroupName/providers/Microsoft.CognitiveServices/accounts/NewLuisResourceName", - "internalId": "################################", - "kind": "luis", - "location": "westus", - "name": "NewLuisResourceName", - "provisioningState": "Succeeded", - "resourceGroup": "ResourceGroupName", - "sku": { - "name": "S0", - "tier": null - }, - "tags": null, - "type": "Microsoft.CognitiveServices/accounts" -} -``` - - - -Take the output from the previous command and create a JSON file in the following format: - -```json -{ - "azureSubscriptionId": "00000000-0000-0000-0000-000000000000", - "resourceGroup": "ResourceGroupName", - "accountName": "NewLuisResourceName" -} -``` - -### 2. Retrieve ARM access token via Azure CLI - -```bash -az account get-access-token --subscription "AzureSubscriptionGuid" -``` - -This will return an object that looks like this: - -```json -{ - "accessToken": "eyJ0eXAiOiJKVtokentokentokentokentokeng1dCI6Ik4tbEMwbi05REFMcXdodUhZbkhRNjNHZUNYYyIsItokenI6Ik4tbEMwbi05REFMcXdodUhZbkhRNjNHZUNYYyJ9.eyJhdWQiOiJodHRwczovL21hbmFnZW1lbnQuY29yZS53aW5kb3dzLm5ldC8iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNTUzODc3MTUwLCJuYmYiOjE1NTM4NzcxNTAsImV4cCI6MTU1Mzg4MTA1MCwiX2NsYWltX25hbWVzIjp7Imdyb3VwcyI6InNyYzEifSwiX2NsYWltX3NvdXJjZXMiOnsic3JjMSI6eyJlbmRwb2ludCI6Imh0dHBzOi8vZ3JhcGgud2luZG93cy5uZXQvNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3L3VzZXJzL2ZmZTQyM2RkLWJhM2YtNDg0Ny04NjgyLWExNTI5MDA4MjM4Ny9nZXRNZW1iZXJPYmplY3RzIn19LCJhY3IiOiIxIiwiYWlvIjoiQVZRQXEvOEtBQUFBeGVUc201NDlhVHg4RE1mMFlRVnhGZmxxOE9RSC9PODR3QktuSmRqV1FqTkkwbmxLYzB0bHJEZzMyMFZ5bWZGaVVBSFBvNUFFUTNHL0FZNDRjdk01T3M0SEt0OVJkcE5JZW9WU0dzd0kvSkk9IiwiYW1yIjpbIndpYSIsIm1mYSJdLCJhcHBpZCI6IjA0YjA3Nzk1LThkZGItNDYxYS1iYmVlLTAyZjllMWJmN2I0NiIsImFwcGlkYWNyIjoiMCIsImRldmljZWlkIjoiNDhmNDVjNjEtMTg3Zi00MjUxLTlmZWItMTllZGFkZmMwMmE3IiwiZmFtaWx5X25hbWUiOiJHdW0iLCJnaXZlbl9uYW1lIjoiU3RldmVuIiwiaXBhZGRyIjoiMTY3LjIyMC4yLjU1IiwibmFtZSI6IlN0ZXZlbiBHdW0iLCJvaWQiOiJmZmU0MjNkZC1iYTNmLTQ4NDctODY4Mi1hMTUyOTAwODIzODciLCJvbnByZW1fc2lkIjoiUy0xLTUtMjEtMjEyNzUyMTE4NC0xNjA0MDEyOTIwLTE4ODc5Mjc1MjctMjYwOTgyODUiLCJwdWlkIjoiMTAwMzdGRkVBMDQ4NjlBNyIsInJoIjoiSSIsInNjcCI6InVzZXJfaW1wZXJzb25hdGlvbiIsInN1YiI6Ik1rMGRNMWszN0U5ckJyMjhieUhZYjZLSU85LXVFQVVkZFVhNWpkSUd1Nk0iLCJ0aWQiOiI3MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDciLCJ1bmlxdWVfbmFtZSI6InN0Z3VtQG1pY3Jvc29mdC5jb20iLCJ1cG4iOiJzdGd1bUBtaWNyb3NvZnQuY29tIiwidXRpIjoiT2w2NGN0TXY4RVNEQzZZQWRqRUFtokenInZlciI6IjEuMCJ9.kFAsEilE0mlS1pcpqxf4rEnRKeYsehyk-gz-zJHUrE__oad3QjgDSBDPrR_ikLdweynxbj86pgG4QFaHURNCeE6SzrbaIrNKw-n9jrEtokenlosOxg_0l2g1LeEUOi5Q4gQREAU_zvSbl-RY6sAadpOgNHtGvz3Rc6FZRITfkckSLmsKAOFoh-aWC6tFKG8P52rtB0qVVRz9tovBeNqkMYL49s9ypduygbXNVwSQhm5JszeWDgrFuVFHBUP_iENCQYGQpEZf_KvjmX1Ur1F9Eh9nb4yI2gFlKncKNsQl-tokenK7-tokentokentokentokentokentokenatoken", - "expiresOn": "2200-12-31 23:59:59.999999", - "subscription": "AzureSubscriptionGuid", - "tenant": "tenant-guid", - "tokenType": "Bearer" -} -``` - -The value needed for the next step is the `"accessToken"`. - -### 3. Use `luis add appazureaccount` to pair your LUIS resource with a LUIS Application - -```bash -luis add appazureaccount --in "path/to/created/requestBody.json" --appId "LuisAppId" --authoringKey "LuisAuthoringKey" --armToken "accessToken" -``` - -If successful, it should yield a response like this: - -```json -{ - "code": "Success", - "message": "Operation Successful" -} -``` - -### 4. See the LUIS Cognitive Services' keys - -```bash -az cognitiveservices account keys list --name "NewLuisResourceName" --subscription "AzureSubscriptionGuid" -g "ResourceGroupName" -``` - -This will return an object that looks like this: - -```json -{ - "key1": "9a69####dc8f####8eb4####399f####", - "key2": "####f99e####4b1a####fb3b####6b9f" -} -``` \ No newline at end of file diff --git a/samples/python_django/13.core-bot/README.md b/samples/python_django/13.core-bot/README.md deleted file mode 100644 index 1724d8d04..000000000 --- a/samples/python_django/13.core-bot/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# CoreBot - -Bot Framework v4 core bot sample. - -This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to: - -- Use [LUIS](https://www.luis.ai) to implement core AI capabilities -- Implement a multi-turn conversation using Dialogs -- Handle user interruptions for such things as `Help` or `Cancel` -- Prompt for and validate requests for information from the user - -## Prerequisites - -This sample **requires** prerequisites in order to run. - -### Overview - -This bot uses [LUIS](https://www.luis.ai), an AI based cognitive service, to implement language understanding. - -### Install Python 3.6 - - -### Create a LUIS Application to enable language understanding - -LUIS language model setup, training, and application configuration steps can be found [here](https://docs.microsoft.com/azure/bot-service/bot-builder-howto-v4-luis?view=azure-bot-service-4.0&tabs=cs). - -If you wish to create a LUIS application via the CLI, these steps can be found in the [README-LUIS.md](README-LUIS.md). - - -### Configure your bot to use your LUIS app - -Update config.py with your newly imported LUIS app id, LUIS API key from https:///user/settings, LUIS API host name, ie .api.cognitive.microsoft.com. LUIS authoring region is listed on https:///user/settings. - - -## Testing the bot using Bot Framework Emulator - -[Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. - -- Install the Bot Framework Emulator version 4.3.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) - -### Connect to the bot using Bot Framework Emulator - -- Launch Bot Framework Emulator -- File -> Open Bot -- Enter a Bot URL of `http://localhost:3978/api/messages` - - -## Further reading - -- [Bot Framework Documentation](https://docs.botframework.com) -- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) -- [Dialogs](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0) -- [Gathering Input Using Prompts](https://docs.microsoft.com/azure/bot-service/bot-builder-prompts?view=azure-bot-service-4.0&tabs=csharp) -- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) -- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) -- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) -- [.NET Core CLI tools](https://docs.microsoft.com/dotnet/core/tools/?tabs=netcore2x) -- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest) -- [Azure Portal](https://portal.azure.com) -- [Language Understanding using LUIS](https://docs.microsoft.com/azure/cognitive-services/luis/) -- [Channels and Bot Connector Service](https://docs.microsoft.com/azure/bot-service/bot-concepts?view=azure-bot-service-4.0) \ No newline at end of file diff --git a/samples/python_django/13.core-bot/booking_details.py b/samples/python_django/13.core-bot/booking_details.py deleted file mode 100644 index 4502ee974..000000000 --- a/samples/python_django/13.core-bot/booking_details.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -"""Booking detail.""" - - -class BookingDetails: - """Booking detail implementation""" - - def __init__( - self, destination: str = None, origin: str = None, travel_date: str = None - ): - self.destination = destination - self.origin = origin - self.travel_date = travel_date diff --git a/samples/python_django/13.core-bot/bots/__init__.py b/samples/python_django/13.core-bot/bots/__init__.py deleted file mode 100644 index ee478912d..000000000 --- a/samples/python_django/13.core-bot/bots/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""bots module.""" - -from .dialog_bot import DialogBot -from .dialog_and_welcome_bot import DialogAndWelcomeBot - -__all__ = ["DialogBot", "DialogAndWelcomeBot"] diff --git a/samples/python_django/13.core-bot/bots/bots.py b/samples/python_django/13.core-bot/bots/bots.py deleted file mode 100644 index a1d783449..000000000 --- a/samples/python_django/13.core-bot/bots/bots.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -""" Bot initialization """ -# pylint: disable=line-too-long -import sys -import asyncio -from django.apps import AppConfig -from botbuilder.core import ( - BotFrameworkAdapter, - BotFrameworkAdapterSettings, - TurnContext, - ConversationState, - MemoryStorage, - UserState, -) -from dialogs import MainDialog -from bots import DialogAndWelcomeBot -import config - - -class BotConfig(AppConfig): - """ Bot initialization """ - - name = "bots" - appConfig = config.DefaultConfig - - SETTINGS = BotFrameworkAdapterSettings(appConfig.APP_ID, appConfig.APP_PASSWORD) - ADAPTER = BotFrameworkAdapter(SETTINGS) - LOOP = asyncio.get_event_loop() - - # Create MemoryStorage, UserState and ConversationState - memory = MemoryStorage() - user_state = UserState(memory) - conversation_state = ConversationState(memory) - - dialog = MainDialog(appConfig) - bot = DialogAndWelcomeBot(conversation_state, user_state, dialog) - - async def on_error(self, context: TurnContext, error: Exception): - """ - Catch-all for errors. - This check writes out errors to console log - NOTE: In production environment, you should consider logging this to Azure - application insights. - """ - print(f"\n [on_turn_error]: { error }", file=sys.stderr) - # Send a message to the user - await context.send_activity("Oops. Something went wrong!") - # Clear out state - await self.conversation_state.delete(context) - - def ready(self): - self.ADAPTER.on_turn_error = self.on_error diff --git a/samples/python_django/13.core-bot/bots/dialog_and_welcome_bot.py b/samples/python_django/13.core-bot/bots/dialog_and_welcome_bot.py deleted file mode 100644 index fe030d056..000000000 --- a/samples/python_django/13.core-bot/bots/dialog_and_welcome_bot.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Main dialog to welcome users.""" -import json -import os.path -from typing import List -from botbuilder.core import TurnContext -from botbuilder.schema import Activity, Attachment, ChannelAccount -from helpers.activity_helper import create_activity_reply -from .dialog_bot import DialogBot - - -class DialogAndWelcomeBot(DialogBot): - """Main dialog to welcome users implementation.""" - - async def on_members_added_activity( - self, members_added: List[ChannelAccount], turn_context: TurnContext - ): - for member in members_added: - # Greet anyone that was not the target (recipient) of this message. - # To learn more about Adaptive Cards, see https://aka.ms/msbot-adaptivecards - # for more details. - if member.id != turn_context.activity.recipient.id: - welcome_card = self.create_adaptive_card_attachment() - response = self.create_response(turn_context.activity, welcome_card) - await turn_context.send_activity(response) - - def create_response(self, activity: Activity, attachment: Attachment): - """Create an attachment message response.""" - response = create_activity_reply(activity) - response.attachments = [attachment] - return response - - # Load attachment from file. - def create_adaptive_card_attachment(self): - """Create an adaptive card.""" - relative_path = os.path.abspath(os.path.dirname(__file__)) - path = os.path.join(relative_path, "resources/welcomeCard.json") - with open(path) as card_file: - card = json.load(card_file) - - return Attachment( - content_type="application/vnd.microsoft.card.adaptive", content=card - ) diff --git a/samples/python_django/13.core-bot/bots/dialog_bot.py b/samples/python_django/13.core-bot/bots/dialog_bot.py deleted file mode 100644 index f8b221e87..000000000 --- a/samples/python_django/13.core-bot/bots/dialog_bot.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Implements bot Activity handler.""" - -from botbuilder.core import ActivityHandler, ConversationState, UserState, TurnContext -from botbuilder.dialogs import Dialog -from helpers.dialog_helper import DialogHelper - - -class DialogBot(ActivityHandler): - """Main activity handler for the bot.""" - - def __init__( - self, - conversation_state: ConversationState, - user_state: UserState, - dialog: Dialog, - ): - if conversation_state is None: - raise Exception( - "[DialogBot]: Missing parameter. conversation_state is required" - ) - if user_state is None: - raise Exception("[DialogBot]: Missing parameter. user_state is required") - if dialog is None: - raise Exception("[DialogBot]: Missing parameter. dialog is required") - - self.conversation_state = conversation_state - self.user_state = user_state - self.dialog = dialog - self.dialogState = self.conversation_state.create_property( - "DialogState" - ) # pylint: disable=C0103 - - async def on_turn(self, turn_context: TurnContext): - await super().on_turn(turn_context) - - # Save any state changes that might have occured during the turn. - await self.conversation_state.save_changes(turn_context, False) - await self.user_state.save_changes(turn_context, False) - - async def on_message_activity(self, turn_context: TurnContext): - await DialogHelper.run_dialog( - self.dialog, - turn_context, - self.conversation_state.create_property("DialogState"), - ) # pylint: disable=C0103 diff --git a/samples/python_django/13.core-bot/bots/resources/welcomeCard.json b/samples/python_django/13.core-bot/bots/resources/welcomeCard.json deleted file mode 100644 index 100aa5287..000000000 --- a/samples/python_django/13.core-bot/bots/resources/welcomeCard.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", - "type": "AdaptiveCard", - "version": "1.0", - "body": [ - { - "type": "Image", - "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU", - "size": "stretch" - }, - { - "type": "TextBlock", - "spacing": "medium", - "size": "default", - "weight": "bolder", - "text": "Welcome to Bot Framework!", - "wrap": true, - "maxLines": 0 - }, - { - "type": "TextBlock", - "size": "default", - "isSubtle": "true", - "text": "Now that you have successfully run your bot, follow the links in this Adaptive Card to expand your knowledge of Bot Framework.", - "wrap": true, - "maxLines": 0 - } - ], - "actions": [ - { - "type": "Action.OpenUrl", - "title": "Get an overview", - "url": "https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0" - }, - { - "type": "Action.OpenUrl", - "title": "Ask a question", - "url": "https://stackoverflow.com/questions/tagged/botframework" - }, - { - "type": "Action.OpenUrl", - "title": "Learn how to deploy", - "url": "https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-deploy-azure?view=azure-bot-service-4.0" - } - ] -} \ No newline at end of file diff --git a/samples/python_django/13.core-bot/bots/settings.py b/samples/python_django/13.core-bot/bots/settings.py deleted file mode 100644 index 99fd265c7..000000000 --- a/samples/python_django/13.core-bot/bots/settings.py +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -""" -Django settings for bots project. - -Generated by 'django-admin startproject' using Django 2.2.1. - -For more information on this file, see -https://docs.djangoproject.com/en/2.2/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/2.2/ref/settings/ -""" - -import os - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = "My Secret Key" - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = [] - - -# Application definition - -INSTALLED_APPS = [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", - "bots.bots.BotConfig", -] - -MIDDLEWARE = [ - "django.middleware.security.SecurityMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", -] - -ROOT_URLCONF = "bots.urls" - -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - ] - }, - } -] - -WSGI_APPLICATION = "bots.wsgi.application" - - -# Database -# https://docs.djangoproject.com/en/2.2/ref/settings/#databases - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": os.path.join(BASE_DIR, "db.sqlite3"), - } -} - - -# Password validation -# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" - }, - {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, - {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, - {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, -] - - -# Internationalization -# https://docs.djangoproject.com/en/2.2/topics/i18n/ - -LANGUAGE_CODE = "en-us" - -TIME_ZONE = "UTC" - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/2.2/howto/static-files/ - -STATIC_URL = "/static/" diff --git a/samples/python_django/13.core-bot/bots/urls.py b/samples/python_django/13.core-bot/bots/urls.py deleted file mode 100644 index 99cf42018..000000000 --- a/samples/python_django/13.core-bot/bots/urls.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -""" URL configuration for bot message handler """ - -from django.urls import path -from django.views.decorators.csrf import csrf_exempt -from . import views - -# pylint:disable=invalid-name -urlpatterns = [ - path("", views.home, name="home"), - path("api/messages", csrf_exempt(views.messages), name="messages"), -] diff --git a/samples/python_django/13.core-bot/bots/views.py b/samples/python_django/13.core-bot/bots/views.py deleted file mode 100644 index 04f354424..000000000 --- a/samples/python_django/13.core-bot/bots/views.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -""" -This sample shows how to create a bot that demonstrates the following: -- Use [LUIS](https://www.luis.ai) to implement core AI capabilities. -- Implement a multi-turn conversation using Dialogs. -- Handle user interruptions for such things as `Help` or `Cancel`. -- Prompt for and validate requests for information from the user. -""" - -import asyncio -import json -from django.http import HttpResponse -from django.apps import apps -from botbuilder.schema import Activity - -# pylint: disable=line-too-long -def home(): - """Default handler.""" - return HttpResponse("Hello!") - - -def messages(request): - """Main bot message handler.""" - if "application/json" in request.headers["Content-Type"]: - body = json.loads(request.body.decode("utf-8")) - else: - return HttpResponse(status=415) - - activity = Activity().deserialize(body) - auth_header = ( - request.headers["Authorization"] if "Authorization" in request.headers else "" - ) - - bot_app = apps.get_app_config("bots") - bot = bot_app.bot - loop = bot_app.LOOP - adapter = bot_app.ADAPTER - - async def aux_func(turn_context): - await bot.on_turn(turn_context) - - try: - task = asyncio.ensure_future( - adapter.process_activity(activity, auth_header, aux_func), loop=loop - ) - loop.run_until_complete(task) - return HttpResponse(status=201) - except Exception as exception: - raise exception - return HttpResponse("This is message processing!") diff --git a/samples/python_django/13.core-bot/bots/wsgi.py b/samples/python_django/13.core-bot/bots/wsgi.py deleted file mode 100644 index 869e12e78..000000000 --- a/samples/python_django/13.core-bot/bots/wsgi.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -""" -WSGI config for bots project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ -""" - -import os -from django.core.wsgi import get_wsgi_application - -# pylint:disable=invalid-name -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bots.settings") -application = get_wsgi_application() diff --git a/samples/python_django/13.core-bot/cognitiveModels/FlightBooking.json b/samples/python_django/13.core-bot/cognitiveModels/FlightBooking.json deleted file mode 100644 index 0a0d6c4a7..000000000 --- a/samples/python_django/13.core-bot/cognitiveModels/FlightBooking.json +++ /dev/null @@ -1,226 +0,0 @@ -{ - "luis_schema_version": "3.2.0", - "versionId": "0.1", - "name": "Airline Reservation", - "desc": "A LUIS model that uses intent and entities.", - "culture": "en-us", - "tokenizerVersion": "1.0.0", - "intents": [ - { - "name": "Book flight" - }, - { - "name": "Cancel" - }, - { - "name": "None" - } - ], - "entities": [], - "composites": [ - { - "name": "From", - "children": [ - "Airport" - ], - "roles": [] - }, - { - "name": "To", - "children": [ - "Airport" - ], - "roles": [] - } - ], - "closedLists": [ - { - "name": "Airport", - "subLists": [ - { - "canonicalForm": "Paris", - "list": [ - "paris" - ] - }, - { - "canonicalForm": "London", - "list": [ - "london" - ] - }, - { - "canonicalForm": "Berlin", - "list": [ - "berlin" - ] - }, - { - "canonicalForm": "New York", - "list": [ - "new york" - ] - } - ], - "roles": [] - } - ], - "patternAnyEntities": [], - "regex_entities": [], - "prebuiltEntities": [ - { - "name": "datetimeV2", - "roles": [] - } - ], - "model_features": [], - "regex_features": [], - "patterns": [], - "utterances": [ - { - "text": "book flight from london to paris on feb 14th", - "intent": "Book flight", - "entities": [ - { - "entity": "To", - "startPos": 27, - "endPos": 31 - }, - { - "entity": "From", - "startPos": 17, - "endPos": 22 - } - ] - }, - { - "text": "book flight to berlin on feb 14th", - "intent": "Book flight", - "entities": [ - { - "entity": "To", - "startPos": 15, - "endPos": 20 - } - ] - }, - { - "text": "book me a flight from london to paris", - "intent": "Book flight", - "entities": [ - { - "entity": "From", - "startPos": 22, - "endPos": 27 - }, - { - "entity": "To", - "startPos": 32, - "endPos": 36 - } - ] - }, - { - "text": "bye", - "intent": "Cancel", - "entities": [] - }, - { - "text": "cancel booking", - "intent": "Cancel", - "entities": [] - }, - { - "text": "exit", - "intent": "Cancel", - "entities": [] - }, - { - "text": "flight to paris", - "intent": "Book flight", - "entities": [ - { - "entity": "To", - "startPos": 10, - "endPos": 14 - } - ] - }, - { - "text": "flight to paris from london on feb 14th", - "intent": "Book flight", - "entities": [ - { - "entity": "To", - "startPos": 10, - "endPos": 14 - }, - { - "entity": "From", - "startPos": 21, - "endPos": 26 - } - ] - }, - { - "text": "fly from berlin to paris on may 5th", - "intent": "Book flight", - "entities": [ - { - "entity": "To", - "startPos": 19, - "endPos": 23 - }, - { - "entity": "From", - "startPos": 9, - "endPos": 14 - } - ] - }, - { - "text": "go to paris", - "intent": "Book flight", - "entities": [ - { - "entity": "To", - "startPos": 6, - "endPos": 10 - } - ] - }, - { - "text": "going from paris to berlin", - "intent": "Book flight", - "entities": [ - { - "entity": "To", - "startPos": 20, - "endPos": 25 - }, - { - "entity": "From", - "startPos": 11, - "endPos": 15 - } - ] - }, - { - "text": "ignore", - "intent": "Cancel", - "entities": [] - }, - { - "text": "travel to paris", - "intent": "Book flight", - "entities": [ - { - "entity": "To", - "startPos": 10, - "endPos": 14 - } - ] - } - ], - "settings": [] -} \ No newline at end of file diff --git a/samples/python_django/13.core-bot/config.py b/samples/python_django/13.core-bot/config.py deleted file mode 100644 index c2dbd7827..000000000 --- a/samples/python_django/13.core-bot/config.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -""" Bot Configuration """ - - -class DefaultConfig(object): - """ Bot Configuration """ - - PORT = 3978 - APP_ID = "" - APP_PASSWORD = "" - - LUIS_APP_ID = "" - # LUIS authoring key from LUIS portal or LUIS Cognitive Service subscription key - LUIS_API_KEY = "" - # LUIS endpoint host name, ie "https://westus.api.cognitive.microsoft.com" - LUIS_API_HOST_NAME = "" diff --git a/samples/python_django/13.core-bot/db.sqlite3 b/samples/python_django/13.core-bot/db.sqlite3 deleted file mode 100644 index e69de29bb..000000000 diff --git a/samples/python_django/13.core-bot/dialogs/__init__.py b/samples/python_django/13.core-bot/dialogs/__init__.py deleted file mode 100644 index 88d9489fd..000000000 --- a/samples/python_django/13.core-bot/dialogs/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Dialogs module""" -from .booking_dialog import BookingDialog -from .cancel_and_help_dialog import CancelAndHelpDialog -from .date_resolver_dialog import DateResolverDialog -from .main_dialog import MainDialog - -__all__ = ["BookingDialog", "CancelAndHelpDialog", "DateResolverDialog", "MainDialog"] diff --git a/samples/python_django/13.core-bot/dialogs/booking_dialog.py b/samples/python_django/13.core-bot/dialogs/booking_dialog.py deleted file mode 100644 index 8b345fd7c..000000000 --- a/samples/python_django/13.core-bot/dialogs/booking_dialog.py +++ /dev/null @@ -1,119 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Flight booking dialog.""" - -from botbuilder.dialogs import WaterfallDialog, WaterfallStepContext, DialogTurnResult -from botbuilder.dialogs.prompts import ConfirmPrompt, TextPrompt, PromptOptions -from botbuilder.core import MessageFactory -from datatypes_date_time.timex import Timex -from .cancel_and_help_dialog import CancelAndHelpDialog -from .date_resolver_dialog import DateResolverDialog - - -class BookingDialog(CancelAndHelpDialog): - """Flight booking implementation.""" - - def __init__(self, dialog_id: str = None): - super(BookingDialog, self).__init__(dialog_id or BookingDialog.__name__) - - self.add_dialog(TextPrompt(TextPrompt.__name__)) - # self.add_dialog(ConfirmPrompt(ConfirmPrompt.__name__)) - self.add_dialog(DateResolverDialog(DateResolverDialog.__name__)) - self.add_dialog( - WaterfallDialog( - WaterfallDialog.__name__, - [ - self.destination_step, - self.origin_step, - self.travel_date_step, - # self.confirm_step, - self.final_step, - ], - ) - ) - - self.initial_dialog_id = WaterfallDialog.__name__ - - async def destination_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - """Prompt for destination.""" - booking_details = step_context.options - - if booking_details.destination is None: - return await step_context.prompt( - TextPrompt.__name__, - PromptOptions( - prompt=MessageFactory.text("To what city would you like to travel?") - ), - ) # pylint: disable=line-too-long,bad-continuation - else: - return await step_context.next(booking_details.destination) - - async def origin_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - """Prompt for origin city.""" - booking_details = step_context.options - - # Capture the response to the previous step's prompt - booking_details.destination = step_context.result - if booking_details.origin is None: - return await step_context.prompt( - TextPrompt.__name__, - PromptOptions( - prompt=MessageFactory.text("From what city will you be travelling?") - ), - ) # pylint: disable=line-too-long,bad-continuation - else: - return await step_context.next(booking_details.origin) - - async def travel_date_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - """Prompt for travel date. - This will use the DATE_RESOLVER_DIALOG.""" - - booking_details = step_context.options - - # Capture the results of the previous step - booking_details.origin = step_context.result - if not booking_details.travel_date or self.is_ambiguous( - booking_details.travel_date - ): - return await step_context.begin_dialog( - DateResolverDialog.__name__, booking_details.travel_date - ) # pylint: disable=line-too-long - else: - return await step_context.next(booking_details.travel_date) - - async def confirm_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - """Confirm the information the user has provided.""" - booking_details = step_context.options - - # Capture the results of the previous step - booking_details.travel_date = step_context.result - msg = ( - f"Please confirm, I have you traveling to: { booking_details.destination }" - f" from: { booking_details.origin } on: { booking_details.travel_date}." - ) - - # Offer a YES/NO prompt. - return await step_context.prompt( - ConfirmPrompt.__name__, PromptOptions(prompt=MessageFactory.text(msg)) - ) - - async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - """Complete the interaction and end the dialog.""" - if step_context.result: - booking_details = step_context.options - booking_details.travel_date = step_context.result - - return await step_context.end_dialog(booking_details) - else: - return await step_context.end_dialog() - - def is_ambiguous(self, timex: str) -> bool: - """Ensure time is correct.""" - timex_property = Timex(timex) - return "definite" not in timex_property.types diff --git a/samples/python_django/13.core-bot/dialogs/cancel_and_help_dialog.py b/samples/python_django/13.core-bot/dialogs/cancel_and_help_dialog.py deleted file mode 100644 index 35cb15ec2..000000000 --- a/samples/python_django/13.core-bot/dialogs/cancel_and_help_dialog.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Handle cancel and help intents.""" -from botbuilder.dialogs import ( - ComponentDialog, - DialogContext, - DialogTurnResult, - DialogTurnStatus, -) -from botbuilder.schema import ActivityTypes - - -class CancelAndHelpDialog(ComponentDialog): - """Implementation of handling cancel and help.""" - - async def on_begin_dialog( - self, inner_dc: DialogContext, options: object - ) -> DialogTurnResult: - result = await self.interrupt(inner_dc) - if result is not None: - return result - - return await super(CancelAndHelpDialog, self).on_begin_dialog(inner_dc, options) - - async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult: - result = await self.interrupt(inner_dc) - if result is not None: - return result - - return await super(CancelAndHelpDialog, self).on_continue_dialog(inner_dc) - - async def interrupt(self, inner_dc: DialogContext) -> DialogTurnResult: - """Detect interruptions.""" - if inner_dc.context.activity.type == ActivityTypes.message: - text = inner_dc.context.activity.text.lower() - - if text == "help" or text == "?": - await inner_dc.context.send_activity("Show Help...") - return DialogTurnResult(DialogTurnStatus.Waiting) - - if text == "cancel" or text == "quit": - await inner_dc.context.send_activity("Cancelling") - return await inner_dc.cancel_all_dialogs() - - return None diff --git a/samples/python_django/13.core-bot/dialogs/date_resolver_dialog.py b/samples/python_django/13.core-bot/dialogs/date_resolver_dialog.py deleted file mode 100644 index 6dc683c91..000000000 --- a/samples/python_django/13.core-bot/dialogs/date_resolver_dialog.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Handle date/time resolution for booking dialog.""" -from botbuilder.core import MessageFactory -from botbuilder.dialogs import WaterfallDialog, DialogTurnResult, WaterfallStepContext -from botbuilder.dialogs.prompts import ( - DateTimePrompt, - PromptValidatorContext, - PromptOptions, - DateTimeResolution, -) -from datatypes_date_time.timex import Timex -from .cancel_and_help_dialog import CancelAndHelpDialog - - -class DateResolverDialog(CancelAndHelpDialog): - """Resolve the date""" - - def __init__(self, dialog_id: str = None): - super(DateResolverDialog, self).__init__( - dialog_id or DateResolverDialog.__name__ - ) - - self.add_dialog( - DateTimePrompt( - DateTimePrompt.__name__, DateResolverDialog.datetime_prompt_validator - ) - ) - self.add_dialog( - WaterfallDialog( - WaterfallDialog.__name__ + "2", [self.initial_step, self.final_step] - ) - ) - - self.initial_dialog_id = WaterfallDialog.__name__ + "2" - - async def initial_step( - self, step_context: WaterfallStepContext - ) -> DialogTurnResult: - """Prompt for the date.""" - timex = step_context.options - - prompt_msg = "On what date would you like to travel?" - reprompt_msg = ( - "I'm sorry, for best results, please enter your travel " - "date including the month, day and year." - ) - - if timex is None: - # We were not given any date at all so prompt the user. - return await step_context.prompt( - DateTimePrompt.__name__, - PromptOptions( # pylint: disable=bad-continuation - prompt=MessageFactory.text(prompt_msg), - retry_prompt=MessageFactory.text(reprompt_msg), - ), - ) - else: - # We have a Date we just need to check it is unambiguous. - if "definite" in Timex(timex).types: - # This is essentially a "reprompt" of the data we were given up front. - return await step_context.prompt( - DateTimePrompt.__name__, PromptOptions(prompt=reprompt_msg) - ) - else: - return await step_context.next(DateTimeResolution(timex=timex)) - - async def final_step(self, step_context: WaterfallStepContext): - """Cleanup - set final return value and end dialog.""" - timex = step_context.result[0].timex - return await step_context.end_dialog(timex) - - @staticmethod - async def datetime_prompt_validator(prompt_context: PromptValidatorContext) -> bool: - """ Validate the date provided is in proper form. """ - if prompt_context.recognized.succeeded: - timex = prompt_context.recognized.value[0].timex.split("T")[0] - - # TODO: Needs TimexProperty - return "definite" in Timex(timex).types - - return False diff --git a/samples/python_django/13.core-bot/dialogs/main_dialog.py b/samples/python_django/13.core-bot/dialogs/main_dialog.py deleted file mode 100644 index e92fe58a4..000000000 --- a/samples/python_django/13.core-bot/dialogs/main_dialog.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Main dialog. """ -from botbuilder.dialogs import ( - ComponentDialog, - WaterfallDialog, - WaterfallStepContext, - DialogTurnResult, -) -from botbuilder.dialogs.prompts import TextPrompt, PromptOptions -from botbuilder.core import MessageFactory -from booking_details import BookingDetails -from helpers.luis_helper import LuisHelper -from .booking_dialog import BookingDialog - - -class MainDialog(ComponentDialog): - """Main dialog. """ - - def __init__(self, configuration: dict, dialog_id: str = None): - super(MainDialog, self).__init__(dialog_id or MainDialog.__name__) - - self._configuration = configuration - - self.add_dialog(TextPrompt(TextPrompt.__name__)) - self.add_dialog(BookingDialog()) - self.add_dialog( - WaterfallDialog( - "WFDialog", [self.intro_step, self.act_step, self.final_step] - ) - ) - - self.initial_dialog_id = "WFDialog" - - async def intro_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - """Initial prompt.""" - return await step_context.prompt( - TextPrompt.__name__, - PromptOptions( - prompt=MessageFactory.text("What can I help you with today?") - ), - ) - - async def act_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - """Use language understanding to gather details about booking.""" - - # In this sample we only have a single Intent we are concerned with. - # However, typically a scenario will have multiple different Intents - # each corresponding to starting a different child Dialog. - booking_details = ( - await LuisHelper.execute_luis_query( - self._configuration, step_context.context - ) - if step_context.result is not None - else BookingDetails() - ) - - # Run the BookingDialog giving it whatever details we have from the - # model. The dialog will prompt to find out the remaining details. - return await step_context.begin_dialog(BookingDialog.__name__, booking_details) - - async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - """Complete dialog. - At this step, with details from the user, display the completed - flight booking to the user. - """ - # If the child dialog ("BookingDialog") was cancelled or the user failed - # to confirm, the Result here will be null. - if step_context.result is not None: - result = step_context.result - - # Now we have all the booking details call the booking service. - # If the call to the booking service was successful tell the user. - # time_property = Timex(result.travel_date) - # travel_date_msg = time_property.to_natural_language(datetime.now()) - msg = ( - f"I have you booked to {result.destination} from" - f" {result.origin} on {result.travel_date}." - ) - await step_context.context.send_activity(MessageFactory.text(msg)) - else: - await step_context.context.send_activity(MessageFactory.text("Thank you.")) - return await step_context.end_dialog() diff --git a/samples/python_django/13.core-bot/helpers/__init__.py b/samples/python_django/13.core-bot/helpers/__init__.py deleted file mode 100644 index 1ef1e54a6..000000000 --- a/samples/python_django/13.core-bot/helpers/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -"""Helpers module.""" -from . import activity_helper, luis_helper, dialog_helper - -__all__ = ["activity_helper", "dialog_helper", "luis_helper"] diff --git a/samples/python_django/13.core-bot/helpers/activity_helper.py b/samples/python_django/13.core-bot/helpers/activity_helper.py deleted file mode 100644 index 78353902e..000000000 --- a/samples/python_django/13.core-bot/helpers/activity_helper.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Helper to create reply object.""" - -from datetime import datetime -from botbuilder.schema import ( - Activity, - ActivityTypes, - ChannelAccount, - ConversationAccount, -) - - -def create_activity_reply(activity: Activity, text: str = None, locale: str = None): - """Helper to create reply object.""" - return Activity( - type=ActivityTypes.message, - timestamp=datetime.utcnow(), - from_property=ChannelAccount( - id=getattr(activity.recipient, "id", None), - name=getattr(activity.recipient, "name", None), - ), - recipient=ChannelAccount( - id=activity.from_property.id, name=activity.from_property.name - ), - reply_to_id=activity.id, - service_url=activity.service_url, - channel_id=activity.channel_id, - conversation=ConversationAccount( - is_group=activity.conversation.is_group, - id=activity.conversation.id, - name=activity.conversation.name, - ), - text=text or "", - locale=locale or "", - attachments=[], - entities=[], - ) diff --git a/samples/python_django/13.core-bot/helpers/dialog_helper.py b/samples/python_django/13.core-bot/helpers/dialog_helper.py deleted file mode 100644 index 7c896d18c..000000000 --- a/samples/python_django/13.core-bot/helpers/dialog_helper.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -"""Utility to run dialogs.""" -from botbuilder.core import StatePropertyAccessor, TurnContext -from botbuilder.dialogs import Dialog, DialogSet, DialogTurnStatus - - -class DialogHelper: - """Dialog Helper implementation.""" - - @staticmethod - async def run_dialog( - dialog: Dialog, turn_context: TurnContext, accessor: StatePropertyAccessor - ): # pylint: disable=line-too-long - """Run dialog.""" - dialog_set = DialogSet(accessor) - dialog_set.add(dialog) - - dialog_context = await dialog_set.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dialog_context.begin_dialog(dialog.id) diff --git a/samples/python_django/13.core-bot/helpers/luis_helper.py b/samples/python_django/13.core-bot/helpers/luis_helper.py deleted file mode 100644 index 45a3ab5e5..000000000 --- a/samples/python_django/13.core-bot/helpers/luis_helper.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -"""Helper to call LUIS service.""" -from botbuilder.ai.luis import LuisRecognizer, LuisApplication -from botbuilder.core import TurnContext - -from booking_details import BookingDetails - -# pylint: disable=line-too-long -class LuisHelper: - """LUIS helper implementation.""" - - @staticmethod - async def execute_luis_query( - configuration, turn_context: TurnContext - ) -> BookingDetails: - """Invoke LUIS service to perform prediction/evaluation of utterance.""" - booking_details = BookingDetails() - - # pylint:disable=broad-except - try: - luis_application = LuisApplication( - configuration.LUIS_APP_ID, - configuration.LUIS_API_KEY, - configuration.LUIS_API_HOST_NAME, - ) - - recognizer = LuisRecognizer(luis_application) - recognizer_result = await recognizer.recognize(turn_context) - - if recognizer_result.intents: - intent = sorted( - recognizer_result.intents, - key=recognizer_result.intents.get, - reverse=True, - )[:1][0] - if intent == "Book_flight": - # We need to get the result from the LUIS JSON which at every level returns an array. - to_entities = recognizer_result.entities.get("$instance", {}).get( - "To", [] - ) - if to_entities: - booking_details.destination = to_entities[0]["text"] - from_entities = recognizer_result.entities.get("$instance", {}).get( - "From", [] - ) - if from_entities: - booking_details.origin = from_entities[0]["text"] - - # This value will be a TIMEX. And we are only interested in a Date so grab the first result and drop the Time part. - # TIMEX is a format that represents DateTime expressions that include some ambiguity. e.g. missing a Year. - date_entities = recognizer_result.entities.get("$instance", {}).get( - "datetime", [] - ) - if date_entities: - booking_details.travel_date = ( - None - ) # Set when we get a timex format - except Exception as exception: - print(exception) - - return booking_details diff --git a/samples/python_django/13.core-bot/manage.py b/samples/python_django/13.core-bot/manage.py deleted file mode 100644 index 5b6b9621b..000000000 --- a/samples/python_django/13.core-bot/manage.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -"""Django's command-line utility for administrative tasks.""" -import os -import sys -from django.core.management.commands.runserver import Command as runserver -import config - - -def main(): - """Django's command-line utility for administrative tasks.""" - runserver.default_port = config.DefaultConfig.PORT - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bots.settings") - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == "__main__": - main() diff --git a/samples/python_django/13.core-bot/requirements.txt b/samples/python_django/13.core-bot/requirements.txt deleted file mode 100644 index bc7fd496e..000000000 --- a/samples/python_django/13.core-bot/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -Django>=2.2.1 -requests>=2.18.1 -botframework-connector>=4.4.0.b1 -botbuilder-schema>=4.4.0.b1 -botbuilder-core>=4.4.0.b1 -botbuilder-dialogs>=4.4.0.b1 -botbuilder-ai>=4.4.0.b1 -datatypes-date-time>=1.0.0.a1 -azure-cognitiveservices-language-luis>=0.2.0 \ No newline at end of file