From 0d0e75a9ca34b7914d610eadd0a96f13cc17a7f7 Mon Sep 17 00:00:00 2001 From: Tien Suwandy Date: Wed, 19 Jun 2019 10:47:32 -0700 Subject: [PATCH 1/6] initial django sample --- samples/django/13.core-bot/README-LUIS.md | 216 +++++++++++++++++ samples/django/13.core-bot/README.md | 61 +++++ samples/django/13.core-bot/booking_details.py | 8 + samples/django/13.core-bot/bots/__init__.py | 9 + samples/django/13.core-bot/bots/bots.py | 39 +++ .../bots/dialog_and_welcome_bot.py | 45 ++++ samples/django/13.core-bot/bots/dialog_bot.py | 33 +++ .../bots/resources/welcomeCard.json | 46 ++++ samples/django/13.core-bot/bots/settings.py | 121 ++++++++++ samples/django/13.core-bot/bots/urls.py | 13 + samples/django/13.core-bot/bots/views.py | 36 +++ samples/django/13.core-bot/bots/wsgi.py | 20 ++ .../cognitiveModels/FlightBooking.json | 226 ++++++++++++++++++ samples/django/13.core-bot/config.py | 14 ++ samples/django/13.core-bot/db.sqlite3 | 0 .../django/13.core-bot/dialogs/__init__.py | 13 + .../13.core-bot/dialogs/booking_dialog.py | 99 ++++++++ .../dialogs/cancel_and_help_dialog.py | 39 +++ .../dialogs/date_resolver_dialog.py | 55 +++++ .../django/13.core-bot/dialogs/main_dialog.py | 65 +++++ .../django/13.core-bot/helpers/__init__.py | 9 + .../13.core-bot/helpers/activity_helper.py | 22 ++ .../13.core-bot/helpers/dialog_helper.py | 17 ++ .../django/13.core-bot/helpers/luis_helper.py | 45 ++++ samples/django/13.core-bot/manage.py | 26 ++ samples/django/13.core-bot/requirements.txt | 9 + samples/python-flask/13.core-bot/config.py | 1 + 27 files changed, 1287 insertions(+) create mode 100644 samples/django/13.core-bot/README-LUIS.md create mode 100644 samples/django/13.core-bot/README.md create mode 100644 samples/django/13.core-bot/booking_details.py create mode 100644 samples/django/13.core-bot/bots/__init__.py create mode 100644 samples/django/13.core-bot/bots/bots.py create mode 100644 samples/django/13.core-bot/bots/dialog_and_welcome_bot.py create mode 100644 samples/django/13.core-bot/bots/dialog_bot.py create mode 100644 samples/django/13.core-bot/bots/resources/welcomeCard.json create mode 100644 samples/django/13.core-bot/bots/settings.py create mode 100644 samples/django/13.core-bot/bots/urls.py create mode 100644 samples/django/13.core-bot/bots/views.py create mode 100644 samples/django/13.core-bot/bots/wsgi.py create mode 100644 samples/django/13.core-bot/cognitiveModels/FlightBooking.json create mode 100644 samples/django/13.core-bot/config.py create mode 100644 samples/django/13.core-bot/db.sqlite3 create mode 100644 samples/django/13.core-bot/dialogs/__init__.py create mode 100644 samples/django/13.core-bot/dialogs/booking_dialog.py create mode 100644 samples/django/13.core-bot/dialogs/cancel_and_help_dialog.py create mode 100644 samples/django/13.core-bot/dialogs/date_resolver_dialog.py create mode 100644 samples/django/13.core-bot/dialogs/main_dialog.py create mode 100644 samples/django/13.core-bot/helpers/__init__.py create mode 100644 samples/django/13.core-bot/helpers/activity_helper.py create mode 100644 samples/django/13.core-bot/helpers/dialog_helper.py create mode 100644 samples/django/13.core-bot/helpers/luis_helper.py create mode 100644 samples/django/13.core-bot/manage.py create mode 100644 samples/django/13.core-bot/requirements.txt diff --git a/samples/django/13.core-bot/README-LUIS.md b/samples/django/13.core-bot/README-LUIS.md new file mode 100644 index 000000000..b6b9b925f --- /dev/null +++ b/samples/django/13.core-bot/README-LUIS.md @@ -0,0 +1,216 @@ +# 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/django/13.core-bot/README.md b/samples/django/13.core-bot/README.md new file mode 100644 index 000000000..1724d8d04 --- /dev/null +++ b/samples/django/13.core-bot/README.md @@ -0,0 +1,61 @@ +# 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/django/13.core-bot/booking_details.py b/samples/django/13.core-bot/booking_details.py new file mode 100644 index 000000000..098838966 --- /dev/null +++ b/samples/django/13.core-bot/booking_details.py @@ -0,0 +1,8 @@ +# 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): + self.destination = destination + self.origin = origin + self.travel_date = travel_date \ No newline at end of file diff --git a/samples/django/13.core-bot/bots/__init__.py b/samples/django/13.core-bot/bots/__init__.py new file mode 100644 index 000000000..431b7d8ff --- /dev/null +++ b/samples/django/13.core-bot/bots/__init__.py @@ -0,0 +1,9 @@ +# 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'] \ No newline at end of file diff --git a/samples/django/13.core-bot/bots/bots.py b/samples/django/13.core-bot/bots/bots.py new file mode 100644 index 000000000..ca765c207 --- /dev/null +++ b/samples/django/13.core-bot/bots/bots.py @@ -0,0 +1,39 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import sys +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): + name = 'bots' + appConfig = config.DefaultConfig + + SETTINGS = BotFrameworkAdapterSettings(appConfig.APP_ID, appConfig.APP_PASSWORD) + ADAPTER = BotFrameworkAdapter(SETTINGS) + + # 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) + + # 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. + async def on_error(self, context: TurnContext, error: Exception): + 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/django/13.core-bot/bots/dialog_and_welcome_bot.py b/samples/django/13.core-bot/bots/dialog_and_welcome_bot.py new file mode 100644 index 000000000..cee50366e --- /dev/null +++ b/samples/django/13.core-bot/bots/dialog_and_welcome_bot.py @@ -0,0 +1,45 @@ +# 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 CardFactory +from botbuilder.core import ActivityHandler, ConversationState, UserState, TurnContext +from botbuilder.dialogs import Dialog +from botbuilder.schema import Activity, Attachment, ChannelAccount +from helpers.activity_helper import create_activity_reply + +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 = self.create_response(turn_context.activity, welcome_card) + await turn_context.send_activity(response) + + # Create an attachment message response. + def create_response(self, activity: Activity, attachment: Attachment): + response = create_activity_reply(activity) + response.attachments = [attachment] + return response + + # 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, "resources/welcomeCard.json") + with open(path) as f: + card = json.load(f) + + return Attachment( + content_type= "application/vnd.microsoft.card.adaptive", + content= card) \ No newline at end of file diff --git a/samples/django/13.core-bot/bots/dialog_bot.py b/samples/django/13.core-bot/bots/dialog_bot.py new file mode 100644 index 000000000..e9d1dd008 --- /dev/null +++ b/samples/django/13.core-bot/bots/dialog_bot.py @@ -0,0 +1,33 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import asyncio + +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 + self.dialogState = 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")) \ No newline at end of file diff --git a/samples/django/13.core-bot/bots/resources/welcomeCard.json b/samples/django/13.core-bot/bots/resources/welcomeCard.json new file mode 100644 index 000000000..b6b5f1828 --- /dev/null +++ b/samples/django/13.core-bot/bots/resources/welcomeCard.json @@ -0,0 +1,46 @@ +{ + "$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": "yes", + "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/django/13.core-bot/bots/settings.py b/samples/django/13.core-bot/bots/settings.py new file mode 100644 index 000000000..ca57c8d30 --- /dev/null +++ b/samples/django/13.core-bot/bots/settings.py @@ -0,0 +1,121 @@ +""" +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 = 'q8)bznhagppa$5^_0v8#pm@2)j2@-wh-6waq$hhks5&jw#a7*v' + +# 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/django/13.core-bot/bots/urls.py b/samples/django/13.core-bot/bots/urls.py new file mode 100644 index 000000000..c5cd6d4b7 --- /dev/null +++ b/samples/django/13.core-bot/bots/urls.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from django.contrib import admin +from django.urls import path +from django.views.decorators.csrf import csrf_exempt +from . import views + +urlpatterns = [ + path("", views.home, name="home"), + path("api/messages", csrf_exempt(views.messages), name="messages"), +] \ No newline at end of file diff --git a/samples/django/13.core-bot/bots/views.py b/samples/django/13.core-bot/bots/views.py new file mode 100644 index 000000000..66c770be5 --- /dev/null +++ b/samples/django/13.core-bot/bots/views.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import asyncio +import json +from django.http import HttpResponse +from django.apps import apps +from botbuilder.schema import (Activity, ActivityTypes) + +def home(request): + return HttpResponse("Hello, Django!") + +def messages(request): + if request.headers['Content-Type'] == 'application/json': + 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 '' + loop = asyncio.get_event_loop() + + bot_app = apps.get_app_config('bots') + bot = bot_app.bot + ADAPTER = bot_app.ADAPTER + + async def aux_func(turn_context): + asyncio.ensure_future(bot.on_turn(turn_context)) + try: + task = loop.create_task(ADAPTER.process_activity(activity, auth_header, aux_func)) + loop.run_until_complete(task) + return HttpResponse(status=201) + except Exception as e: + raise e + return HttpResponse("This is message processing!") \ No newline at end of file diff --git a/samples/django/13.core-bot/bots/wsgi.py b/samples/django/13.core-bot/bots/wsgi.py new file mode 100644 index 000000000..7d341ef21 --- /dev/null +++ b/samples/django/13.core-bot/bots/wsgi.py @@ -0,0 +1,20 @@ +#!/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 + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'bots.settings') + +application = get_wsgi_application() diff --git a/samples/django/13.core-bot/cognitiveModels/FlightBooking.json b/samples/django/13.core-bot/cognitiveModels/FlightBooking.json new file mode 100644 index 000000000..0a0d6c4a7 --- /dev/null +++ b/samples/django/13.core-bot/cognitiveModels/FlightBooking.json @@ -0,0 +1,226 @@ +{ + "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/django/13.core-bot/config.py b/samples/django/13.core-bot/config.py new file mode 100644 index 000000000..abaf7ece2 --- /dev/null +++ b/samples/django/13.core-bot/config.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +class DefaultConfig(object): + 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 = "" \ No newline at end of file diff --git a/samples/django/13.core-bot/db.sqlite3 b/samples/django/13.core-bot/db.sqlite3 new file mode 100644 index 000000000..e69de29bb diff --git a/samples/django/13.core-bot/dialogs/__init__.py b/samples/django/13.core-bot/dialogs/__init__.py new file mode 100644 index 000000000..8edc5dc49 --- /dev/null +++ b/samples/django/13.core-bot/dialogs/__init__.py @@ -0,0 +1,13 @@ +# 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'] \ No newline at end of file diff --git a/samples/django/13.core-bot/dialogs/booking_dialog.py b/samples/django/13.core-bot/dialogs/booking_dialog.py new file mode 100644 index 000000000..b7247ca9a --- /dev/null +++ b/samples/django/13.core-bot/dialogs/booking_dialog.py @@ -0,0 +1,99 @@ +# 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 .cancel_and_help_dialog import CancelAndHelpDialog +from .date_resolver_dialog import DateResolverDialog +from datatypes_date_time.timex import Timex + +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__ + + """ + If a destination city has not been provided, prompt for one. + """ + async def destination_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + 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?'))) + else: + return await step_context.next(booking_details.destination) + + """ + If an origin city has not been provided, prompt for one. + """ + async def origin_step(self, step_context: WaterfallStepContext) -> 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): + return await step_context.prompt(TextPrompt.__name__, PromptOptions(prompt= MessageFactory.text('From what city will you be travelling?'))) + else: + return await step_context.next(booking_details.origin) + + """ + If a travel date has not been provided, prompt for one. + This will use the DATE_RESOLVER_DIALOG. + """ + async def travel_date_step(self, step_context: WaterfallStepContext) -> 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) + else: + return await step_context.next(booking_details.travel_date) + + """ + Confirm the information the user has provided. + """ + async def confirm_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + 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 } 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))) + + """ + Complete the interaction and end the dialog. + """ + async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + + 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: + timex_property = Timex(timex) + return 'definite' not in timex_property.types + + + + \ No newline at end of file diff --git a/samples/django/13.core-bot/dialogs/cancel_and_help_dialog.py b/samples/django/13.core-bot/dialogs/cancel_and_help_dialog.py new file mode 100644 index 000000000..70e078cbb --- /dev/null +++ b/samples/django/13.core-bot/dialogs/cancel_and_help_dialog.py @@ -0,0 +1,39 @@ +# 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 + + +class CancelAndHelpDialog(ComponentDialog): + + def __init__(self, dialog_id: str): + super(CancelAndHelpDialog, self).__init__(dialog_id) + + 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: + 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 \ No newline at end of file diff --git a/samples/django/13.core-bot/dialogs/date_resolver_dialog.py b/samples/django/13.core-bot/dialogs/date_resolver_dialog.py new file mode 100644 index 000000000..fc4a07bb7 --- /dev/null +++ b/samples/django/13.core-bot/dialogs/date_resolver_dialog.py @@ -0,0 +1,55 @@ +# 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 .cancel_and_help_dialog import CancelAndHelpDialog +from datatypes_date_time.timex import Timex +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.initialStep, + self.finalStep + ])) + + self.initial_dialog_id = WaterfallDialog.__name__ + '2' + + async def initialStep(self,step_context: WaterfallStepContext) -> DialogTurnResult: + 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( + 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 finalStep(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/django/13.core-bot/dialogs/main_dialog.py b/samples/django/13.core-bot/dialogs/main_dialog.py new file mode 100644 index 000000000..4248d80ec --- /dev/null +++ b/samples/django/13.core-bot/dialogs/main_dialog.py @@ -0,0 +1,65 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from datetime import datetime +from botbuilder.dialogs import ComponentDialog, DialogSet, DialogTurnStatus, WaterfallDialog, WaterfallStepContext, DialogTurnResult +from botbuilder.dialogs.prompts import TextPrompt, ConfirmPrompt, PromptOptions +from botbuilder.core import MessageFactory +from booking_details import BookingDetails +from .booking_dialog import BookingDialog +from helpers.luis_helper import LuisHelper +from datatypes_date_time.timex import Timex +import config + +class MainDialog(ComponentDialog): + + def __init__(self, configuration: dict, dialog_id: str = None): + super(MainDialog, self).__init__(dialog_id or MainDialog.__name__) + + self._configuration = config.DefaultConfig + + 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: + if (not self._configuration.LUIS_APP_ID or not self._configuration.LUIS_API_KEY or not self._configuration.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) + else: + 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: + # 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) if step_context.result is not None else BookingDetails() + + # 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 LUIS call, it will fill out the remainder. + return await step_context.begin_dialog(BookingDialog.__name__, booking_details) + + 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 = f'I have you booked to {result.destination} from {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/django/13.core-bot/helpers/__init__.py b/samples/django/13.core-bot/helpers/__init__.py new file mode 100644 index 000000000..a03686074 --- /dev/null +++ b/samples/django/13.core-bot/helpers/__init__.py @@ -0,0 +1,9 @@ +# 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'] \ No newline at end of file diff --git a/samples/django/13.core-bot/helpers/activity_helper.py b/samples/django/13.core-bot/helpers/activity_helper.py new file mode 100644 index 000000000..043792f15 --- /dev/null +++ b/samples/django/13.core-bot/helpers/activity_helper.py @@ -0,0 +1,22 @@ +# 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 = [] + ) \ No newline at end of file diff --git a/samples/django/13.core-bot/helpers/dialog_helper.py b/samples/django/13.core-bot/helpers/dialog_helper.py new file mode 100644 index 000000000..ad78abc98 --- /dev/null +++ b/samples/django/13.core-bot/helpers/dialog_helper.py @@ -0,0 +1,17 @@ +# 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) \ No newline at end of file diff --git a/samples/django/13.core-bot/helpers/luis_helper.py b/samples/django/13.core-bot/helpers/luis_helper.py new file mode 100644 index 000000000..7f40feeff --- /dev/null +++ b/samples/django/13.core-bot/helpers/luis_helper.py @@ -0,0 +1,45 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from botbuilder.ai.luis import LuisRecognizer, LuisApplication +from botbuilder.core import TurnContext + +from booking_details import BookingDetails + +class LuisHelper: + + @staticmethod + async def execute_luis_query(configuration, turn_context: TurnContext) -> BookingDetails: + booking_details = BookingDetails() + + 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 len(to_entities) > 0: + booking_details.destination = to_entities[0]['text'] + from_entities = recognizer_result.entities.get("$instance", {}).get("From", []) + if len(from_entities) > 0: + booking_details.origin = from_entities[0]['text'] + + # TODO: 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 len(date_entities) > 0: + text = date_entities[0]['text'] + booking_details.travel_date = None # TODO: Set when we get a timex format + except Exception as e: + print(e) + + return booking_details + diff --git a/samples/django/13.core-bot/manage.py b/samples/django/13.core-bot/manage.py new file mode 100644 index 000000000..154a2f4b0 --- /dev/null +++ b/samples/django/13.core-bot/manage.py @@ -0,0 +1,26 @@ +#!/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(): + 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/django/13.core-bot/requirements.txt b/samples/django/13.core-bot/requirements.txt new file mode 100644 index 000000000..1f14188f4 --- /dev/null +++ b/samples/django/13.core-bot/requirements.txt @@ -0,0 +1,9 @@ +Django>=2.2.1 +asyncio>=3.4.3 +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 diff --git a/samples/python-flask/13.core-bot/config.py b/samples/python-flask/13.core-bot/config.py index 1ac541316..4daeb6b39 100644 --- a/samples/python-flask/13.core-bot/config.py +++ b/samples/python-flask/13.core-bot/config.py @@ -8,5 +8,6 @@ class DefaultConfig(object): APP_PASSWORD = "" LUIS_APP_ID = "" LUIS_API_KEY = "" + # LUIS endpoint host name, ie "https://westus.api.cognitive.microsoft.com" LUIS_API_HOST_NAME = "" From 95e044bd77a78e0b0ba318a97120a08b0504e8d7 Mon Sep 17 00:00:00 2001 From: Tien Suwandy Date: Wed, 19 Jun 2019 11:01:14 -0700 Subject: [PATCH 2/6] changes to match flask version --- samples/django/13.core-bot/bots/settings.py | 3 +++ samples/django/13.core-bot/bots/views.py | 4 ++-- samples/django/13.core-bot/requirements.txt | 1 - 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/samples/django/13.core-bot/bots/settings.py b/samples/django/13.core-bot/bots/settings.py index ca57c8d30..ff098f9a7 100644 --- a/samples/django/13.core-bot/bots/settings.py +++ b/samples/django/13.core-bot/bots/settings.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + """ Django settings for bots project. diff --git a/samples/django/13.core-bot/bots/views.py b/samples/django/13.core-bot/bots/views.py index 66c770be5..182802749 100644 --- a/samples/django/13.core-bot/bots/views.py +++ b/samples/django/13.core-bot/bots/views.py @@ -26,9 +26,9 @@ def messages(request): ADAPTER = bot_app.ADAPTER async def aux_func(turn_context): - asyncio.ensure_future(bot.on_turn(turn_context)) + asyncio.ensure_future(bot.on_turn(turn_context), loop=loop) try: - task = loop.create_task(ADAPTER.process_activity(activity, auth_header, aux_func)) + 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 e: diff --git a/samples/django/13.core-bot/requirements.txt b/samples/django/13.core-bot/requirements.txt index 1f14188f4..81f70a761 100644 --- a/samples/django/13.core-bot/requirements.txt +++ b/samples/django/13.core-bot/requirements.txt @@ -1,5 +1,4 @@ Django>=2.2.1 -asyncio>=3.4.3 botframework-connector>=4.4.0.b1 botbuilder-schema>=4.4.0.b1 botbuilder-core>=4.4.0.b1 From cf933dc05b38f4c871c72f80d5b0d01182acae64 Mon Sep 17 00:00:00 2001 From: Tien Suwandy Date: Wed, 19 Jun 2019 17:05:31 -0700 Subject: [PATCH 3/6] pylint cleanup --- samples/django/13.core-bot/bots/__init__.py | 5 +- samples/django/13.core-bot/bots/bots.py | 14 ++-- .../bots/dialog_and_welcome_bot.py | 31 ++++--- samples/django/13.core-bot/bots/dialog_bot.py | 17 ++-- samples/django/13.core-bot/bots/urls.py | 10 ++- samples/django/13.core-bot/bots/views.py | 29 +++++-- samples/django/13.core-bot/config.py | 8 +- .../django/13.core-bot/dialogs/__init__.py | 10 +-- .../13.core-bot/dialogs/booking_dialog.py | 82 +++++++++---------- .../dialogs/cancel_and_help_dialog.py | 12 ++- .../dialogs/date_resolver_dialog.py | 48 ++++++----- .../django/13.core-bot/dialogs/main_dialog.py | 56 +++++++------ .../django/13.core-bot/helpers/__init__.py | 9 +- .../13.core-bot/helpers/activity_helper.py | 32 ++++---- .../13.core-bot/helpers/dialog_helper.py | 8 +- .../django/13.core-bot/helpers/luis_helper.py | 25 +++--- .../python-flask/13.core-bot/bots/__init__.py | 5 +- .../bots/dialog_and_welcome_bot.py | 31 ++++--- .../13.core-bot/bots/dialog_bot.py | 17 ++-- samples/python-flask/13.core-bot/config.py | 3 +- .../13.core-bot/dialogs/__init__.py | 10 +-- .../13.core-bot/dialogs/booking_dialog.py | 82 +++++++++---------- .../dialogs/cancel_and_help_dialog.py | 12 ++- .../dialogs/date_resolver_dialog.py | 48 ++++++----- .../13.core-bot/dialogs/main_dialog.py | 55 +++++++------ .../13.core-bot/helpers/__init__.py | 9 +- .../13.core-bot/helpers/activity_helper.py | 32 ++++---- .../13.core-bot/helpers/dialog_helper.py | 8 +- .../13.core-bot/helpers/luis_helper.py | 24 +++--- samples/python-flask/13.core-bot/main.py | 72 ++++++---------- 30 files changed, 418 insertions(+), 386 deletions(-) diff --git a/samples/django/13.core-bot/bots/__init__.py b/samples/django/13.core-bot/bots/__init__.py index 431b7d8ff..74b723fd8 100644 --- a/samples/django/13.core-bot/bots/__init__.py +++ b/samples/django/13.core-bot/bots/__init__.py @@ -1,9 +1,10 @@ # 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'] \ No newline at end of file + 'DialogBot', + 'DialogAndWelcomeBot'] diff --git a/samples/django/13.core-bot/bots/bots.py b/samples/django/13.core-bot/bots/bots.py index ca765c207..b68118735 100644 --- a/samples/django/13.core-bot/bots/bots.py +++ b/samples/django/13.core-bot/bots/bots.py @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +""" Bot initialization """ +# pylint: disable=line-too-long import sys from django.apps import AppConfig from botbuilder.core import (BotFrameworkAdapter, BotFrameworkAdapterSettings, TurnContext, ConversationState, MemoryStorage, UserState) @@ -9,6 +11,7 @@ import config class BotConfig(AppConfig): + """ Bot initialization """ name = 'bots' appConfig = config.DefaultConfig @@ -23,11 +26,13 @@ class BotConfig(AppConfig): dialog = MainDialog(appConfig) bot = DialogAndWelcomeBot(conversation_state, user_state, dialog) - # 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. 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!') @@ -36,4 +41,3 @@ async def on_error(self, context: TurnContext, error: Exception): def ready(self): self.ADAPTER.on_turn_error = self.on_error - diff --git a/samples/django/13.core-bot/bots/dialog_and_welcome_bot.py b/samples/django/13.core-bot/bots/dialog_and_welcome_bot.py index cee50366e..357b00fef 100644 --- a/samples/django/13.core-bot/bots/dialog_and_welcome_bot.py +++ b/samples/django/13.core-bot/bots/dialog_and_welcome_bot.py @@ -1,45 +1,42 @@ # 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 CardFactory -from botbuilder.core import ActivityHandler, ConversationState, UserState, TurnContext -from botbuilder.dialogs import Dialog +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.""" - 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): + 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. + # 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) - - # Create an attachment message 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 f: - card = json.load(f) + with open(path) as card_file: + card = json.load(card_file) return Attachment( - content_type= "application/vnd.microsoft.card.adaptive", - content= card) \ No newline at end of file + content_type="application/vnd.microsoft.card.adaptive", + content=card) diff --git a/samples/django/13.core-bot/bots/dialog_bot.py b/samples/django/13.core-bot/bots/dialog_bot.py index e9d1dd008..f73b71c72 100644 --- a/samples/django/13.core-bot/bots/dialog_bot.py +++ b/samples/django/13.core-bot/bots/dialog_bot.py @@ -1,26 +1,26 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - -import asyncio +"""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): - - def __init__(self, conversation_state: ConversationState, user_state: UserState, dialog: Dialog): + """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') + 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) @@ -28,6 +28,7 @@ async def on_turn(self, turn_context: TurnContext): # 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")) \ No newline at end of file + await DialogHelper.run_dialog(self.dialog, turn_context, + self.conversation_state.create_property("DialogState")) # pylint: disable=C0103 diff --git a/samples/django/13.core-bot/bots/urls.py b/samples/django/13.core-bot/bots/urls.py index c5cd6d4b7..99cf42018 100644 --- a/samples/django/13.core-bot/bots/urls.py +++ b/samples/django/13.core-bot/bots/urls.py @@ -2,12 +2,14 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from django.contrib import admin +""" 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"), -] \ No newline at end of file + path("", views.home, name="home"), + path("api/messages", csrf_exempt(views.messages), name="messages"), +] diff --git a/samples/django/13.core-bot/bots/views.py b/samples/django/13.core-bot/bots/views.py index 182802749..5417befba 100644 --- a/samples/django/13.core-bot/bots/views.py +++ b/samples/django/13.core-bot/bots/views.py @@ -2,18 +2,29 @@ # 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, ActivityTypes) +from botbuilder.schema import Activity -def home(request): - return HttpResponse("Hello, Django!") +# pylint: disable=line-too-long +def home(): + """Default handler.""" + return HttpResponse("Hello!") def messages(request): + """Main bot message handler.""" if request.headers['Content-Type'] == 'application/json': - body = json.loads(request.body.decode("utf-8")) + body = json.loads(request.body.decode("utf-8")) else: return HttpResponse(status=415) @@ -23,14 +34,14 @@ def messages(request): bot_app = apps.get_app_config('bots') bot = bot_app.bot - ADAPTER = bot_app.ADAPTER + adapter = bot_app.ADAPTER async def aux_func(turn_context): asyncio.ensure_future(bot.on_turn(turn_context), loop=loop) try: - task = asyncio.ensure_future(ADAPTER.process_activity(activity, auth_header, aux_func), loop=loop) + 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 e: - raise e - return HttpResponse("This is message processing!") \ No newline at end of file + except Exception as exception: + raise exception + return HttpResponse("This is message processing!") diff --git a/samples/django/13.core-bot/config.py b/samples/django/13.core-bot/config.py index abaf7ece2..77cba6c52 100644 --- a/samples/django/13.core-bot/config.py +++ b/samples/django/13.core-bot/config.py @@ -2,13 +2,15 @@ # 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_APP_ID = "f292bd26-91a9-4d33-b83b-15010fd7aaac" # LUIS authoring key from LUIS portal or LUIS Cognitive Service subscription key - LUIS_API_KEY = "" + LUIS_API_KEY = "4db5bf288edd4945822aeb124c59d8a5" # LUIS endpoint host name, ie "https://westus.api.cognitive.microsoft.com" - LUIS_API_HOST_NAME = "" \ No newline at end of file + LUIS_API_HOST_NAME = "https://westus.api.cognitive.microsoft.com" diff --git a/samples/django/13.core-bot/dialogs/__init__.py b/samples/django/13.core-bot/dialogs/__init__.py index 8edc5dc49..1d3c05892 100644 --- a/samples/django/13.core-bot/dialogs/__init__.py +++ b/samples/django/13.core-bot/dialogs/__init__.py @@ -1,13 +1,13 @@ # 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'] \ No newline at end of file + 'BookingDialog', + 'CancelAndHelpDialog', + 'DateResolverDialog', + 'MainDialog'] diff --git a/samples/django/13.core-bot/dialogs/booking_dialog.py b/samples/django/13.core-bot/dialogs/booking_dialog.py index b7247ca9a..fd6464370 100644 --- a/samples/django/13.core-bot/dialogs/booking_dialog.py +++ b/samples/django/13.core-bot/dialogs/booking_dialog.py @@ -1,14 +1,18 @@ # 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 -from datatypes_date_time.timex import Timex + + class BookingDialog(CancelAndHelpDialog): + """Flight booking implementation.""" def __init__(self, dialog_id: str = None): super(BookingDialog, self).__init__(dialog_id or BookingDialog.__name__) @@ -25,75 +29,67 @@ def __init__(self, dialog_id: str = None): ])) self.initial_dialog_id = WaterfallDialog.__name__ - - """ - If a destination city has not been provided, prompt for one. - """ - async def destination_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + + 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?'))) - else: + 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) - """ - If an origin city has not been provided, prompt for one. - """ - async def origin_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + 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?'))) - else: + 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) - """ - If a travel date has not been provided, prompt for one. - This will use the DATE_RESOLVER_DIALOG. - """ - async def travel_date_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + 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) - else: + 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) - """ - Confirm the information the user has provided. - """ - async def confirm_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + 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 } from: { booking_details.origin } on: { booking_details.travel_date}.' + 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))) + return await step_context.prompt(ConfirmPrompt.__name__, + PromptOptions(prompt=MessageFactory.text(msg))) - """ - Complete the interaction and end the dialog. - """ async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - - if step_context.result: + """Complete the interaction and end the dialog.""" + if step_context.result: booking_details = step_context.options - booking_details.travel_date= step_context.result + booking_details.travel_date = step_context.result return await step_context.end_dialog(booking_details) - else: + else: return await step_context.end_dialog() - def is_ambiguous(self, timex: str) -> bool: + def is_ambiguous(self, timex: str) -> bool: + """Ensure time is correct.""" timex_property = Timex(timex) return 'definite' not in timex_property.types - - - - \ No newline at end of file diff --git a/samples/django/13.core-bot/dialogs/cancel_and_help_dialog.py b/samples/django/13.core-bot/dialogs/cancel_and_help_dialog.py index 70e078cbb..0e9010ffb 100644 --- a/samples/django/13.core-bot/dialogs/cancel_and_help_dialog.py +++ b/samples/django/13.core-bot/dialogs/cancel_and_help_dialog.py @@ -1,22 +1,19 @@ # 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): - - def __init__(self, dialog_id: str): - super(CancelAndHelpDialog, self).__init__(dialog_id) - + """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: @@ -25,6 +22,7 @@ async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult: 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() @@ -36,4 +34,4 @@ async def interrupt(self, inner_dc: DialogContext) -> DialogTurnResult: await inner_dc.context.send_activity("Cancelling") return await inner_dc.cancel_all_dialogs() - return None \ No newline at end of file + return None diff --git a/samples/django/13.core-bot/dialogs/date_resolver_dialog.py b/samples/django/13.core-bot/dialogs/date_resolver_dialog.py index fc4a07bb7..ba910c7d2 100644 --- a/samples/django/13.core-bot/dialogs/date_resolver_dialog.py +++ b/samples/django/13.core-bot/dialogs/date_resolver_dialog.py @@ -1,55 +1,63 @@ # 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 .cancel_and_help_dialog import CancelAndHelpDialog +from botbuilder.dialogs.prompts import DateTimePrompt, PromptValidatorContext, \ + PromptOptions, DateTimeResolution from datatypes_date_time.timex import Timex -class DateResolverDialog(CancelAndHelpDialog): +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(DateTimePrompt(DateTimePrompt.__name__, + DateResolverDialog.datetime_prompt_validator)) self.add_dialog(WaterfallDialog(WaterfallDialog.__name__ + '2', [ - self.initialStep, - self.finalStep + self.initial_step, + self.final_step ])) self.initial_dialog_id = WaterfallDialog.__name__ + '2' - - async def initialStep(self,step_context: WaterfallStepContext) -> DialogTurnResult: + + 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." + 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( - prompt= MessageFactory.text(prompt_msg), - retry_prompt= MessageFactory.text(reprompt_msg) + 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)) + return await step_context.prompt(DateTimePrompt.__name__, + PromptOptions(prompt=reprompt_msg)) else: - return await step_context.next(DateTimeResolution(timex= timex)) + return await step_context.next(DateTimeResolution(timex=timex)) - async def finalStep(self, step_context: WaterfallStepContext): + 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 + # TODO: Needs TimexProperty return 'definite' in Timex(timex).types - + return False diff --git a/samples/django/13.core-bot/dialogs/main_dialog.py b/samples/django/13.core-bot/dialogs/main_dialog.py index 4248d80ec..2df612c5e 100644 --- a/samples/django/13.core-bot/dialogs/main_dialog.py +++ b/samples/django/13.core-bot/dialogs/main_dialog.py @@ -1,22 +1,20 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - -from datetime import datetime -from botbuilder.dialogs import ComponentDialog, DialogSet, DialogTurnStatus, WaterfallDialog, WaterfallStepContext, DialogTurnResult -from botbuilder.dialogs.prompts import TextPrompt, ConfirmPrompt, PromptOptions +"""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.language_helper import LanguageHelper from .booking_dialog import BookingDialog -from helpers.luis_helper import LuisHelper -from datatypes_date_time.timex import Timex -import config 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 = config.DefaultConfig + self._configuration = configuration self.add_dialog(TextPrompt(TextPrompt.__name__)) self.add_dialog(BookingDialog()) @@ -27,38 +25,44 @@ def __init__(self, configuration: dict, dialog_id: str = None): ])) self.initial_dialog_id = 'WFDialog' - - async def intro_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - if (not self._configuration.LUIS_APP_ID or not self._configuration.LUIS_API_KEY or not self._configuration.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) - else: - return await step_context.prompt(TextPrompt.__name__, PromptOptions(prompt = MessageFactory.text("What can I help you with today?"))) + 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: - # 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) if step_context.result is not None else BookingDetails() + """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. + # 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 LanguageHelper.excecute_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 LUIS call, it will fill out the remainder. + + # 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: - # 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): + """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 {result.origin} on {result.travel_date}' + 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.")) diff --git a/samples/django/13.core-bot/helpers/__init__.py b/samples/django/13.core-bot/helpers/__init__.py index a03686074..9b996eadf 100644 --- a/samples/django/13.core-bot/helpers/__init__.py +++ b/samples/django/13.core-bot/helpers/__init__.py @@ -1,9 +1,10 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +"""Helpers module.""" -from . import activity_helper, luis_helper, dialog_helper +from . import activity_helper, language_helper, dialog_helper __all__ = [ - 'activity_helper', - 'dialog_helper', - 'luis_helper'] \ No newline at end of file + 'activity_helper', + 'dialog_helper', + 'luis_helper'] diff --git a/samples/django/13.core-bot/helpers/activity_helper.py b/samples/django/13.core-bot/helpers/activity_helper.py index 043792f15..12dd60143 100644 --- a/samples/django/13.core-bot/helpers/activity_helper.py +++ b/samples/django/13.core-bot/helpers/activity_helper.py @@ -1,22 +1,26 @@ # 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 = [] - ) \ No newline at end of file + 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/django/13.core-bot/helpers/dialog_helper.py b/samples/django/13.core-bot/helpers/dialog_helper.py index ad78abc98..550a17b5f 100644 --- a/samples/django/13.core-bot/helpers/dialog_helper.py +++ b/samples/django/13.core-bot/helpers/dialog_helper.py @@ -1,17 +1,19 @@ # 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): + 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) \ No newline at end of file + await dialog_context.begin_dialog(dialog.id) diff --git a/samples/django/13.core-bot/helpers/luis_helper.py b/samples/django/13.core-bot/helpers/luis_helper.py index 7f40feeff..9bb597c20 100644 --- a/samples/django/13.core-bot/helpers/luis_helper.py +++ b/samples/django/13.core-bot/helpers/luis_helper.py @@ -1,16 +1,21 @@ # 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, @@ -26,20 +31,18 @@ async def execute_luis_query(configuration, turn_context: TurnContext) -> Bookin 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 len(to_entities) > 0: + if to_entities: booking_details.destination = to_entities[0]['text'] from_entities = recognizer_result.entities.get("$instance", {}).get("From", []) - if len(from_entities) > 0: + if from_entities: booking_details.origin = from_entities[0]['text'] - # TODO: This value will be a TIMEX. And we are only interested in a Date so grab the first result and drop the Time part. + # 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 len(date_entities) > 0: - text = date_entities[0]['text'] - booking_details.travel_date = None # TODO: Set when we get a timex format - except Exception as e: - print(e) - - return booking_details + 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-flask/13.core-bot/bots/__init__.py b/samples/python-flask/13.core-bot/bots/__init__.py index 431b7d8ff..74b723fd8 100644 --- a/samples/python-flask/13.core-bot/bots/__init__.py +++ b/samples/python-flask/13.core-bot/bots/__init__.py @@ -1,9 +1,10 @@ # 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'] \ No newline at end of file + 'DialogBot', + 'DialogAndWelcomeBot'] diff --git a/samples/python-flask/13.core-bot/bots/dialog_and_welcome_bot.py b/samples/python-flask/13.core-bot/bots/dialog_and_welcome_bot.py index cee50366e..357b00fef 100644 --- a/samples/python-flask/13.core-bot/bots/dialog_and_welcome_bot.py +++ b/samples/python-flask/13.core-bot/bots/dialog_and_welcome_bot.py @@ -1,45 +1,42 @@ # 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 CardFactory -from botbuilder.core import ActivityHandler, ConversationState, UserState, TurnContext -from botbuilder.dialogs import Dialog +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.""" - 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): + 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. + # 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) - - # Create an attachment message 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 f: - card = json.load(f) + with open(path) as card_file: + card = json.load(card_file) return Attachment( - content_type= "application/vnd.microsoft.card.adaptive", - content= card) \ No newline at end of file + content_type="application/vnd.microsoft.card.adaptive", + content=card) diff --git a/samples/python-flask/13.core-bot/bots/dialog_bot.py b/samples/python-flask/13.core-bot/bots/dialog_bot.py index e9d1dd008..f73b71c72 100644 --- a/samples/python-flask/13.core-bot/bots/dialog_bot.py +++ b/samples/python-flask/13.core-bot/bots/dialog_bot.py @@ -1,26 +1,26 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - -import asyncio +"""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): - - def __init__(self, conversation_state: ConversationState, user_state: UserState, dialog: Dialog): + """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') + 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) @@ -28,6 +28,7 @@ async def on_turn(self, turn_context: TurnContext): # 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")) \ No newline at end of file + await DialogHelper.run_dialog(self.dialog, turn_context, + self.conversation_state.create_property("DialogState")) # pylint: disable=C0103 diff --git a/samples/python-flask/13.core-bot/config.py b/samples/python-flask/13.core-bot/config.py index 4daeb6b39..612a2d73d 100644 --- a/samples/python-flask/13.core-bot/config.py +++ b/samples/python-flask/13.core-bot/config.py @@ -2,7 +2,9 @@ # 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 = "" @@ -10,4 +12,3 @@ class DefaultConfig(object): LUIS_API_KEY = "" # LUIS endpoint host name, ie "https://westus.api.cognitive.microsoft.com" LUIS_API_HOST_NAME = "" - diff --git a/samples/python-flask/13.core-bot/dialogs/__init__.py b/samples/python-flask/13.core-bot/dialogs/__init__.py index 8edc5dc49..1d3c05892 100644 --- a/samples/python-flask/13.core-bot/dialogs/__init__.py +++ b/samples/python-flask/13.core-bot/dialogs/__init__.py @@ -1,13 +1,13 @@ # 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'] \ No newline at end of file + 'BookingDialog', + 'CancelAndHelpDialog', + 'DateResolverDialog', + 'MainDialog'] diff --git a/samples/python-flask/13.core-bot/dialogs/booking_dialog.py b/samples/python-flask/13.core-bot/dialogs/booking_dialog.py index b7247ca9a..fd6464370 100644 --- a/samples/python-flask/13.core-bot/dialogs/booking_dialog.py +++ b/samples/python-flask/13.core-bot/dialogs/booking_dialog.py @@ -1,14 +1,18 @@ # 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 -from datatypes_date_time.timex import Timex + + class BookingDialog(CancelAndHelpDialog): + """Flight booking implementation.""" def __init__(self, dialog_id: str = None): super(BookingDialog, self).__init__(dialog_id or BookingDialog.__name__) @@ -25,75 +29,67 @@ def __init__(self, dialog_id: str = None): ])) self.initial_dialog_id = WaterfallDialog.__name__ - - """ - If a destination city has not been provided, prompt for one. - """ - async def destination_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + + 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?'))) - else: + 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) - """ - If an origin city has not been provided, prompt for one. - """ - async def origin_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + 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?'))) - else: + 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) - """ - If a travel date has not been provided, prompt for one. - This will use the DATE_RESOLVER_DIALOG. - """ - async def travel_date_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + 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) - else: + 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) - """ - Confirm the information the user has provided. - """ - async def confirm_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + 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 } from: { booking_details.origin } on: { booking_details.travel_date}.' + 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))) + return await step_context.prompt(ConfirmPrompt.__name__, + PromptOptions(prompt=MessageFactory.text(msg))) - """ - Complete the interaction and end the dialog. - """ async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - - if step_context.result: + """Complete the interaction and end the dialog.""" + if step_context.result: booking_details = step_context.options - booking_details.travel_date= step_context.result + booking_details.travel_date = step_context.result return await step_context.end_dialog(booking_details) - else: + else: return await step_context.end_dialog() - def is_ambiguous(self, timex: str) -> bool: + def is_ambiguous(self, timex: str) -> bool: + """Ensure time is correct.""" timex_property = Timex(timex) return 'definite' not in timex_property.types - - - - \ No newline at end of file diff --git a/samples/python-flask/13.core-bot/dialogs/cancel_and_help_dialog.py b/samples/python-flask/13.core-bot/dialogs/cancel_and_help_dialog.py index 70e078cbb..0e9010ffb 100644 --- a/samples/python-flask/13.core-bot/dialogs/cancel_and_help_dialog.py +++ b/samples/python-flask/13.core-bot/dialogs/cancel_and_help_dialog.py @@ -1,22 +1,19 @@ # 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): - - def __init__(self, dialog_id: str): - super(CancelAndHelpDialog, self).__init__(dialog_id) - + """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: @@ -25,6 +22,7 @@ async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult: 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() @@ -36,4 +34,4 @@ async def interrupt(self, inner_dc: DialogContext) -> DialogTurnResult: await inner_dc.context.send_activity("Cancelling") return await inner_dc.cancel_all_dialogs() - return None \ No newline at end of file + return None diff --git a/samples/python-flask/13.core-bot/dialogs/date_resolver_dialog.py b/samples/python-flask/13.core-bot/dialogs/date_resolver_dialog.py index fc4a07bb7..ba910c7d2 100644 --- a/samples/python-flask/13.core-bot/dialogs/date_resolver_dialog.py +++ b/samples/python-flask/13.core-bot/dialogs/date_resolver_dialog.py @@ -1,55 +1,63 @@ # 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 .cancel_and_help_dialog import CancelAndHelpDialog +from botbuilder.dialogs.prompts import DateTimePrompt, PromptValidatorContext, \ + PromptOptions, DateTimeResolution from datatypes_date_time.timex import Timex -class DateResolverDialog(CancelAndHelpDialog): +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(DateTimePrompt(DateTimePrompt.__name__, + DateResolverDialog.datetime_prompt_validator)) self.add_dialog(WaterfallDialog(WaterfallDialog.__name__ + '2', [ - self.initialStep, - self.finalStep + self.initial_step, + self.final_step ])) self.initial_dialog_id = WaterfallDialog.__name__ + '2' - - async def initialStep(self,step_context: WaterfallStepContext) -> DialogTurnResult: + + 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." + 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( - prompt= MessageFactory.text(prompt_msg), - retry_prompt= MessageFactory.text(reprompt_msg) + 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)) + return await step_context.prompt(DateTimePrompt.__name__, + PromptOptions(prompt=reprompt_msg)) else: - return await step_context.next(DateTimeResolution(timex= timex)) + return await step_context.next(DateTimeResolution(timex=timex)) - async def finalStep(self, step_context: WaterfallStepContext): + 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 + # TODO: Needs TimexProperty return 'definite' in Timex(timex).types - + return False diff --git a/samples/python-flask/13.core-bot/dialogs/main_dialog.py b/samples/python-flask/13.core-bot/dialogs/main_dialog.py index db42d586c..2df612c5e 100644 --- a/samples/python-flask/13.core-bot/dialogs/main_dialog.py +++ b/samples/python-flask/13.core-bot/dialogs/main_dialog.py @@ -1,17 +1,16 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - -from datetime import datetime -from botbuilder.dialogs import ComponentDialog, DialogSet, DialogTurnStatus, WaterfallDialog, WaterfallStepContext, DialogTurnResult -from botbuilder.dialogs.prompts import TextPrompt, ConfirmPrompt, PromptOptions +"""Main dialog. """ +from botbuilder.dialogs import ComponentDialog, WaterfallDialog, \ + WaterfallStepContext, DialogTurnResult +from botbuilder.dialogs.prompts import TextPrompt, PromptOptions from botbuilder.core import MessageFactory -from .booking_dialog import BookingDialog from booking_details import BookingDetails -from helpers.luis_helper import LuisHelper -from datatypes_date_time.timex import Timex +from helpers.language_helper import LanguageHelper +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__) @@ -26,38 +25,44 @@ def __init__(self, configuration: dict, dialog_id: str = None): ])) self.initial_dialog_id = 'WFDialog' - - async def intro_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - 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) - else: - return await step_context.prompt(TextPrompt.__name__, PromptOptions(prompt = MessageFactory.text("What can I help you with today?"))) + 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: - # Call LUIS and gather any potential booking details. (Note the TurnContext has the response to the prompt.) - booking_details = await LuisHelper.excecute_luis_query(self._configuration, step_context.context) if step_context.result is not None else BookingDetails() + """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. + # 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 LanguageHelper.excecute_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 LUIS call, it will fill out the remainder. + + # 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: - # 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): + """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 {result.origin} on {result.travel_date}' + 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.")) diff --git a/samples/python-flask/13.core-bot/helpers/__init__.py b/samples/python-flask/13.core-bot/helpers/__init__.py index a03686074..9b996eadf 100644 --- a/samples/python-flask/13.core-bot/helpers/__init__.py +++ b/samples/python-flask/13.core-bot/helpers/__init__.py @@ -1,9 +1,10 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +"""Helpers module.""" -from . import activity_helper, luis_helper, dialog_helper +from . import activity_helper, language_helper, dialog_helper __all__ = [ - 'activity_helper', - 'dialog_helper', - 'luis_helper'] \ No newline at end of file + 'activity_helper', + 'dialog_helper', + 'luis_helper'] diff --git a/samples/python-flask/13.core-bot/helpers/activity_helper.py b/samples/python-flask/13.core-bot/helpers/activity_helper.py index 043792f15..12dd60143 100644 --- a/samples/python-flask/13.core-bot/helpers/activity_helper.py +++ b/samples/python-flask/13.core-bot/helpers/activity_helper.py @@ -1,22 +1,26 @@ # 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 = [] - ) \ No newline at end of file + 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-flask/13.core-bot/helpers/dialog_helper.py b/samples/python-flask/13.core-bot/helpers/dialog_helper.py index ad78abc98..550a17b5f 100644 --- a/samples/python-flask/13.core-bot/helpers/dialog_helper.py +++ b/samples/python-flask/13.core-bot/helpers/dialog_helper.py @@ -1,17 +1,19 @@ # 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): + 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) \ No newline at end of file + await dialog_context.begin_dialog(dialog.id) diff --git a/samples/python-flask/13.core-bot/helpers/luis_helper.py b/samples/python-flask/13.core-bot/helpers/luis_helper.py index 0a3195529..ca1223353 100644 --- a/samples/python-flask/13.core-bot/helpers/luis_helper.py +++ b/samples/python-flask/13.core-bot/helpers/luis_helper.py @@ -1,16 +1,21 @@ # 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 excecute_luis_query(configuration: dict, 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'], @@ -26,20 +31,17 @@ async def excecute_luis_query(configuration: dict, turn_context: TurnContext) -> 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 len(to_entities) > 0: + if to_entities: booking_details.destination = to_entities[0]['text'] from_entities = recognizer_result.entities.get("$instance", {}).get("From", []) - if len(from_entities) > 0: + if from_entities: booking_details.origin = from_entities[0]['text'] - # TODO: This value will be a TIMEX. And we are only interested in a Date so grab the first result and drop the Time part. + # 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 len(date_entities) > 0: - text = date_entities[0]['text'] - booking_details.travel_date = None # TODO: Set when we get a timex format - except Exception as e: - print(e) - + 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-flask/13.core-bot/main.py b/samples/python-flask/13.core-bot/main.py index 542219f67..c166d45fc 100644 --- a/samples/python-flask/13.core-bot/main.py +++ b/samples/python-flask/13.core-bot/main.py @@ -8,53 +8,35 @@ - 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. -gi """ -from functools import wraps -import json + import asyncio -import sys -from flask import Flask, jsonify, request, Response -from botbuilder.schema import (Activity, ActivityTypes) -from botbuilder.core import (BotFrameworkAdapter, BotFrameworkAdapterSettings, TurnContext, - ConversationState, MemoryStorage, UserState) +from flask import Flask, request, Response +from botbuilder.core import (BotFrameworkAdapter, BotFrameworkAdapterSettings, + ConversationState, MemoryStorage, UserState) +from botbuilder.schema import (Activity) from dialogs import MainDialog from bots import DialogAndWelcomeBot -from helpers.dialog_helper import DialogHelper -loop = asyncio.get_event_loop() -app = Flask(__name__, instance_relative_config=True) -app.config.from_object('config.DefaultConfig') +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']) +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('Oops. Something went wrong!') - # Clear out state - await conversation_state.delete(context) - -ADAPTER.on_turn_error = on_error - # Create MemoryStorage, UserState and ConversationState -memory = MemoryStorage() +MEMORY = MemoryStorage() +USER_STATE = UserState(MEMORY) +CONVERSATION_STATE = ConversationState(MEMORY) +DIALOG = MainDialog(APP.config) +BOT = DialogAndWelcomeBot(CONVERSATION_STATE, USER_STATE, DIALOG) -user_state = UserState(memory) -conversation_state = ConversationState(memory) -dialog = MainDialog(app.config) -bot = DialogAndWelcomeBot(conversation_state, user_state, dialog) - -@app.route('/api/messages', methods = ['POST']) +@APP.route('/api/messages', methods=['POST']) def messages(): - + """Main bot message handler.""" if request.headers['Content-Type'] == 'application/json': body = request.json else: @@ -62,19 +44,19 @@ def messages(): activity = Activity().deserialize(body) auth_header = request.headers['Authorization'] if 'Authorization' in request.headers else '' - + async def aux_func(turn_context): - asyncio.ensure_future(bot.on_turn(turn_context), loop=loop) + asyncio.ensure_future(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) + task = LOOP.create_task(ADAPTER.process_activity(activity, auth_header, aux_func)) + LOOP.run_until_complete(task) return Response(status=201) - except Exception as e: - raise e + except Exception as exception: + raise exception -if __name__ == "__main__" : - try: - app.run(debug=True, port=app.config["PORT"]) - except Exception as e: - raise e +if __name__ == "__main__": + try: + APP.run(debug=True, port=APP.config["PORT"]) # nosec debug + except Exception as exception: + raise exception From a779bd30ca19ce6f7b60238c9aa700ae786ef48d Mon Sep 17 00:00:00 2001 From: Tien Suwandy Date: Wed, 19 Jun 2019 17:06:13 -0700 Subject: [PATCH 4/6] Update config.py --- samples/django/13.core-bot/config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/django/13.core-bot/config.py b/samples/django/13.core-bot/config.py index 77cba6c52..f28350b1d 100644 --- a/samples/django/13.core-bot/config.py +++ b/samples/django/13.core-bot/config.py @@ -9,8 +9,8 @@ class DefaultConfig(object): APP_ID = "" APP_PASSWORD = "" - LUIS_APP_ID = "f292bd26-91a9-4d33-b83b-15010fd7aaac" + LUIS_APP_ID = "" # LUIS authoring key from LUIS portal or LUIS Cognitive Service subscription key - LUIS_API_KEY = "4db5bf288edd4945822aeb124c59d8a5" + LUIS_API_KEY = "" # LUIS endpoint host name, ie "https://westus.api.cognitive.microsoft.com" - LUIS_API_HOST_NAME = "https://westus.api.cognitive.microsoft.com" + LUIS_API_HOST_NAME = "" From b056ebb4e9f60bbec7e69377e026025bf93f4b51 Mon Sep 17 00:00:00 2001 From: Tien Suwandy Date: Thu, 20 Jun 2019 01:02:07 -0700 Subject: [PATCH 5/6] Language_helper -> luis_helper --- samples/django/13.core-bot/booking_details.py | 5 ++++- samples/django/13.core-bot/bots/wsgi.py | 3 +-- samples/django/13.core-bot/config.py | 1 + samples/django/13.core-bot/dialogs/booking_dialog.py | 2 -- samples/django/13.core-bot/dialogs/main_dialog.py | 4 ++-- samples/django/13.core-bot/helpers/__init__.py | 5 +++-- samples/django/13.core-bot/helpers/activity_helper.py | 1 + samples/django/13.core-bot/manage.py | 1 + samples/django/13.core-bot/requirements.txt | 1 + samples/python-flask/13.core-bot/booking_details.py | 5 ++++- samples/python-flask/13.core-bot/dialogs/main_dialog.py | 4 ++-- samples/python-flask/13.core-bot/helpers/__init__.py | 5 +++-- 12 files changed, 23 insertions(+), 14 deletions(-) diff --git a/samples/django/13.core-bot/booking_details.py b/samples/django/13.core-bot/booking_details.py index 098838966..dbee56240 100644 --- a/samples/django/13.core-bot/booking_details.py +++ b/samples/django/13.core-bot/booking_details.py @@ -1,8 +1,11 @@ # 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 \ No newline at end of file + self.travel_date = travel_date + \ No newline at end of file diff --git a/samples/django/13.core-bot/bots/wsgi.py b/samples/django/13.core-bot/bots/wsgi.py index 7d341ef21..4475b3fbe 100644 --- a/samples/django/13.core-bot/bots/wsgi.py +++ b/samples/django/13.core-bot/bots/wsgi.py @@ -12,9 +12,8 @@ """ 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/django/13.core-bot/config.py b/samples/django/13.core-bot/config.py index f28350b1d..8bf76757c 100644 --- a/samples/django/13.core-bot/config.py +++ b/samples/django/13.core-bot/config.py @@ -14,3 +14,4 @@ class DefaultConfig(object): LUIS_API_KEY = "" # LUIS endpoint host name, ie "https://westus.api.cognitive.microsoft.com" LUIS_API_HOST_NAME = "" + \ No newline at end of file diff --git a/samples/django/13.core-bot/dialogs/booking_dialog.py b/samples/django/13.core-bot/dialogs/booking_dialog.py index fd6464370..95f7fba85 100644 --- a/samples/django/13.core-bot/dialogs/booking_dialog.py +++ b/samples/django/13.core-bot/dialogs/booking_dialog.py @@ -9,8 +9,6 @@ from .cancel_and_help_dialog import CancelAndHelpDialog from .date_resolver_dialog import DateResolverDialog - - class BookingDialog(CancelAndHelpDialog): """Flight booking implementation.""" diff --git a/samples/django/13.core-bot/dialogs/main_dialog.py b/samples/django/13.core-bot/dialogs/main_dialog.py index 2df612c5e..f28d9993c 100644 --- a/samples/django/13.core-bot/dialogs/main_dialog.py +++ b/samples/django/13.core-bot/dialogs/main_dialog.py @@ -6,7 +6,7 @@ from botbuilder.dialogs.prompts import TextPrompt, PromptOptions from botbuilder.core import MessageFactory from booking_details import BookingDetails -from helpers.language_helper import LanguageHelper +from helpers.luis_helper import LuisHelper from .booking_dialog import BookingDialog class MainDialog(ComponentDialog): @@ -39,7 +39,7 @@ async def act_step(self, step_context: WaterfallStepContext) -> DialogTurnResult # 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 LanguageHelper.excecute_query(self._configuration,\ + booking_details = await LuisHelper.execute_luis_query(self._configuration,\ step_context.context) if step_context.result is not None else BookingDetails() diff --git a/samples/django/13.core-bot/helpers/__init__.py b/samples/django/13.core-bot/helpers/__init__.py index 9b996eadf..7ed8a466a 100644 --- a/samples/django/13.core-bot/helpers/__init__.py +++ b/samples/django/13.core-bot/helpers/__init__.py @@ -1,10 +1,11 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -"""Helpers module.""" -from . import activity_helper, language_helper, dialog_helper +"""Helpers module.""" +from . import activity_helper, luis_helper, dialog_helper __all__ = [ 'activity_helper', 'dialog_helper', 'luis_helper'] + \ No newline at end of file diff --git a/samples/django/13.core-bot/helpers/activity_helper.py b/samples/django/13.core-bot/helpers/activity_helper.py index 12dd60143..334275648 100644 --- a/samples/django/13.core-bot/helpers/activity_helper.py +++ b/samples/django/13.core-bot/helpers/activity_helper.py @@ -24,3 +24,4 @@ def create_activity_reply(activity: Activity, text: str = None, locale: str = No attachments=[], entities=[] ) + \ No newline at end of file diff --git a/samples/django/13.core-bot/manage.py b/samples/django/13.core-bot/manage.py index 154a2f4b0..cdc5a0f34 100644 --- a/samples/django/13.core-bot/manage.py +++ b/samples/django/13.core-bot/manage.py @@ -9,6 +9,7 @@ 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: diff --git a/samples/django/13.core-bot/requirements.txt b/samples/django/13.core-bot/requirements.txt index 81f70a761..bc7fd496e 100644 --- a/samples/django/13.core-bot/requirements.txt +++ b/samples/django/13.core-bot/requirements.txt @@ -1,4 +1,5 @@ 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 diff --git a/samples/python-flask/13.core-bot/booking_details.py b/samples/python-flask/13.core-bot/booking_details.py index 098838966..dbee56240 100644 --- a/samples/python-flask/13.core-bot/booking_details.py +++ b/samples/python-flask/13.core-bot/booking_details.py @@ -1,8 +1,11 @@ # 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 \ No newline at end of file + self.travel_date = travel_date + \ No newline at end of file diff --git a/samples/python-flask/13.core-bot/dialogs/main_dialog.py b/samples/python-flask/13.core-bot/dialogs/main_dialog.py index 2df612c5e..f28d9993c 100644 --- a/samples/python-flask/13.core-bot/dialogs/main_dialog.py +++ b/samples/python-flask/13.core-bot/dialogs/main_dialog.py @@ -6,7 +6,7 @@ from botbuilder.dialogs.prompts import TextPrompt, PromptOptions from botbuilder.core import MessageFactory from booking_details import BookingDetails -from helpers.language_helper import LanguageHelper +from helpers.luis_helper import LuisHelper from .booking_dialog import BookingDialog class MainDialog(ComponentDialog): @@ -39,7 +39,7 @@ async def act_step(self, step_context: WaterfallStepContext) -> DialogTurnResult # 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 LanguageHelper.excecute_query(self._configuration,\ + booking_details = await LuisHelper.execute_luis_query(self._configuration,\ step_context.context) if step_context.result is not None else BookingDetails() diff --git a/samples/python-flask/13.core-bot/helpers/__init__.py b/samples/python-flask/13.core-bot/helpers/__init__.py index 9b996eadf..7ed8a466a 100644 --- a/samples/python-flask/13.core-bot/helpers/__init__.py +++ b/samples/python-flask/13.core-bot/helpers/__init__.py @@ -1,10 +1,11 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -"""Helpers module.""" -from . import activity_helper, language_helper, dialog_helper +"""Helpers module.""" +from . import activity_helper, luis_helper, dialog_helper __all__ = [ 'activity_helper', 'dialog_helper', 'luis_helper'] + \ No newline at end of file From ee4fca48d65b7ccfdac0813c422002ad28f8d455 Mon Sep 17 00:00:00 2001 From: Tien Suwandy Date: Fri, 21 Jun 2019 11:41:03 -0700 Subject: [PATCH 6/6] Update luis_helper.py --- samples/python-flask/13.core-bot/helpers/luis_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/python-flask/13.core-bot/helpers/luis_helper.py b/samples/python-flask/13.core-bot/helpers/luis_helper.py index ca1223353..899f813aa 100644 --- a/samples/python-flask/13.core-bot/helpers/luis_helper.py +++ b/samples/python-flask/13.core-bot/helpers/luis_helper.py @@ -11,7 +11,7 @@ class LuisHelper: """LUIS helper implementation.""" @staticmethod - async def excecute_luis_query(configuration: dict, turn_context: TurnContext) -> BookingDetails: + async def execute_luis_query(configuration: dict, turn_context: TurnContext) -> BookingDetails: """Invoke LUIS service to perform prediction/evaluation of utterance.""" booking_details = BookingDetails()