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..dbee56240 --- /dev/null +++ b/samples/django/13.core-bot/booking_details.py @@ -0,0 +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 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..74b723fd8 --- /dev/null +++ b/samples/django/13.core-bot/bots/__init__.py @@ -0,0 +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'] 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..b68118735 --- /dev/null +++ b/samples/django/13.core-bot/bots/bots.py @@ -0,0 +1,43 @@ +# 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) +from dialogs import MainDialog +from bots import DialogAndWelcomeBot +import config + +class BotConfig(AppConfig): + """ Bot initialization """ + name = 'bots' + appConfig = config.DefaultConfig + + SETTINGS = BotFrameworkAdapterSettings(appConfig.APP_ID, appConfig.APP_PASSWORD) + ADAPTER = BotFrameworkAdapter(SETTINGS) + + # Create MemoryStorage, UserState and ConversationState + memory = MemoryStorage() + user_state = UserState(memory) + conversation_state = ConversationState(memory) + + dialog = MainDialog(appConfig) + bot = DialogAndWelcomeBot(conversation_state, user_state, dialog) + + async def on_error(self, context: TurnContext, error: Exception): + """ + Catch-all for errors. + This check writes out errors to console log + NOTE: In production environment, you should consider logging this to Azure + application insights. + """ + print(f'\n [on_turn_error]: { error }', file=sys.stderr) + # Send a message to the user + await context.send_activity('Oops. Something went wrong!') + # Clear out state + await self.conversation_state.delete(context) + + def ready(self): + self.ADAPTER.on_turn_error = self.on_error diff --git a/samples/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..357b00fef --- /dev/null +++ b/samples/django/13.core-bot/bots/dialog_and_welcome_bot.py @@ -0,0 +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 TurnContext +from botbuilder.schema import Activity, Attachment, ChannelAccount +from helpers.activity_helper import create_activity_reply +from .dialog_bot import DialogBot + +class DialogAndWelcomeBot(DialogBot): + """Main dialog to welcome users implementation.""" + + async def on_members_added_activity(self, members_added: List[ChannelAccount], + turn_context: TurnContext): + for member in members_added: + # Greet anyone that was not the target (recipient) of this message. + # To learn more about Adaptive Cards, see https://aka.ms/msbot-adaptivecards + # for more details. + if member.id != turn_context.activity.recipient.id: + welcome_card = self.create_adaptive_card_attachment() + response = self.create_response(turn_context.activity, welcome_card) + await turn_context.send_activity(response) + + def create_response(self, activity: Activity, attachment: Attachment): + """Create an attachment message response.""" + response = create_activity_reply(activity) + response.attachments = [attachment] + return response + + # Load attachment from file. + def create_adaptive_card_attachment(self): + """Create an adaptive card.""" + relative_path = os.path.abspath(os.path.dirname(__file__)) + path = os.path.join(relative_path, "resources/welcomeCard.json") + with open(path) as card_file: + card = json.load(card_file) + + return Attachment( + content_type="application/vnd.microsoft.card.adaptive", + content=card) diff --git a/samples/django/13.core-bot/bots/dialog_bot.py b/samples/django/13.core-bot/bots/dialog_bot.py new file mode 100644 index 000000000..f73b71c72 --- /dev/null +++ b/samples/django/13.core-bot/bots/dialog_bot.py @@ -0,0 +1,34 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""Implements bot Activity handler.""" + +from botbuilder.core import ActivityHandler, ConversationState, UserState, TurnContext +from botbuilder.dialogs import Dialog +from helpers.dialog_helper import DialogHelper + +class DialogBot(ActivityHandler): + """Main activity handler for the bot.""" + def __init__(self, conversation_state: ConversationState, + user_state: UserState, dialog: Dialog): + if conversation_state is None: + raise Exception('[DialogBot]: Missing parameter. conversation_state is required') + if user_state is None: + raise Exception('[DialogBot]: Missing parameter. user_state is required') + if dialog is None: + raise Exception('[DialogBot]: Missing parameter. dialog is required') + + self.conversation_state = conversation_state + self.user_state = user_state + self.dialog = dialog + self.dialogState = self.conversation_state.create_property('DialogState') # pylint: disable=C0103 + + async def on_turn(self, turn_context: TurnContext): + await super().on_turn(turn_context) + + # Save any state changes that might have occured during the turn. + await self.conversation_state.save_changes(turn_context, False) + await self.user_state.save_changes(turn_context, False) + + async def on_message_activity(self, turn_context: TurnContext): + await DialogHelper.run_dialog(self.dialog, turn_context, + self.conversation_state.create_property("DialogState")) # pylint: disable=C0103 diff --git a/samples/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..ff098f9a7 --- /dev/null +++ b/samples/django/13.core-bot/bots/settings.py @@ -0,0 +1,124 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Django settings for bots project. + +Generated by 'django-admin startproject' using Django 2.2.1. + +For more information on this file, see +https://docs.djangoproject.com/en/2.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/2.2/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '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..99cf42018 --- /dev/null +++ b/samples/django/13.core-bot/bots/urls.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" URL configuration for bot message handler """ + +from django.urls import path +from django.views.decorators.csrf import csrf_exempt +from . import views + +# pylint:disable=invalid-name +urlpatterns = [ + path("", views.home, name="home"), + path("api/messages", csrf_exempt(views.messages), name="messages"), +] diff --git a/samples/django/13.core-bot/bots/views.py b/samples/django/13.core-bot/bots/views.py new file mode 100644 index 000000000..5417befba --- /dev/null +++ b/samples/django/13.core-bot/bots/views.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +This sample shows how to create a bot that demonstrates the following: +- Use [LUIS](https://www.luis.ai) to implement core AI capabilities. +- Implement a multi-turn conversation using Dialogs. +- Handle user interruptions for such things as `Help` or `Cancel`. +- Prompt for and validate requests for information from the user. +""" + +import asyncio +import json +from django.http import HttpResponse +from django.apps import apps +from botbuilder.schema import Activity + +# pylint: disable=line-too-long +def home(): + """Default handler.""" + return HttpResponse("Hello!") + +def messages(request): + """Main bot message handler.""" + if 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), loop=loop) + try: + task = asyncio.ensure_future(adapter.process_activity(activity, auth_header, aux_func), loop=loop) + loop.run_until_complete(task) + return HttpResponse(status=201) + except Exception as exception: + raise exception + return HttpResponse("This is message processing!") diff --git a/samples/django/13.core-bot/bots/wsgi.py b/samples/django/13.core-bot/bots/wsgi.py new file mode 100644 index 000000000..4475b3fbe --- /dev/null +++ b/samples/django/13.core-bot/bots/wsgi.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +WSGI config for bots project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ +""" + +import os +from django.core.wsgi import get_wsgi_application + +# pylint:disable=invalid-name +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'bots.settings') +application = get_wsgi_application() diff --git a/samples/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..8bf76757c --- /dev/null +++ b/samples/django/13.core-bot/config.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" Bot Configuration """ +class DefaultConfig(object): + """ Bot Configuration """ + PORT = 3978 + APP_ID = "" + APP_PASSWORD = "" + + LUIS_APP_ID = "" + # LUIS authoring key from LUIS portal or LUIS Cognitive Service subscription key + LUIS_API_KEY = "" + # LUIS endpoint host name, ie "https://westus.api.cognitive.microsoft.com" + LUIS_API_HOST_NAME = "" + \ 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..1d3c05892 --- /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. +"""Dialogs module""" +from .booking_dialog import BookingDialog +from .cancel_and_help_dialog import CancelAndHelpDialog +from .date_resolver_dialog import DateResolverDialog +from .main_dialog import MainDialog + +__all__ = [ + 'BookingDialog', + 'CancelAndHelpDialog', + 'DateResolverDialog', + 'MainDialog'] diff --git a/samples/django/13.core-bot/dialogs/booking_dialog.py b/samples/django/13.core-bot/dialogs/booking_dialog.py new file mode 100644 index 000000000..95f7fba85 --- /dev/null +++ b/samples/django/13.core-bot/dialogs/booking_dialog.py @@ -0,0 +1,93 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""Flight booking dialog.""" + +from botbuilder.dialogs import WaterfallDialog, WaterfallStepContext, DialogTurnResult +from botbuilder.dialogs.prompts import ConfirmPrompt, TextPrompt, PromptOptions +from botbuilder.core import MessageFactory +from datatypes_date_time.timex import Timex +from .cancel_and_help_dialog import CancelAndHelpDialog +from .date_resolver_dialog import DateResolverDialog + +class BookingDialog(CancelAndHelpDialog): + """Flight booking implementation.""" + + def __init__(self, dialog_id: str = None): + super(BookingDialog, self).__init__(dialog_id or BookingDialog.__name__) + + self.add_dialog(TextPrompt(TextPrompt.__name__)) + #self.add_dialog(ConfirmPrompt(ConfirmPrompt.__name__)) + self.add_dialog(DateResolverDialog(DateResolverDialog.__name__)) + self.add_dialog(WaterfallDialog(WaterfallDialog.__name__, [ + self.destination_step, + self.origin_step, + self.travel_date_step, + #self.confirm_step, + self.final_step + ])) + + self.initial_dialog_id = WaterfallDialog.__name__ + + async def destination_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + """Prompt for destination.""" + booking_details = step_context.options + + if booking_details.destination is None: + return await step_context.prompt(TextPrompt.__name__, + PromptOptions(prompt=MessageFactory.text( + 'To what city would you like to travel?'))) # pylint: disable=line-too-long,bad-continuation + else: + return await step_context.next(booking_details.destination) + + async def origin_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + """Prompt for origin city.""" + booking_details = step_context.options + + # Capture the response to the previous step's prompt + booking_details.destination = step_context.result + if booking_details.origin is None: + return await step_context.prompt(TextPrompt.__name__, + PromptOptions(prompt=MessageFactory.text('From what city will you be travelling?'))) # pylint: disable=line-too-long,bad-continuation + else: + return await step_context.next(booking_details.origin) + + async def travel_date_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + """Prompt for travel date. + This will use the DATE_RESOLVER_DIALOG.""" + + booking_details = step_context.options + + # Capture the results of the previous step + booking_details.origin = step_context.result + if (not booking_details.travel_date or self.is_ambiguous(booking_details.travel_date)): + return await step_context.begin_dialog(DateResolverDialog.__name__, booking_details.travel_date) # pylint: disable=line-too-long + else: + return await step_context.next(booking_details.travel_date) + + async def confirm_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + """Confirm the information the user has provided.""" + booking_details = step_context.options + + # Capture the results of the previous step + booking_details.travel_date = step_context.result + msg = f'Please confirm, I have you traveling to: { booking_details.destination }'\ + f' from: { booking_details.origin } on: { booking_details.travel_date}.' + + # Offer a YES/NO prompt. + return await step_context.prompt(ConfirmPrompt.__name__, + PromptOptions(prompt=MessageFactory.text(msg))) + + async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + """Complete the interaction and end the dialog.""" + if step_context.result: + booking_details = step_context.options + booking_details.travel_date = step_context.result + + return await step_context.end_dialog(booking_details) + else: + return await step_context.end_dialog() + + def is_ambiguous(self, timex: str) -> bool: + """Ensure time is correct.""" + timex_property = Timex(timex) + return 'definite' not in timex_property.types diff --git a/samples/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..0e9010ffb --- /dev/null +++ b/samples/django/13.core-bot/dialogs/cancel_and_help_dialog.py @@ -0,0 +1,37 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""Handle cancel and help intents.""" +from botbuilder.dialogs import ComponentDialog, DialogContext, DialogTurnResult, DialogTurnStatus +from botbuilder.schema import ActivityTypes + + +class CancelAndHelpDialog(ComponentDialog): + """Implementation of handling cancel and help.""" + async def on_begin_dialog(self, inner_dc: DialogContext, options: object) -> DialogTurnResult: + result = await self.interrupt(inner_dc) + if result is not None: + return result + + return await super(CancelAndHelpDialog, self).on_begin_dialog(inner_dc, options) + + async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult: + result = await self.interrupt(inner_dc) + if result is not None: + return result + + return await super(CancelAndHelpDialog, self).on_continue_dialog(inner_dc) + + async def interrupt(self, inner_dc: DialogContext) -> DialogTurnResult: + """Detect interruptions.""" + if inner_dc.context.activity.type == ActivityTypes.message: + text = inner_dc.context.activity.text.lower() + + if text == 'help' or text == '?': + await inner_dc.context.send_activity("Show Help...") + return DialogTurnResult(DialogTurnStatus.Waiting) + + if text == 'cancel' or text == 'quit': + await inner_dc.context.send_activity("Cancelling") + return await inner_dc.cancel_all_dialogs() + + return None diff --git a/samples/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..ba910c7d2 --- /dev/null +++ b/samples/django/13.core-bot/dialogs/date_resolver_dialog.py @@ -0,0 +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 datatypes_date_time.timex import Timex +from .cancel_and_help_dialog import CancelAndHelpDialog + +class DateResolverDialog(CancelAndHelpDialog): + """Resolve the date""" + def __init__(self, dialog_id: str = None): + super(DateResolverDialog, self).__init__(dialog_id or DateResolverDialog.__name__) + + self.add_dialog(DateTimePrompt(DateTimePrompt.__name__, + DateResolverDialog.datetime_prompt_validator)) + self.add_dialog(WaterfallDialog(WaterfallDialog.__name__ + '2', [ + self.initial_step, + self.final_step + ])) + + self.initial_dialog_id = WaterfallDialog.__name__ + '2' + + async def initial_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + """Prompt for the date.""" + timex = step_context.options + + prompt_msg = 'On what date would you like to travel?' + reprompt_msg = "I'm sorry, for best results, please enter your travel "\ + "date including the month, day and year." + + if timex is None: + # We were not given any date at all so prompt the user. + return await step_context.prompt(DateTimePrompt.__name__, + PromptOptions( # pylint: disable=bad-continuation + prompt=MessageFactory.text(prompt_msg), + retry_prompt=MessageFactory.text(reprompt_msg) + )) + else: + # We have a Date we just need to check it is unambiguous. + if 'definite' in Timex(timex).types: + # This is essentially a "reprompt" of the data we were given up front. + return await step_context.prompt(DateTimePrompt.__name__, + PromptOptions(prompt=reprompt_msg)) + else: + return await step_context.next(DateTimeResolution(timex=timex)) + + async def final_step(self, step_context: WaterfallStepContext): + """Cleanup - set final return value and end dialog.""" + timex = step_context.result[0].timex + return await step_context.end_dialog(timex) + + @staticmethod + async def datetime_prompt_validator(prompt_context: PromptValidatorContext) -> bool: + """ Validate the date provided is in proper form. """ + if prompt_context.recognized.succeeded: + timex = prompt_context.recognized.value[0].timex.split('T')[0] + + # TODO: Needs TimexProperty + return 'definite' in Timex(timex).types + + return False diff --git a/samples/django/13.core-bot/dialogs/main_dialog.py b/samples/django/13.core-bot/dialogs/main_dialog.py new file mode 100644 index 000000000..f28d9993c --- /dev/null +++ b/samples/django/13.core-bot/dialogs/main_dialog.py @@ -0,0 +1,69 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""Main dialog. """ +from botbuilder.dialogs import ComponentDialog, WaterfallDialog, \ + WaterfallStepContext, DialogTurnResult +from botbuilder.dialogs.prompts import TextPrompt, PromptOptions +from botbuilder.core import MessageFactory +from booking_details import BookingDetails +from helpers.luis_helper import LuisHelper +from .booking_dialog import BookingDialog + +class MainDialog(ComponentDialog): + """Main dialog. """ + def __init__(self, configuration: dict, dialog_id: str = None): + super(MainDialog, self).__init__(dialog_id or MainDialog.__name__) + + self._configuration = configuration + + self.add_dialog(TextPrompt(TextPrompt.__name__)) + self.add_dialog(BookingDialog()) + self.add_dialog(WaterfallDialog('WFDialog', [ + self.intro_step, + self.act_step, + self.final_step + ])) + + self.initial_dialog_id = 'WFDialog' + + async def intro_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + """Initial prompt.""" + return await step_context.prompt(TextPrompt.__name__, PromptOptions( + prompt=MessageFactory.text("What can I help you with today?"))) + + + async def act_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + """Use language understanding to gather details about booking.""" + + + # In this sample we only have a single Intent we are concerned with. + # However, typically a scenario will have multiple different Intents + # each corresponding to starting a different child Dialog. + booking_details = await LuisHelper.execute_luis_query(self._configuration,\ + step_context.context) if step_context.result is not None else BookingDetails() + + + # Run the BookingDialog giving it whatever details we have from the + # model. The dialog will prompt to find out the remaining details. + return await step_context.begin_dialog(BookingDialog.__name__, booking_details) + + async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + """Complete dialog. + At this step, with details from the user, display the completed + flight booking to the user. + """ + # If the child dialog ("BookingDialog") was cancelled or the user failed + # to confirm, the Result here will be null. + if step_context.result is not None: + result = step_context.result + + # Now we have all the booking details call the booking service. + # If the call to the booking service was successful tell the user. + #time_property = Timex(result.travel_date) + #travel_date_msg = time_property.to_natural_language(datetime.now()) + msg = f'I have you booked to {result.destination} from'\ + f' {result.origin} on {result.travel_date}.' + await step_context.context.send_activity(MessageFactory.text(msg)) + else: + await step_context.context.send_activity(MessageFactory.text("Thank you.")) + return await step_context.end_dialog() diff --git a/samples/django/13.core-bot/helpers/__init__.py b/samples/django/13.core-bot/helpers/__init__.py new file mode 100644 index 000000000..7ed8a466a --- /dev/null +++ b/samples/django/13.core-bot/helpers/__init__.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +"""Helpers module.""" +from . import activity_helper, luis_helper, dialog_helper + +__all__ = [ + 'activity_helper', + 'dialog_helper', + 'luis_helper'] + \ 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..334275648 --- /dev/null +++ b/samples/django/13.core-bot/helpers/activity_helper.py @@ -0,0 +1,27 @@ +# 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 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..550a17b5f --- /dev/null +++ b/samples/django/13.core-bot/helpers/dialog_helper.py @@ -0,0 +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): # pylint: disable=line-too-long + """Run dialog.""" + dialog_set = DialogSet(accessor) + dialog_set.add(dialog) + + dialog_context = await dialog_set.create_context(turn_context) + results = await dialog_context.continue_dialog() + if results.status == DialogTurnStatus.Empty: + await dialog_context.begin_dialog(dialog.id) diff --git a/samples/django/13.core-bot/helpers/luis_helper.py b/samples/django/13.core-bot/helpers/luis_helper.py new file mode 100644 index 000000000..9bb597c20 --- /dev/null +++ b/samples/django/13.core-bot/helpers/luis_helper.py @@ -0,0 +1,48 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +"""Helper to call LUIS service.""" +from botbuilder.ai.luis import LuisRecognizer, LuisApplication +from botbuilder.core import TurnContext + +from booking_details import BookingDetails + +# pylint: disable=line-too-long +class LuisHelper: + """LUIS helper implementation.""" + @staticmethod + async def execute_luis_query(configuration, turn_context: TurnContext) -> BookingDetails: + """Invoke LUIS service to perform prediction/evaluation of utterance.""" + booking_details = BookingDetails() + + # pylint:disable=broad-except + try: + luis_application = LuisApplication( + configuration.LUIS_APP_ID, + configuration.LUIS_API_KEY, + configuration.LUIS_API_HOST_NAME + ) + + recognizer = LuisRecognizer(luis_application) + recognizer_result = await recognizer.recognize(turn_context) + + if recognizer_result.intents: + intent = sorted(recognizer_result.intents, key=recognizer_result.intents.get, reverse=True)[:1][0] + if intent == 'Book_flight': + # We need to get the result from the LUIS JSON which at every level returns an array. + to_entities = recognizer_result.entities.get("$instance", {}).get("To", []) + if to_entities: + booking_details.destination = to_entities[0]['text'] + from_entities = recognizer_result.entities.get("$instance", {}).get("From", []) + if from_entities: + booking_details.origin = from_entities[0]['text'] + + # This value will be a TIMEX. And we are only interested in a Date so grab the first result and drop the Time part. + # TIMEX is a format that represents DateTime expressions that include some ambiguity. e.g. missing a Year. + date_entities = recognizer_result.entities.get("$instance", {}).get("datetime", []) + if date_entities: + booking_details.travel_date = None # Set when we get a timex format + except Exception as exception: + print(exception) + + return booking_details diff --git a/samples/django/13.core-bot/manage.py b/samples/django/13.core-bot/manage.py new file mode 100644 index 000000000..cdc5a0f34 --- /dev/null +++ b/samples/django/13.core-bot/manage.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +"""Django's command-line utility for administrative tasks.""" +import os +import sys +from django.core.management.commands.runserver import Command as runserver +import config + +def main(): + """Django's command-line utility for administrative tasks.""" + runserver.default_port = config.DefaultConfig.PORT + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'bots.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/samples/django/13.core-bot/requirements.txt b/samples/django/13.core-bot/requirements.txt new file mode 100644 index 000000000..bc7fd496e --- /dev/null +++ b/samples/django/13.core-bot/requirements.txt @@ -0,0 +1,9 @@ +Django>=2.2.1 +requests>=2.18.1 +botframework-connector>=4.4.0.b1 +botbuilder-schema>=4.4.0.b1 +botbuilder-core>=4.4.0.b1 +botbuilder-dialogs>=4.4.0.b1 +botbuilder-ai>=4.4.0.b1 +datatypes-date-time>=1.0.0.a1 +azure-cognitiveservices-language-luis>=0.2.0 \ No newline at end of file diff --git a/samples/python-flask/13.core-bot/app.py b/samples/python-flask/13.core-bot/app.py index 542219f67..c166d45fc 100644 --- a/samples/python-flask/13.core-bot/app.py +++ b/samples/python-flask/13.core-bot/app.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 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/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 1ac541316..612a2d73d 100644 --- a/samples/python-flask/13.core-bot/config.py +++ b/samples/python-flask/13.core-bot/config.py @@ -2,11 +2,13 @@ # 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_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..f28d9993c 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 .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 LuisHelper.execute_luis_query(self._configuration,\ + step_context.context) if step_context.result is not None else BookingDetails() - # Run the BookingDialog giving it whatever details we have from the 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..7ed8a466a 100644 --- a/samples/python-flask/13.core-bot/helpers/__init__.py +++ b/samples/python-flask/13.core-bot/helpers/__init__.py @@ -1,9 +1,11 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +"""Helpers module.""" from . import activity_helper, luis_helper, dialog_helper __all__ = [ - 'activity_helper', - 'dialog_helper', - 'luis_helper'] \ No newline at end of file + 'activity_helper', + 'dialog_helper', + 'luis_helper'] + \ No newline at end of file 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..899f813aa 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: + async def execute_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 -