diff --git a/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/LuisRecognizerOptionsV3.java b/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/LuisRecognizerOptionsV3.java index e22e5c251..a4a59d885 100644 --- a/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/LuisRecognizerOptionsV3.java +++ b/libraries/bot-ai-luis-v3/src/main/java/com/microsoft/bot/ai/luis/LuisRecognizerOptionsV3.java @@ -77,7 +77,7 @@ public class LuisRecognizerOptionsV3 extends LuisRecognizerOptions { /** * Value indicating whether or not instance data should be included in response. */ - private boolean includeInstanceData = false; + private boolean includeInstanceData = true; /** * Value indicating whether queries should be logged in LUIS. If queries should diff --git a/libraries/bot-applicationinsights/pom.xml b/libraries/bot-applicationinsights/pom.xml index 1dc45c1cf..649c864a2 100644 --- a/libraries/bot-applicationinsights/pom.xml +++ b/libraries/bot-applicationinsights/pom.xml @@ -57,7 +57,7 @@ com.microsoft.azure applicationinsights-core - 2.4.1 + 2.6.3 diff --git a/libraries/bot-applicationinsights/src/main/java/com/microsoft/bot/applicationinsights/ApplicationInsightsBotTelemetryClient.java b/libraries/bot-applicationinsights/src/main/java/com/microsoft/bot/applicationinsights/ApplicationInsightsBotTelemetryClient.java index 6434a7754..92761e155 100644 --- a/libraries/bot-applicationinsights/src/main/java/com/microsoft/bot/applicationinsights/ApplicationInsightsBotTelemetryClient.java +++ b/libraries/bot-applicationinsights/src/main/java/com/microsoft/bot/applicationinsights/ApplicationInsightsBotTelemetryClient.java @@ -4,6 +4,7 @@ package com.microsoft.bot.applicationinsights; import com.microsoft.applicationinsights.TelemetryClient; +import com.microsoft.applicationinsights.TelemetryConfiguration; import com.microsoft.applicationinsights.telemetry.EventTelemetry; import com.microsoft.applicationinsights.telemetry.ExceptionTelemetry; import com.microsoft.applicationinsights.telemetry.PageViewTelemetry; @@ -12,6 +13,7 @@ import com.microsoft.applicationinsights.telemetry.TraceTelemetry; import com.microsoft.bot.builder.BotTelemetryClient; import com.microsoft.bot.builder.Severity; +import org.apache.commons.lang3.StringUtils; import java.time.Duration; import java.time.OffsetDateTime; @@ -26,17 +28,30 @@ public class ApplicationInsightsBotTelemetryClient implements BotTelemetryClient { private final TelemetryClient telemetryClient; + private final TelemetryConfiguration telemetryConfiguration; + + /** + * Provides access to the Application Insights configuration that is running here. + * Allows developers to adjust the options. + * @return Application insights configuration. + */ + public TelemetryConfiguration getTelemetryConfiguration() { + return telemetryConfiguration; + } /** * Initializes a new instance of the {@link BotTelemetryClient}. * - * @param withTelemetryClient The telemetry client to forward bot events to. + * @param instrumentationKey The instrumentation key provided to create + * the {@link ApplicationInsightsBotTelemetryClient}. */ - public ApplicationInsightsBotTelemetryClient(TelemetryClient withTelemetryClient) { - if (withTelemetryClient == null) { - throw new IllegalArgumentException("withTelemetry should be provided"); + public ApplicationInsightsBotTelemetryClient(String instrumentationKey) { + if (StringUtils.isBlank(instrumentationKey)) { + throw new IllegalArgumentException("instrumentationKey should be provided"); } - this.telemetryClient = withTelemetryClient; + this.telemetryConfiguration = TelemetryConfiguration.getActive(); + telemetryConfiguration.setInstrumentationKey(instrumentationKey); + this.telemetryClient = new TelemetryClient(telemetryConfiguration); } /** diff --git a/libraries/bot-applicationinsights/src/test/java/com/microsoft/bot/applicationinsights/BotTelemetryClientTests.java b/libraries/bot-applicationinsights/src/test/java/com/microsoft/bot/applicationinsights/BotTelemetryClientTests.java index 855f139f9..a9a2fbb31 100644 --- a/libraries/bot-applicationinsights/src/test/java/com/microsoft/bot/applicationinsights/BotTelemetryClientTests.java +++ b/libraries/bot-applicationinsights/src/test/java/com/microsoft/bot/applicationinsights/BotTelemetryClientTests.java @@ -3,8 +3,6 @@ package com.microsoft.bot.applicationinsights; -import com.microsoft.applicationinsights.TelemetryClient; -import com.microsoft.applicationinsights.TelemetryConfiguration; import com.microsoft.applicationinsights.channel.TelemetryChannel; import com.microsoft.applicationinsights.telemetry.EventTelemetry; import com.microsoft.applicationinsights.telemetry.RemoteDependencyTelemetry; @@ -26,19 +24,14 @@ public class BotTelemetryClientTests { - private BotTelemetryClient botTelemetryClient; + private ApplicationInsightsBotTelemetryClient botTelemetryClient; private TelemetryChannel mockTelemetryChannel; @Before public void initialize() { + botTelemetryClient = new ApplicationInsightsBotTelemetryClient("fakeKey"); mockTelemetryChannel = Mockito.mock(TelemetryChannel.class); - - TelemetryConfiguration telemetryConfiguration = new TelemetryConfiguration(); - telemetryConfiguration.setInstrumentationKey("UNITTEST-INSTRUMENTATION-KEY"); - telemetryConfiguration.setChannel(mockTelemetryChannel); - TelemetryClient telemetryClient = new TelemetryClient(telemetryConfiguration); - - botTelemetryClient = new ApplicationInsightsBotTelemetryClient(telemetryClient); + botTelemetryClient.getTelemetryConfiguration().setChannel(mockTelemetryChannel); } @Test @@ -49,16 +42,20 @@ public void nullTelemetryClientThrows() { } @Test - public void nonNullTelemetryClientSucceeds() { - TelemetryClient telemetryClient = new TelemetryClient(); + public void emptyTelemetryClientThrows() { + Assert.assertThrows(IllegalArgumentException.class, () -> { + new ApplicationInsightsBotTelemetryClient(""); + }); + } - BotTelemetryClient botTelemetryClient = new ApplicationInsightsBotTelemetryClient(telemetryClient); + @Test + public void nonNullTelemetryClientSucceeds() { + BotTelemetryClient botTelemetryClient = new ApplicationInsightsBotTelemetryClient("fakeKey"); } @Test public void overrideTest() { - TelemetryClient telemetryClient = new TelemetryClient(); - MyBotTelemetryClient botTelemetryClient = new MyBotTelemetryClient(telemetryClient); + MyBotTelemetryClient botTelemetryClient = new MyBotTelemetryClient("fakeKey"); } @Test @@ -180,4 +177,13 @@ public void trackPageViewTest() { Assert.assertEquals(0, Double.compare(0.6, pageViewTelemetry.getMetrics().get("metric"))); }).send(Mockito.any(PageViewTelemetry.class)); } + + @Test + public void flushTest() { + botTelemetryClient.flush(); + + Mockito.verify(mockTelemetryChannel, invocations -> { + Assert.assertEquals(1, invocations.getAllInvocations().size()); + }).send(Mockito.any()); + } } diff --git a/libraries/bot-applicationinsights/src/test/java/com/microsoft/bot/applicationinsights/MyBotTelemetryClient.java b/libraries/bot-applicationinsights/src/test/java/com/microsoft/bot/applicationinsights/MyBotTelemetryClient.java index 9ccf38495..acc48e9c8 100644 --- a/libraries/bot-applicationinsights/src/test/java/com/microsoft/bot/applicationinsights/MyBotTelemetryClient.java +++ b/libraries/bot-applicationinsights/src/test/java/com/microsoft/bot/applicationinsights/MyBotTelemetryClient.java @@ -3,7 +3,6 @@ package com.microsoft.bot.applicationinsights; -import com.microsoft.applicationinsights.TelemetryClient; import com.microsoft.bot.builder.Severity; import java.time.Duration; @@ -11,8 +10,8 @@ import java.util.Map; public class MyBotTelemetryClient extends ApplicationInsightsBotTelemetryClient { - public MyBotTelemetryClient(TelemetryClient telemetryClient) { - super(telemetryClient); + public MyBotTelemetryClient(String instrumentationKey) { + super(instrumentationKey); } @Override diff --git a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TelemetryLoggerMiddleware.java b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TelemetryLoggerMiddleware.java index 9a6670856..850c69c56 100644 --- a/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TelemetryLoggerMiddleware.java +++ b/libraries/bot-builder/src/main/java/com/microsoft/bot/builder/TelemetryLoggerMiddleware.java @@ -195,14 +195,17 @@ protected CompletableFuture> fillReceiveEventProperties( ) { Map properties = new HashMap(); - properties.put(TelemetryConstants.FROMIDPROPERTY, activity.getFrom().getId()); - properties.put( - TelemetryConstants.CONVERSATIONNAMEPROPERTY, - activity.getConversation().getName() - ); - properties.put(TelemetryConstants.LOCALEPROPERTY, activity.getLocale()); - properties.put(TelemetryConstants.RECIPIENTIDPROPERTY, activity.getRecipient().getId()); - properties.put(TelemetryConstants.RECIPIENTNAMEPROPERTY, activity.getRecipient().getName()); + String fromId = activity.getFrom().getId() != null ? activity.getFrom().getId() : ""; + properties.put(TelemetryConstants.FROMIDPROPERTY, fromId); + String conversationName = + activity.getConversation().getName() != null ? activity.getConversation().getName() : ""; + properties.put(TelemetryConstants.CONVERSATIONNAMEPROPERTY, conversationName); + String activityLocale = activity.getLocale() != null ? activity.getLocale() : ""; + properties.put(TelemetryConstants.LOCALEPROPERTY, activityLocale); + String recipientId = activity.getRecipient().getId() != null ? activity.getRecipient().getId() : ""; + properties.put(TelemetryConstants.RECIPIENTIDPROPERTY, recipientId); + String recipientName = activity.getRecipient().getName() != null ? activity.getRecipient().getName() : ""; + properties.put(TelemetryConstants.RECIPIENTNAMEPROPERTY, recipientName); // Use the LogPersonalInformation flag to toggle logging PII data, text and user // name are common examples diff --git a/samples/21.corebot-app-insights/LICENSE b/samples/21.corebot-app-insights/LICENSE new file mode 100644 index 000000000..21071075c --- /dev/null +++ b/samples/21.corebot-app-insights/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/samples/21.corebot-app-insights/README-LUIS.md b/samples/21.corebot-app-insights/README-LUIS.md new file mode 100644 index 000000000..60a0b8383 --- /dev/null +++ b/samples/21.corebot-app-insights/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" +} +``` diff --git a/samples/21.corebot-app-insights/README.md b/samples/21.corebot-app-insights/README.md new file mode 100644 index 000000000..3f93f6aeb --- /dev/null +++ b/samples/21.corebot-app-insights/README.md @@ -0,0 +1,70 @@ +# CoreBot with Application Insights + +Bot Framework v4 core bot sample. + +This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to: + +- Use [LUIS](https://www.luis.ai) to implement core AI capabilities +- Implement a multi-turn conversation using Dialogs +- Handle user interruptions for such things as `Help` or `Cancel` +- Prompt for and validate requests for information from the user +- Use [Application Insights](https://docs.microsoft.com/azure/azure-monitor/app/cloudservices) to monitor your bot + +## Prerequisites + +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 +and [Application Insights](https://docs.microsoft.com/azure/azure-monitor/app/cloudservices), an extensible Application Performance Management (APM) service for web developers on multiple platforms. + +### Create a LUIS Application to enable language understanding + +LUIS language model setup, training, and application configuration steps can be found [here](https://docs.microsoft.com/azure/bot-service/bot-builder-howto-v4-luis?view=azure-bot-service-4.0&tabs=cs). + +If you wish to create a LUIS application via the CLI, these steps can be found in the [README-LUIS.md](README-LUIS.md). + +### Add Application Insights service to enable the bot monitoring + +Application Insights resource creation steps can be found [here](https://docs.microsoft.com/azure/azure-monitor/app/create-new-resource). + +## To try this sample + +- From the root of this project folder: + - Build the sample using `mvn package` + - Run it by using `java -jar .\target\bot-corebot-app-insights-sample.jar` + +## 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 latest Bot Framework Emulator 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` + +## Deploy the bot to Azure + +To learn more about deploying a bot to Azure, see [Deploy your bot to Azure](https://aka.ms/azuredeployment) for a complete list of deployment instructions. + +## Further reading + +- [Bot Framework Documentation](https://docs.botframework.com) +- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) +- [Dialogs](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0) +- [Gathering Input Using Prompts](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-prompts?view=azure-bot-service-4.0&tabs=csharp) +- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) +- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) +- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) +- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest) +- [Azure Portal](https://portal.azure.com) +- [Language Understanding using LUIS](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/) +- [Channels and Bot Connector Service](https://docs.microsoft.com/en-us/azure/bot-service/bot-concepts?view=azure-bot-service-4.0) +- [Spring Boot](https://spring.io/projects/spring-boot) +- [Application insights Overview](https://docs.microsoft.com/azure/azure-monitor/app/app-insights-overview) +- [Getting Started with Application Insights](https://github.com/Microsoft/ApplicationInsights-aspnetcore/wiki/Getting-Started-with-Application-Insights-for-ASP.NET-Core) +- [Filtering and preprocessing telemetry in the Application Insights SDK](https://docs.microsoft.com/azure/azure-monitor/app/api-filtering-sampling) diff --git a/samples/21.corebot-app-insights/cognitiveModels/flightbooking.json b/samples/21.corebot-app-insights/cognitiveModels/flightbooking.json new file mode 100644 index 000000000..89aad31ae --- /dev/null +++ b/samples/21.corebot-app-insights/cognitiveModels/flightbooking.json @@ -0,0 +1,339 @@ +{ + "luis_schema_version": "3.2.0", + "versionId": "0.1", + "name": "FlightBooking", + "desc": "Luis Model for CoreBot", + "culture": "en-us", + "tokenizerVersion": "1.0.0", + "intents": [ + { + "name": "BookFlight" + }, + { + "name": "Cancel" + }, + { + "name": "GetWeather" + }, + { + "name": "None" + } + ], + "entities": [], + "composites": [ + { + "name": "From", + "children": [ + "Airport" + ], + "roles": [] + }, + { + "name": "To", + "children": [ + "Airport" + ], + "roles": [] + } + ], + "closedLists": [ + { + "name": "Airport", + "subLists": [ + { + "canonicalForm": "Paris", + "list": [ + "paris", + "cdg" + ] + }, + { + "canonicalForm": "London", + "list": [ + "london", + "lhr" + ] + }, + { + "canonicalForm": "Berlin", + "list": [ + "berlin", + "txl" + ] + }, + { + "canonicalForm": "New York", + "list": [ + "new york", + "jfk" + ] + }, + { + "canonicalForm": "Seattle", + "list": [ + "seattle", + "sea" + ] + } + ], + "roles": [] + } + ], + "patternAnyEntities": [], + "regex_entities": [], + "prebuiltEntities": [ + { + "name": "datetimeV2", + "roles": [] + } + ], + "model_features": [], + "regex_features": [], + "patterns": [], + "utterances": [ + { + "text": "book a flight", + "intent": "BookFlight", + "entities": [] + }, + { + "text": "book a flight from new york", + "intent": "BookFlight", + "entities": [ + { + "entity": "From", + "startPos": 19, + "endPos": 26 + } + ] + }, + { + "text": "book a flight from seattle", + "intent": "BookFlight", + "entities": [ + { + "entity": "From", + "startPos": 19, + "endPos": 25 + } + ] + }, + { + "text": "book a hotel in new york", + "intent": "None", + "entities": [] + }, + { + "text": "book a restaurant", + "intent": "None", + "entities": [] + }, + { + "text": "book flight from london to paris on feb 14th", + "intent": "BookFlight", + "entities": [ + { + "entity": "From", + "startPos": 17, + "endPos": 22 + }, + { + "entity": "To", + "startPos": 27, + "endPos": 31 + } + ] + }, + { + "text": "book flight to berlin on feb 14th", + "intent": "BookFlight", + "entities": [ + { + "entity": "To", + "startPos": 15, + "endPos": 20 + } + ] + }, + { + "text": "book me a flight from london to paris", + "intent": "BookFlight", + "entities": [ + { + "entity": "From", + "startPos": 22, + "endPos": 27 + }, + { + "entity": "To", + "startPos": 32, + "endPos": 36 + } + ] + }, + { + "text": "bye", + "intent": "Cancel", + "entities": [] + }, + { + "text": "cancel booking", + "intent": "Cancel", + "entities": [] + }, + { + "text": "exit", + "intent": "Cancel", + "entities": [] + }, + { + "text": "find an airport near me", + "intent": "None", + "entities": [] + }, + { + "text": "flight to paris", + "intent": "BookFlight", + "entities": [ + { + "entity": "To", + "startPos": 10, + "endPos": 14 + } + ] + }, + { + "text": "flight to paris from london on feb 14th", + "intent": "BookFlight", + "entities": [ + { + "entity": "To", + "startPos": 10, + "endPos": 14 + }, + { + "entity": "From", + "startPos": 21, + "endPos": 26 + } + ] + }, + { + "text": "fly from berlin to paris on may 5th", + "intent": "BookFlight", + "entities": [ + { + "entity": "From", + "startPos": 9, + "endPos": 14 + }, + { + "entity": "To", + "startPos": 19, + "endPos": 23 + } + ] + }, + { + "text": "go to paris", + "intent": "BookFlight", + "entities": [ + { + "entity": "To", + "startPos": 6, + "endPos": 10 + } + ] + }, + { + "text": "going from paris to berlin", + "intent": "BookFlight", + "entities": [ + { + "entity": "From", + "startPos": 11, + "endPos": 15 + }, + { + "entity": "To", + "startPos": 20, + "endPos": 25 + } + ] + }, + { + "text": "i'd like to rent a car", + "intent": "None", + "entities": [] + }, + { + "text": "ignore", + "intent": "Cancel", + "entities": [] + }, + { + "text": "travel from new york to paris", + "intent": "BookFlight", + "entities": [ + { + "entity": "From", + "startPos": 12, + "endPos": 19 + }, + { + "entity": "To", + "startPos": 24, + "endPos": 28 + } + ] + }, + { + "text": "travel to new york", + "intent": "BookFlight", + "entities": [ + { + "entity": "To", + "startPos": 10, + "endPos": 17 + } + ] + }, + { + "text": "travel to paris", + "intent": "BookFlight", + "entities": [ + { + "entity": "To", + "startPos": 10, + "endPos": 14 + } + ] + }, + { + "text": "what's the forecast for this friday?", + "intent": "GetWeather", + "entities": [] + }, + { + "text": "what's the weather like for tomorrow", + "intent": "GetWeather", + "entities": [] + }, + { + "text": "what's the weather like in new york", + "intent": "GetWeather", + "entities": [] + }, + { + "text": "what's the weather like?", + "intent": "GetWeather", + "entities": [] + }, + { + "text": "winter is coming", + "intent": "None", + "entities": [] + } + ], + "settings": [] +} diff --git a/samples/21.corebot-app-insights/deploymentTemplates/template-with-new-rg.json b/samples/21.corebot-app-insights/deploymentTemplates/template-with-new-rg.json new file mode 100644 index 000000000..ec2460d3a --- /dev/null +++ b/samples/21.corebot-app-insights/deploymentTemplates/template-with-new-rg.json @@ -0,0 +1,291 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "groupLocation": { + "defaultValue": "", + "type": "string", + "metadata": { + "description": "Specifies the location of the Resource Group." + } + }, + "groupName": { + "type": "string", + "metadata": { + "description": "Specifies the name of the Resource Group." + } + }, + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "defaultValue": "S1", + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "defaultValue": "", + "type": "string", + "metadata": { + "description": "The name of the App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "P1v2", + "tier": "PremiumV2", + "size": "P1v2", + "family": "Pv2", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "newAppServicePlanLocation": { + "defaultValue": "", + "type": "string", + "metadata": { + "description": "The location of the App Service Plan. Defaults to \"westus\"." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "appServicePlanName": "[parameters('newAppServicePlanName')]", + "resourcesLocation": "[parameters('newAppServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]", + "publishingUsername": "[concat('$', parameters('newWebAppName'))]", + "resourceGroupId": "[concat(subscription().id, '/resourceGroups/', parameters('groupName'))]" + }, + "resources": [ + { + "name": "[parameters('groupName')]", + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2018-05-01", + "location": "[parameters('groupLocation')]", + "properties": {} + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2018-05-01", + "name": "storageDeployment", + "resourceGroup": "[parameters('groupName')]", + "dependsOn": [ + "[resourceId('Microsoft.Resources/resourceGroups/', parameters('groupName'))]" + ], + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "variables": {}, + "resources": [ + { + "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "name": "[variables('appServicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "kind": "linux", + "properties": { + "perSiteScaling": false, + "maximumElasticWorkerCount": 1, + "isSpot": false, + "reserved": true, + "isXenon": false, + "hyperV": false, + "targetWorkerCount": 0, + "targetWorkerSizeId": 0 + } + }, + { + "comments": "Create a Web App using a Linux App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2018-11-01", + "location": "[variables('resourcesLocation')]", + "kind": "app,linux", + "dependsOn": [ + "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" + ], + "name": "[variables('webAppName')]", + "properties": { + "name": "[variables('webAppName')]", + "hostNameSslStates": [ + { + "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Standard" + }, + { + "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Repository" + } + ], + "serverFarmId": "[variables('appServicePlanName')]", + "reserved": true, + "isXenon": false, + "hyperV": false, + "scmSiteAlsoStopped": false, + "clientAffinityEnabled": true, + "clientCertEnabled": false, + "hostNamesDisabled": false, + "containerSize": 0, + "dailyMemoryTimeQuota": 0, + "httpsOnly": false, + "redundancyMode": "None", + "siteConfig": { + "appSettings": [ + { + "name": "JAVA_OPTS", + "value": "-Dserver.port=80" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2018-11-01", + "name": "[concat(variables('webAppName'), '/web')]", + "location": "[variables('resourcesLocation')]", + "dependsOn": [ + "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/sites/', variables('webAppName'))]" + ], + "properties": { + "numberOfWorkers": 1, + "defaultDocuments": [ + "Default.htm", + "Default.html", + "Default.asp", + "index.htm", + "index.html", + "iisstart.htm", + "default.aspx", + "index.php", + "hostingstart.html" + ], + "netFrameworkVersion": "v4.0", + "linuxFxVersion": "JAVA|8-jre8", + "requestTracingEnabled": false, + "remoteDebuggingEnabled": false, + "httpLoggingEnabled": false, + "logsDirectorySizeLimit": 35, + "detailedErrorLoggingEnabled": false, + "publishingUsername": "[variables('publishingUsername')]", + "scmType": "None", + "use32BitWorkerProcess": true, + "webSocketsEnabled": false, + "alwaysOn": true, + "managedPipelineMode": "Integrated", + "virtualApplications": [ + { + "virtualPath": "/", + "physicalPath": "site\\wwwroot", + "preloadEnabled": true + } + ], + "loadBalancing": "LeastRequests", + "experiments": { + "rampUpRules": [] + }, + "autoHealEnabled": false, + "localMySqlEnabled": false, + "ipSecurityRestrictions": [ + { + "ipAddress": "Any", + "action": "Allow", + "priority": 1, + "name": "Allow all", + "description": "Allow all access" + } + ], + "scmIpSecurityRestrictions": [ + { + "ipAddress": "Any", + "action": "Allow", + "priority": 1, + "name": "Allow all", + "description": "Allow all access" + } + ], + "scmIpSecurityRestrictionsUseMain": false, + "http20Enabled": false, + "minTlsVersion": "1.2", + "ftpsState": "AllAllowed", + "reservedInstanceCount": 0 + } + }, + { + "apiVersion": "2017-12-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "bot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "developerAppInsightsApplicationId": null, + "developerAppInsightKey": null, + "publishingCredentials": null, + "storageResourceId": null + }, + "dependsOn": [ + "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ], + "outputs": {} + } + } + } + ] +} diff --git a/samples/21.corebot-app-insights/deploymentTemplates/template-with-preexisting-rg.json b/samples/21.corebot-app-insights/deploymentTemplates/template-with-preexisting-rg.json new file mode 100644 index 000000000..024dcf08d --- /dev/null +++ b/samples/21.corebot-app-insights/deploymentTemplates/template-with-preexisting-rg.json @@ -0,0 +1,259 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Defaults to \"\"." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "defaultValue": "S1", + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The name of the new App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "P1v2", + "tier": "PremiumV2", + "size": "P1v2", + "family": "Pv2", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "appServicePlanLocation": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The location of the App Service Plan." + } + }, + "existingAppServicePlan": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the existing App Service Plan used to create the Web App for the bot." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", + "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", + "publishingUsername": "[concat('$', parameters('newWebAppName'))]", + "resourcesLocation": "[parameters('appServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" + }, + "resources": [ + { + "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "condition": "[not(variables('useExistingAppServicePlan'))]", + "name": "[variables('servicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "kind": "linux", + "properties": { + "perSiteScaling": false, + "maximumElasticWorkerCount": 1, + "isSpot": false, + "reserved": true, + "isXenon": false, + "hyperV": false, + "targetWorkerCount": 0, + "targetWorkerSizeId": 0 + } + }, + { + "comments": "Create a Web App using a Linux App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2018-11-01", + "location": "[variables('resourcesLocation')]", + "kind": "app,linux", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]" + ], + "name": "[variables('webAppName')]", + "properties": { + "name": "[variables('webAppName')]", + "hostNameSslStates": [ + { + "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Standard" + }, + { + "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Repository" + } + ], + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]", + "reserved": true, + "isXenon": false, + "hyperV": false, + "scmSiteAlsoStopped": false, + "clientAffinityEnabled": true, + "clientCertEnabled": false, + "hostNamesDisabled": false, + "containerSize": 0, + "dailyMemoryTimeQuota": 0, + "httpsOnly": false, + "redundancyMode": "None", + "siteConfig": { + "appSettings": [ + { + "name": "JAVA_OPTS", + "value": "-Dserver.port=80" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2018-11-01", + "name": "[concat(variables('webAppName'), '/web')]", + "location": "[variables('resourcesLocation')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ], + "properties": { + "numberOfWorkers": 1, + "defaultDocuments": [ + "Default.htm", + "Default.html", + "Default.asp", + "index.htm", + "index.html", + "iisstart.htm", + "default.aspx", + "index.php", + "hostingstart.html" + ], + "netFrameworkVersion": "v4.0", + "linuxFxVersion": "JAVA|8-jre8", + "requestTracingEnabled": false, + "remoteDebuggingEnabled": false, + "httpLoggingEnabled": false, + "logsDirectorySizeLimit": 35, + "detailedErrorLoggingEnabled": false, + "publishingUsername": "[variables('publishingUsername')]", + "scmType": "None", + "use32BitWorkerProcess": true, + "webSocketsEnabled": false, + "alwaysOn": true, + "managedPipelineMode": "Integrated", + "virtualApplications": [ + { + "virtualPath": "/", + "physicalPath": "site\\wwwroot", + "preloadEnabled": true + } + ], + "loadBalancing": "LeastRequests", + "experiments": { + "rampUpRules": [] + }, + "autoHealEnabled": false, + "localMySqlEnabled": false, + "ipSecurityRestrictions": [ + { + "ipAddress": "Any", + "action": "Allow", + "priority": 1, + "name": "Allow all", + "description": "Allow all access" + } + ], + "scmIpSecurityRestrictions": [ + { + "ipAddress": "Any", + "action": "Allow", + "priority": 1, + "name": "Allow all", + "description": "Allow all access" + } + ], + "scmIpSecurityRestrictionsUseMain": false, + "http20Enabled": false, + "minTlsVersion": "1.2", + "ftpsState": "AllAllowed", + "reservedInstanceCount": 0 + } + }, + { + "apiVersion": "2017-12-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "bot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "developerAppInsightsApplicationId": null, + "developerAppInsightKey": null, + "publishingCredentials": null, + "storageResourceId": null + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ] +} diff --git a/samples/21.corebot-app-insights/pom.xml b/samples/21.corebot-app-insights/pom.xml new file mode 100644 index 000000000..ea30a54e4 --- /dev/null +++ b/samples/21.corebot-app-insights/pom.xml @@ -0,0 +1,253 @@ + + + + 4.0.0 + + com.microsoft.bot.sample + bot-corebot-app-insights + sample + jar + + ${project.groupId}:${project.artifactId} + This package contains a Java Core Bot sample with Application Insights using Spring Boot. + http://maven.apache.org + + + org.springframework.boot + spring-boot-starter-parent + 2.4.0 + + + + + + MIT License + http://www.opensource.org/licenses/mit-license.php + + + + + + Bot Framework Development + + Microsoft + https://dev.botframework.com/ + + + + + 1.8 + 1.8 + 1.8 + com.microsoft.bot.sample.corebot.app.insights.Application + + + + + org.springframework.boot + spring-boot-starter-test + 2.4.0 + test + + + junit + junit + 4.13.1 + test + + + org.junit.vintage + junit-vintage-engine + test + + + + org.slf4j + slf4j-api + + + org.apache.logging.log4j + log4j-api + 2.11.0 + + + org.apache.logging.log4j + log4j-core + 2.13.2 + + + + com.microsoft.bot + bot-integration-spring + 4.6.0-preview9 + compile + + + com.microsoft.bot + bot-dialogs + 4.6.0-preview9 + + + com.microsoft.bot + bot-ai-luis-v3 + 4.6.0-preview9 + + + com.microsoft.bot + bot-applicationinsights + 4.6.0-preview9 + + + + + + build + + true + + + + + src/main/resources + false + + + + + maven-compiler-plugin + 3.8.1 + + + maven-war-plugin + 3.2.3 + + src/main/webapp + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + com.microsoft.bot.sample.corebot.app.insights.Application + + + + + + com.microsoft.azure + azure-webapp-maven-plugin + 1.12.0 + + V2 + ${groupname} + ${botname} + + + JAVA_OPTS + -Dserver.port=80 + + + + linux + Java 8 + Java SE + + + + + ${project.basedir}/target + + *.jar + + + + + + + + + + + + publish + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + maven-war-plugin + 3.2.3 + + src/main/webapp + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + true + + true + ossrh + https://oss.sonatype.org/ + true + + + + + org.apache.maven.plugins + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + 8 + false + + + + attach-javadocs + + jar + + + + + + + + + diff --git a/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/AdapterWithErrorHandler.java b/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/AdapterWithErrorHandler.java new file mode 100644 index 000000000..fb82e8328 --- /dev/null +++ b/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/AdapterWithErrorHandler.java @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.corebot.app.insights; + +import com.microsoft.bot.applicationinsights.core.TelemetryInitializerMiddleware; +import com.microsoft.bot.builder.BotTelemetryClient; +import com.microsoft.bot.builder.ConversationState; +import com.microsoft.bot.builder.MessageFactory; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.connector.Channels; +import com.microsoft.bot.integration.BotFrameworkHttpAdapter; +import com.microsoft.bot.integration.Configuration; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * An Adapter that provides exception handling. + */ +public class AdapterWithErrorHandler extends BotFrameworkHttpAdapter { + + private static final String ERROR_MSG_ONE = "The bot encountered an error or bug."; + private static final String ERROR_MSG_TWO = + "To continue to run this bot, please fix the bot source code."; + // Create field for telemetry client. Add IBotTelemetryClient parameter to AdapterWithErrorHandler + private BotTelemetryClient adapterBotTelemetryClient; + + /** + * Constructs an error handling BotFrameworkHttpAdapter by providing an + * {@link com.microsoft.bot.builder.OnTurnErrorHandler}. + * + *

+ * For this sample, a simple message is displayed. For a production Bot, a more + * informative message or action is likely preferred. + *

+ * + * @param withConfiguration The Configuration object to use. + * @param telemetryInitializerMiddleware The TelemetryInitializerMiddleware object to use. + * @param botTelemetryClient The BotTelemetryClient object to use. + * @param withConversationState The ConversationState object to use. + */ + public AdapterWithErrorHandler( + Configuration withConfiguration, + TelemetryInitializerMiddleware telemetryInitializerMiddleware, + BotTelemetryClient botTelemetryClient, + @Nullable ConversationState withConversationState) { + super(withConfiguration); + this.use(telemetryInitializerMiddleware); + + // Use telemetry client so that we can trace exceptions into Application Insights + this.adapterBotTelemetryClient = botTelemetryClient; + setOnTurnError((turnContext, exception) -> { + // Track exceptions into Application Insights + // Set up some properties for our exception tracing to give more information + Map properties = new HashMap(); + properties.put("Bot exception caught in", "AdapterWithErrorHandler - OnTurnError"); + + // Send the exception telemetry: + adapterBotTelemetryClient.trackException((Exception) exception, properties, null); + + // Log any leaked exception from the application. + // NOTE: In production environment, you should consider logging this to + // Azure Application Insights. Visit https://aka.ms/bottelemetry to see how + // to add telemetry capture to your bot. + LoggerFactory.getLogger(AdapterWithErrorHandler.class).error("onTurnError", exception); + + // Send a message to the user + return turnContext.sendActivities( + MessageFactory.text(ERROR_MSG_ONE), MessageFactory.text(ERROR_MSG_TWO) + ).thenCompose(resourceResponse -> sendTraceActivity(turnContext, exception)) + .thenCompose(stageResult -> { + if (withConversationState != null) { + // Delete the conversationState for the current conversation to prevent the + // bot from getting stuck in a error-loop caused by being in a bad state. + // ConversationState should be thought of as similar to "cookie-state" in a + // Web pages. + return withConversationState.delete(turnContext) + .exceptionally(deleteException -> { + LoggerFactory.getLogger(AdapterWithErrorHandler.class) + .error("ConversationState.delete", deleteException); + return null; + }); + } + return CompletableFuture.completedFuture(null); + }); + }); + } + + private CompletableFuture sendTraceActivity( + TurnContext turnContext, + Throwable exception + ) { + if (StringUtils.equals(turnContext.getActivity().getChannelId(), Channels.EMULATOR)) { + Activity traceActivity = new Activity(ActivityTypes.TRACE); + traceActivity.setLabel("TurnError"); + traceActivity.setName("OnTurnError Trace"); + traceActivity.setValue(ExceptionUtils.getStackTrace(exception)); + traceActivity.setValueType("https://www.botframework.com/schemas/error"); + + // Send a trace activity, which will be displayed in the Bot Framework Emulator + return turnContext.sendActivity(traceActivity).thenApply(resourceResponse -> null); + } + + return CompletableFuture.completedFuture(null); + } +} diff --git a/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/Application.java b/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/Application.java new file mode 100644 index 000000000..6cc9570e7 --- /dev/null +++ b/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/Application.java @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.corebot.app.insights; + +import com.microsoft.bot.applicationinsights.ApplicationInsightsBotTelemetryClient; +import com.microsoft.bot.applicationinsights.core.TelemetryInitializerMiddleware; +import com.microsoft.bot.builder.Bot; +import com.microsoft.bot.builder.BotTelemetryClient; +import com.microsoft.bot.builder.ConversationState; +import com.microsoft.bot.builder.NullBotTelemetryClient; +import com.microsoft.bot.builder.Storage; +import com.microsoft.bot.builder.TelemetryLoggerMiddleware; +import com.microsoft.bot.builder.UserState; +import com.microsoft.bot.integration.BotFrameworkHttpAdapter; +import com.microsoft.bot.integration.Configuration; +import com.microsoft.bot.integration.spring.BotController; +import com.microsoft.bot.integration.spring.BotDependencyConfiguration; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +/** + * This is the starting point of the Sprint Boot Bot application. + */ +@SpringBootApplication + +// Use the default BotController to receive incoming Channel messages. A custom +// controller could be used by eliminating this import and creating a new +// org.springframework.web.bind.annotation.RestController. +// The default controller is created by the Spring Boot container using +// dependency injection. The default route is /api/messages. +@Import({BotController.class}) + +/** + * This class extends the BotDependencyConfiguration which provides the default + * implementations for a Bot application. The Application class should + * override methods in order to provide custom implementations. + */ +public class Application extends BotDependencyConfiguration { + + /** + * The start method. + * + * @param args The args. + */ + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + /** + * Returns the Bot for this application. + * + *

+ * The @Component annotation could be used on the Bot class instead of this method with the + * @Bean annotation. + *

+ * + * @param configuration The Configuration object to use. + * @param userState The UserState object to use. + * @param conversationState The ConversationState object to use. + * @return The Bot implementation for this application. + */ + @Bean + public Bot getBot( + Configuration configuration, + UserState userState, + ConversationState conversationState + ) { + BotTelemetryClient botTelemetryClient = getBotTelemetryClient(configuration); + FlightBookingRecognizer recognizer = new FlightBookingRecognizer(configuration, botTelemetryClient); + MainDialog dialog = new MainDialog(recognizer, new BookingDialog(), botTelemetryClient); + + return new DialogAndWelcomeBot<>(conversationState, userState, dialog); + } + + /** + * Returns a custom Adapter that provides error handling. + * + * @param configuration The Configuration object to use. + * @return An error handling BotFrameworkHttpAdapter. + */ + @Override + public BotFrameworkHttpAdapter getBotFrameworkHttpAdaptor(Configuration configuration) { + Storage storage = getStorage(); + ConversationState conversationState = getConversationState(storage); + BotTelemetryClient botTelemetryClient = getBotTelemetryClient(configuration); + TelemetryLoggerMiddleware telemetryLoggerMiddleware = new TelemetryLoggerMiddleware(botTelemetryClient, false); + TelemetryInitializerMiddleware telemetryInitializerMiddleware = + new TelemetryInitializerMiddleware(telemetryLoggerMiddleware, false); + + AdapterWithErrorHandler adapter = new AdapterWithErrorHandler( + configuration, + telemetryInitializerMiddleware, + botTelemetryClient, + conversationState); + + return adapter; + } + + /** + * Returns a Bot Telemetry Client. + * + * @param configuration The Configuration object to use. + * @return A Bot Telemetry Client. + */ + @Bean + public BotTelemetryClient getBotTelemetryClient(Configuration configuration) { + String instrumentationKey = configuration.getProperty("ApplicationInsights.InstrumentationKey"); + if (StringUtils.isNotBlank(instrumentationKey)) { + return new ApplicationInsightsBotTelemetryClient(instrumentationKey); + } + + return new NullBotTelemetryClient(); + } +} diff --git a/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/BookingDetails.java b/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/BookingDetails.java new file mode 100644 index 000000000..a4bd9b202 --- /dev/null +++ b/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/BookingDetails.java @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.corebot.app.insights; + +/** + * The model class to retrieve the information of the booking. + */ +public class BookingDetails { + private String destination; + private String origin; + private String travelDate; + + /** + * Gets the destination of the booking. + * + * @return The destination. + */ + public String getDestination() { + return destination; + } + + /** + * Sets the destination of the booking. + * + * @param withDestination The new destination. + */ + public void setDestination(String withDestination) { + destination = withDestination; + } + + /** + * Gets the origin of the booking. + * + * @return The origin. + */ + public String getOrigin() { + return origin; + } + + /** + * Sets the origin of the booking. + * + * @param withOrigin The new origin. + */ + public void setOrigin(String withOrigin) { + origin = withOrigin; + } + + /** + * Gets the travel date of the booking. + * + * @return The travel date. + */ + public String getTravelDate() { + return travelDate; + } + + /** + * Sets the travel date of the booking. + * + * @param withTravelDate The new travel date. + */ + public void setTravelDate(String withTravelDate) { + travelDate = withTravelDate; + } +} diff --git a/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/BookingDialog.java b/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/BookingDialog.java new file mode 100644 index 000000000..92dc7e1db --- /dev/null +++ b/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/BookingDialog.java @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.corebot.app.insights; + +import com.microsoft.bot.builder.MessageFactory; +import com.microsoft.bot.dialogs.DialogTurnResult; +import com.microsoft.bot.dialogs.WaterfallDialog; +import com.microsoft.bot.dialogs.WaterfallStep; +import com.microsoft.bot.dialogs.WaterfallStepContext; +import com.microsoft.bot.dialogs.prompts.ConfirmPrompt; +import com.microsoft.bot.dialogs.prompts.PromptOptions; +import com.microsoft.bot.dialogs.prompts.TextPrompt; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.InputHints; +import com.microsoft.recognizers.datatypes.timex.expression.Constants; +import com.microsoft.recognizers.datatypes.timex.expression.TimexProperty; + +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; + +/** + * The class containing the booking dialogs. + */ +public class BookingDialog extends CancelAndHelpDialog { + private final String destinationStepMsgText = "Where would you like to travel to?"; + private final String originStepMsgText = "Where are you traveling from?"; + + /** + * The constructor of the Booking Dialog class. + */ + public BookingDialog() { + super("BookingDialog"); + + addDialog(new TextPrompt("TextPrompt")); + addDialog(new ConfirmPrompt("ConfirmPrompt")); + addDialog(new DateResolverDialog(null)); + WaterfallStep[] waterfallSteps = { + this::destinationStep, + this::originStep, + this::travelDateStep, + this::confirmStep, + this::finalStep + }; + + addDialog(new WaterfallDialog("WaterfallDialog", Arrays.asList(waterfallSteps))); + + // The initial child Dialog to run. + setInitialDialogId("WaterfallDialog"); + } + + private CompletableFuture destinationStep(WaterfallStepContext stepContext) { + BookingDetails bookingDetails = (BookingDetails) stepContext.getOptions(); + if (bookingDetails.getDestination().isEmpty()) { + Activity promptMessage = + MessageFactory.text(destinationStepMsgText, destinationStepMsgText, + InputHints.EXPECTING_INPUT + ); + PromptOptions promptOptions = new PromptOptions(); + promptOptions.setPrompt(promptMessage); + return stepContext.prompt("TextPrompt", promptOptions); + } + + return stepContext.next(bookingDetails.getDestination()); + } + + private CompletableFuture originStep(WaterfallStepContext stepContext) { + BookingDetails bookingDetails = (BookingDetails) stepContext.getOptions(); + + bookingDetails.setDestination((String) stepContext.getResult()); + + if (bookingDetails.getOrigin().isEmpty()) { + Activity promptMessage = + MessageFactory + .text(originStepMsgText, originStepMsgText, InputHints.EXPECTING_INPUT); + PromptOptions promptOptions = new PromptOptions(); + promptOptions.setPrompt(promptMessage); + return stepContext.prompt("TextPrompt", promptOptions); + } + + return stepContext.next(bookingDetails.getOrigin()); + } + + private CompletableFuture travelDateStep(WaterfallStepContext stepContext) { + BookingDetails bookingDetails = (BookingDetails) stepContext.getOptions(); + + bookingDetails.setOrigin((String) stepContext.getResult()); + + if (bookingDetails.getTravelDate() == null || isAmbiguous(bookingDetails.getTravelDate())) { + return stepContext.beginDialog("DateResolverDialog", bookingDetails.getTravelDate()); + } + + return stepContext.next(bookingDetails.getTravelDate()); + } + + private CompletableFuture confirmStep(WaterfallStepContext stepContext) { + BookingDetails bookingDetails = (BookingDetails) stepContext.getOptions(); + + bookingDetails.setTravelDate((String) stepContext.getResult()); + + String messageText = + String.format( + "Please confirm, I have you traveling to: %s from: %s on: %s. Is this correct?", + bookingDetails.getDestination(), bookingDetails.getOrigin(), + bookingDetails.getTravelDate() + ); + Activity promptMessage = MessageFactory + .text(messageText, messageText, InputHints.EXPECTING_INPUT); + + PromptOptions promptOptions = new PromptOptions(); + promptOptions.setPrompt(promptMessage); + + return stepContext.prompt("ConfirmPrompt", promptOptions); + } + + private CompletableFuture finalStep(WaterfallStepContext stepContext) { + if ((Boolean) stepContext.getResult()) { + BookingDetails bookingDetails = (BookingDetails) stepContext.getOptions(); + return stepContext.endDialog(bookingDetails); + } + + return stepContext.endDialog(null); + } + + private static boolean isAmbiguous(String timex) { + TimexProperty timexProperty = new TimexProperty(timex); + return !timexProperty.getTypes().contains(Constants.TimexTypes.DEFINITE); + } +} diff --git a/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/CancelAndHelpDialog.java b/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/CancelAndHelpDialog.java new file mode 100644 index 000000000..db4c1675b --- /dev/null +++ b/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/CancelAndHelpDialog.java @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.corebot.app.insights; + +import com.microsoft.bot.builder.MessageFactory; +import com.microsoft.bot.dialogs.ComponentDialog; +import com.microsoft.bot.dialogs.DialogContext; +import com.microsoft.bot.dialogs.DialogTurnResult; +import com.microsoft.bot.dialogs.DialogTurnStatus; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.ActivityTypes; +import com.microsoft.bot.schema.InputHints; + +import java.util.concurrent.CompletableFuture; + +/** + * The class in charge of the dialog interruptions. + */ +public class CancelAndHelpDialog extends ComponentDialog { + + private final String helpMsgText = "Show help here"; + private final String cancelMsgText = "Cancelling..."; + + /** + * The constructor of the CancelAndHelpDialog class. + * + * @param id The dialog's Id. + */ + public CancelAndHelpDialog(String id) { + super(id); + } + + /** + * Called when the dialog is _continued_, where it is the active dialog and the user replies + * with a new activity. + * + * @param innerDc innerDc The inner {@link DialogContext} for the current turn of conversation. + * @return A {@link CompletableFuture} representing the asynchronous operation. If the task is + * successful, the result indicates whether the dialog is still active after the turn has been + * processed by the dialog. The result may also contain a return value. + */ + @Override + protected CompletableFuture onContinueDialog(DialogContext innerDc) { + return interrupt(innerDc).thenCompose(result -> { + if (result != null) { + return CompletableFuture.completedFuture(result); + } + return super.onContinueDialog(innerDc); + }); + } + + private CompletableFuture interrupt(DialogContext innerDc) { + if (innerDc.getContext().getActivity().isType(ActivityTypes.MESSAGE)) { + String text = innerDc.getContext().getActivity().getText().toLowerCase(); + + switch (text) { + case "help": + case "?": + Activity helpMessage = MessageFactory + .text(helpMsgText, helpMsgText, InputHints.EXPECTING_INPUT); + return innerDc.getContext().sendActivity(helpMessage) + .thenCompose(sendResult -> + CompletableFuture + .completedFuture(new DialogTurnResult(DialogTurnStatus.WAITING))); + case "cancel": + case "quit": + Activity cancelMessage = MessageFactory + .text(cancelMsgText, cancelMsgText, InputHints.IGNORING_INPUT); + return innerDc.getContext() + .sendActivity(cancelMessage) + .thenCompose(sendResult -> innerDc.cancelAllDialogs()); + default: + break; + } + } + + return CompletableFuture.completedFuture(null); + } +} diff --git a/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/DateResolverDialog.java b/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/DateResolverDialog.java new file mode 100644 index 000000000..cde5d96a0 --- /dev/null +++ b/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/DateResolverDialog.java @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.corebot.app.insights; + +import com.microsoft.bot.builder.MessageFactory; +import com.microsoft.bot.dialogs.DialogTurnResult; +import com.microsoft.bot.dialogs.WaterfallDialog; +import com.microsoft.bot.dialogs.WaterfallStep; +import com.microsoft.bot.dialogs.WaterfallStepContext; +import com.microsoft.bot.dialogs.prompts.DateTimePrompt; +import com.microsoft.bot.dialogs.prompts.DateTimeResolution; +import com.microsoft.bot.dialogs.prompts.PromptOptions; +import com.microsoft.bot.dialogs.prompts.PromptValidatorContext; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.InputHints; +import com.microsoft.recognizers.datatypes.timex.expression.Constants; +import com.microsoft.recognizers.datatypes.timex.expression.TimexProperty; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * The class containing the date resolver dialogs. + */ +public class DateResolverDialog extends CancelAndHelpDialog { + private final String promptMsgText = "When would you like to travel?"; + private final String repromptMsgText = + "I'm sorry, to make your booking please enter a full travel date including Day Month and Year."; + + /** + * The constructor of the DateResolverDialog class. + * @param id The dialog's id. + */ + public DateResolverDialog(@Nullable String id) { + super(id != null ? id : "DateResolverDialog"); + + addDialog(new DateTimePrompt("DateTimePrompt", + DateResolverDialog::dateTimePromptValidator, null)); + WaterfallStep[] waterfallSteps = { + this::initialStep, + this::finalStep + }; + addDialog(new WaterfallDialog("WaterfallDialog", Arrays.asList(waterfallSteps))); + + // The initial child Dialog to run. + setInitialDialogId("WaterfallDialog"); + } + + private CompletableFuture initialStep(WaterfallStepContext stepContext) { + String timex = (String) stepContext.getOptions(); + + Activity promptMessage = MessageFactory.text(promptMsgText, promptMsgText, InputHints.EXPECTING_INPUT); + Activity repromptMessage = MessageFactory.text(repromptMsgText, repromptMsgText, InputHints.EXPECTING_INPUT); + + if (timex == null) { + // We were not given any date at all so prompt the user. + PromptOptions promptOptions = new PromptOptions(); + promptOptions.setPrompt(promptMessage); + promptOptions.setRetryPrompt(repromptMessage); + return stepContext.prompt("DateTimePrompt", promptOptions); + } + + // We have a Date we just need to check it is unambiguous. + TimexProperty timexProperty = new TimexProperty(timex); + if (!timexProperty.getTypes().contains(Constants.TimexTypes.DEFINITE)) { + // This is essentially a "reprompt" of the data we were given up front. + PromptOptions promptOptions = new PromptOptions(); + promptOptions.setPrompt(repromptMessage); + return stepContext.prompt("DateTimePrompt", promptOptions); + } + + DateTimeResolution dateTimeResolution = new DateTimeResolution(); + dateTimeResolution.setTimex(timex); + List dateTimeResolutions = new ArrayList(); + dateTimeResolutions.add(dateTimeResolution); + return stepContext.next(dateTimeResolutions); + } + + private CompletableFuture finalStep(WaterfallStepContext stepContext) { + String timex = ((ArrayList) stepContext.getResult()).get(0).getTimex(); + return stepContext.endDialog(timex); + } + + private static CompletableFuture dateTimePromptValidator( + PromptValidatorContext> promptContext + ) { + if (promptContext.getRecognized().getSucceeded()) { + // 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. + String timex = ((List) promptContext.getRecognized().getValue()) + .get(0).getTimex().split("T")[0]; + + // If this is a definite Date including year, month and day we are good otherwise reprompt. + // A better solution might be to let the user know what part is actually missing. + Boolean isDefinite = new TimexProperty(timex).getTypes().contains(Constants.TimexTypes.DEFINITE); + + return CompletableFuture.completedFuture(isDefinite); + } + + return CompletableFuture.completedFuture(false); + } +} diff --git a/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/DialogAndWelcomeBot.java b/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/DialogAndWelcomeBot.java new file mode 100644 index 000000000..fd3bf4ce4 --- /dev/null +++ b/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/DialogAndWelcomeBot.java @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.corebot.app.insights; + +import com.codepoetics.protonpack.collectors.CompletableFutures; +import com.microsoft.applicationinsights.core.dependencies.apachecommons.io.IOUtils; +import com.microsoft.bot.builder.ConversationState; +import com.microsoft.bot.builder.MessageFactory; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.UserState; +import com.microsoft.bot.dialogs.Dialog; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.Attachment; +import com.microsoft.bot.schema.ChannelAccount; +import com.microsoft.bot.schema.Serialization; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * The class containing the welcome dialog. + * + * @param is a Dialog. + */ +public class DialogAndWelcomeBot extends DialogBot { + + /** + * Creates a DialogBot. + * + * @param withConversationState ConversationState to use in the bot + * @param withUserState UserState to use + * @param withDialog Param inheriting from Dialog class + */ + public DialogAndWelcomeBot( + ConversationState withConversationState, UserState withUserState, T withDialog + ) { + super(withConversationState, withUserState, withDialog); + } + + /** + * When the {@link #onConversationUpdateActivity(TurnContext)} method receives a conversation + * update activity that indicates one or more users other than the bot are joining the + * conversation, it calls this method. + * + * @param membersAdded A list of all the members added to the conversation, as described by the + * conversation update activity + * @param turnContext The context object for this turn. + * @return A task that represents the work queued to execute. + */ + @Override + protected CompletableFuture onMembersAdded( + List membersAdded, TurnContext turnContext + ) { + return turnContext.getActivity().getMembersAdded().stream() + .filter(member -> !StringUtils + .equals(member.getId(), turnContext.getActivity().getRecipient().getId())) + .map(channel -> { + // 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. + Attachment welcomeCard = createAdaptiveCardAttachment(); + Activity response = MessageFactory + .attachment(welcomeCard, null, "Welcome to Bot Framework!", null); + + return turnContext.sendActivity(response).thenApply(sendResult -> { + return Dialog.run(getDialog(), turnContext, + getConversationState().createProperty("DialogState") + ); + }); + }) + .collect(CompletableFutures.toFutureList()) + .thenApply(resourceResponse -> null); + } + + // Load attachment from embedded resource. + private Attachment createAdaptiveCardAttachment() { + try ( + InputStream inputStream = Thread.currentThread(). + getContextClassLoader().getResourceAsStream("cards/welcomeCard.json") + ) { + String adaptiveCardJson = IOUtils + .toString(inputStream, StandardCharsets.UTF_8.toString()); + + Attachment attachment = new Attachment(); + attachment.setContentType("application/vnd.microsoft.card.adaptive"); + attachment.setContent(Serialization.jsonToTree(adaptiveCardJson)); + return attachment; + + } catch (IOException e) { + e.printStackTrace(); + return new Attachment(); + } + } +} diff --git a/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/DialogBot.java b/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/DialogBot.java new file mode 100644 index 000000000..5fe2be5d0 --- /dev/null +++ b/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/DialogBot.java @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.corebot.app.insights; + +import com.microsoft.bot.builder.ActivityHandler; +import com.microsoft.bot.builder.BotState; +import com.microsoft.bot.builder.ConversationState; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.UserState; +import com.microsoft.bot.dialogs.Dialog; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.CompletableFuture; + +/** + * This Bot implementation can run any type of Dialog. The use of type parameterization is to allow + * multiple different bots to be run at different endpoints within the same project. This can be + * achieved by defining distinct Controller types each with dependency on distinct Bot types. The + * ConversationState is used by the Dialog system. The UserState isn't, however, it might have been + * used in a Dialog implementation, and the requirement is that all BotState objects are saved at + * the end of a turn. + * + * @param parameter of a type inheriting from Dialog + */ +public class DialogBot extends ActivityHandler { + + private Dialog dialog; + private BotState conversationState; + private BotState userState; + + /** + * Gets the dialog in use. + * + * @return instance of dialog + */ + protected Dialog getDialog() { + return dialog; + } + + /** + * Gets the conversation state. + * + * @return instance of conversationState + */ + protected BotState getConversationState() { + return conversationState; + } + + /** + * Gets the user state. + * + * @return instance of userState + */ + protected BotState getUserState() { + return userState; + } + + /** + * Sets the dialog in use. + * + * @param withDialog the dialog (of Dialog type) to be set + */ + protected void setDialog(Dialog withDialog) { + dialog = withDialog; + } + + /** + * Sets the conversation state. + * + * @param withConversationState the conversationState (of BotState type) to be set + */ + protected void setConversationState(BotState withConversationState) { + conversationState = withConversationState; + } + + /** + * Sets the user state. + * + * @param withUserState the userState (of BotState type) to be set + */ + protected void setUserState(BotState withUserState) { + userState = withUserState; + } + + /** + * Creates a DialogBot. + * + * @param withConversationState ConversationState to use in the bot + * @param withUserState UserState to use + * @param withDialog Param inheriting from Dialog class + */ + public DialogBot( + ConversationState withConversationState, UserState withUserState, T withDialog + ) { + this.conversationState = withConversationState; + this.userState = withUserState; + this.dialog = withDialog; + } + + /** + * Saves the BotState objects at the end of each turn. + * + * @param turnContext + * @return + */ + @Override + public CompletableFuture onTurn(TurnContext turnContext) { + return super.onTurn(turnContext) + .thenCompose(turnResult -> conversationState.saveChanges(turnContext, false)) + .thenCompose(saveResult -> userState.saveChanges(turnContext, false)); + } + + /** + * This method is executed when the turnContext receives a message activity. + * + * @param turnContext + * @return + */ + @Override + protected CompletableFuture onMessageActivity(TurnContext turnContext) { + LoggerFactory.getLogger(DialogBot.class).info("Running dialog with Message Activity."); + + // Run the Dialog with the new message Activity. + return Dialog.run(dialog, turnContext, conversationState.createProperty("DialogState")); + } +} diff --git a/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/FlightBookingRecognizer.java b/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/FlightBookingRecognizer.java new file mode 100644 index 000000000..0693ab236 --- /dev/null +++ b/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/FlightBookingRecognizer.java @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.corebot.app.insights; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.apache.commons.lang3.StringUtils; +import com.microsoft.bot.ai.luis.LuisApplication; +import com.microsoft.bot.ai.luis.LuisRecognizer; +import com.microsoft.bot.ai.luis.LuisRecognizerOptionsV3; +import com.microsoft.bot.builder.BotTelemetryClient; +import com.microsoft.bot.builder.Recognizer; +import com.microsoft.bot.builder.RecognizerResult; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.integration.Configuration; + +import java.util.concurrent.CompletableFuture; + +/** + * The class in charge of recognizing the booking information. + */ +public class FlightBookingRecognizer implements Recognizer { + private LuisRecognizer recognizer = null; + + /** + * The constructor of the FlightBookingRecognizer class. + * + * @param configuration The Configuration object to use. + * @param telemetryClient The BotTelemetryClient to use. + */ + public FlightBookingRecognizer(Configuration configuration, BotTelemetryClient telemetryClient) { + Boolean luisIsConfigured = StringUtils.isNotBlank(configuration.getProperty("LuisAppId")) + && StringUtils.isNotBlank(configuration.getProperty("LuisAPIKey")) + && StringUtils.isNotBlank(configuration.getProperty("LuisAPIHostName")); + + if (luisIsConfigured) { + LuisApplication luisApplication = new LuisApplication( + configuration.getProperty("LuisAppId"), + configuration.getProperty("LuisAPIKey"), + "https://".concat(configuration.getProperty("LuisAPIHostName")) + ); + + // Set the recognizer options depending on which endpoint version you want to use. + // More details can be found in + // https://docs.microsoft.com/en-gb/azure/cognitive-services/luis/luis-migration-api-v3 + LuisRecognizerOptionsV3 recognizerOptions = new LuisRecognizerOptionsV3(luisApplication); + recognizerOptions.setTelemetryClient(telemetryClient); + + recognizer = new LuisRecognizer(recognizerOptions); + } + } + + /** + * Verify if the recognizer is configured. + * + * @return True if it's configured, False if it's not. + */ + public Boolean isConfigured() { + return this.recognizer != null; + } + + /** + * Return an object with preformatted LUIS results for the bot's dialogs to consume. + * + * @param context A {link TurnContext} + * @return A {link RecognizerResult} + */ + public CompletableFuture executeLuisQuery(TurnContext context) { + // Returns true if luis is configured in the application.properties and initialized. + return this.recognizer.recognize(context); + } + + /** + * Gets the From data from the entities which is part of the result. + * + * @param result The recognizer result. + * @return The object node representing the From data. + */ + public ObjectNode getFromEntities(RecognizerResult result) { + String fromValue = "", fromAirportValue = ""; + if (result.getEntities().get("$instance").get("From") != null) { + fromValue = result.getEntities().get("$instance").get("From").get(0).get("text") + .asText(); + } + if (!fromValue.isEmpty() + && result.getEntities().get("From").get(0).get("Airport") != null) { + fromAirportValue = result.getEntities().get("From").get(0).get("Airport").get(0).get(0) + .asText(); + } + + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + ObjectNode entitiesNode = mapper.createObjectNode(); + entitiesNode.put("from", fromValue); + entitiesNode.put("airport", fromAirportValue); + return entitiesNode; + } + + /** + * Gets the To data from the entities which is part of the result. + * + * @param result The recognizer result. + * @return The object node representing the To data. + */ + public ObjectNode getToEntities(RecognizerResult result) { + String toValue = "", toAirportValue = ""; + if (result.getEntities().get("$instance").get("To") != null) { + toValue = result.getEntities().get("$instance").get("To").get(0).get("text").asText(); + } + if (!toValue.isEmpty() && result.getEntities().get("To").get(0).get("Airport") != null) { + toAirportValue = result.getEntities().get("To").get(0).get("Airport").get(0).get(0) + .asText(); + } + + ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + ObjectNode entitiesNode = mapper.createObjectNode(); + entitiesNode.put("to", toValue); + entitiesNode.put("airport", toAirportValue); + return entitiesNode; + } + + /** + * 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. + * + * @param result A {link RecognizerResult} + * @return The Timex value without the Time model + */ + public String getTravelDate(RecognizerResult result) { + JsonNode datetimeEntity = result.getEntities().get("datetime"); + if (datetimeEntity == null || datetimeEntity.get(0) == null) { + return null; + } + + JsonNode timex = datetimeEntity.get(0).get("timex"); + if (timex == null || timex.get(0) == null) { + return null; + } + + String datetime = timex.get(0).asText().split("T")[0]; + return datetime; + } + + /** + * Runs an utterance through a recognizer and returns a generic recognizer result. + * + * @param turnContext Turn context. + * @return Analysis of utterance. + */ + @Override + public CompletableFuture recognize(TurnContext turnContext) { + return this.recognizer.recognize(turnContext); + } +} diff --git a/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/MainDialog.java b/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/MainDialog.java new file mode 100644 index 000000000..841de553c --- /dev/null +++ b/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/MainDialog.java @@ -0,0 +1,238 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.corebot.app.insights; + +import com.microsoft.bot.builder.MessageFactory; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.dialogs.ComponentDialog; +import com.microsoft.bot.dialogs.DialogTurnResult; +import com.microsoft.bot.dialogs.WaterfallDialog; +import com.microsoft.bot.dialogs.WaterfallStep; +import com.microsoft.bot.dialogs.WaterfallStepContext; +import com.microsoft.bot.dialogs.prompts.PromptOptions; +import com.microsoft.bot.dialogs.prompts.TextPrompt; +import com.microsoft.bot.schema.Activity; +import com.microsoft.bot.schema.InputHints; +import com.microsoft.recognizers.datatypes.timex.expression.TimexProperty; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.microsoft.bot.builder.BotTelemetryClient; +import org.apache.commons.lang3.StringUtils; + +/** + * The class containing the main dialog for the sample. + */ +public class MainDialog extends ComponentDialog { + + private final FlightBookingRecognizer luisRecognizer; + private final Integer plusDayValue = 7; + + /** + * The constructor of the Main Dialog class. + * + * @param withLuisRecognizer The FlightBookingRecognizer object. + * @param bookingDialog The BookingDialog object with booking dialogs. + * @param withTelemetryClient The BotTelemetryClient to use. + */ + public MainDialog(FlightBookingRecognizer withLuisRecognizer, + BookingDialog bookingDialog, + BotTelemetryClient withTelemetryClient) { + super("MainDialog"); + + luisRecognizer = withLuisRecognizer; + + // Set the telemetry client for this and all child dialogs + this.setTelemetryClient(withTelemetryClient); + + addDialog(new TextPrompt("TextPrompt")); + addDialog(bookingDialog); + WaterfallStep[] waterfallSteps = { + this::introStep, + this::actStep, + this::finalStep + }; + addDialog(new WaterfallDialog("WaterfallDialog", Arrays.asList(waterfallSteps))); + + // The initial child Dialog to run. + setInitialDialogId("WaterfallDialog"); + } + + /** + * First step in the waterfall dialog. Prompts the user for a command. Currently, this expects a + * booking request, like "book me a flight from Paris to Berlin on march 22" Note that the + * sample LUIS model will only recognize Paris, Berlin, New York and London as airport cities. + * + * @param stepContext A {@link WaterfallStepContext} + * @return A {@link DialogTurnResult} + */ + private CompletableFuture introStep(WaterfallStepContext stepContext) { + if (!luisRecognizer.isConfigured()) { + Activity text = MessageFactory.text("NOTE: LUIS is not configured. " + + "To enable all capabilities, add 'LuisAppId', 'LuisAPIKey' and 'LuisAPIHostName' " + + "to the application.properties file.", null, InputHints.IGNORING_INPUT); + return stepContext.getContext().sendActivity(text) + .thenCompose(sendResult -> stepContext.next(null)); + } + + // Use the text provided in FinalStepAsync or the default if it is the first time. + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMMM d, yyyy"); + String weekLaterDate = LocalDateTime.now().plusDays(plusDayValue).format(formatter); + String messageText = stepContext.getOptions() != null + ? stepContext.getOptions().toString() + : String.format("What can I help you with today?\n" + + "Say something like \"Book a flight from Paris to Berlin on %s\"", weekLaterDate); + Activity promptMessage = MessageFactory + .text(messageText, messageText, InputHints.EXPECTING_INPUT); + PromptOptions promptOptions = new PromptOptions(); + promptOptions.setPrompt(promptMessage); + return stepContext.prompt("TextPrompt", promptOptions); + } + + /** + * Second step in the waterfall. This will use LUIS to attempt to extract the origin, + * destination and travel dates. Then, it hands off to the bookingDialog child dialog to collect + * any remaining details. + * + * @param stepContext A {@link WaterfallStepContext} + * @return A {@link DialogTurnResult} + */ + private CompletableFuture actStep(WaterfallStepContext stepContext) { + if (!luisRecognizer.isConfigured()) { + // LUIS is not configured, we just run the BookingDialog path with an empty BookingDetailsInstance. + return stepContext.beginDialog("BookingDialog", new BookingDetails()); + } + + // Call LUIS and gather any potential booking details. (Note the TurnContext has the response to the prompt.) + return luisRecognizer.recognize(stepContext.getContext()).thenCompose(luisResult -> { + switch (luisResult.getTopScoringIntent().intent) { + case "BookFlight": + // Extract the values for the composite entities from the LUIS result. + ObjectNode fromEntities = luisRecognizer.getFromEntities(luisResult); + ObjectNode toEntities = luisRecognizer.getToEntities(luisResult); + + // Show a warning for Origin and Destination if we can't resolve them. + return showWarningForUnsupportedCities( + stepContext.getContext(), fromEntities, toEntities) + .thenCompose(showResult -> { + // Initialize BookingDetails with any entities we may have found in the response. + BookingDetails bookingDetails = new BookingDetails(); + bookingDetails.setDestination(toEntities.get("airport").asText()); + bookingDetails.setOrigin(fromEntities.get("airport").asText()); + bookingDetails.setTravelDate(luisRecognizer.getTravelDate(luisResult)); + // Run the BookingDialog giving it whatever details we have from the LUIS call, + // it will fill out the remainder. + return stepContext.beginDialog("BookingDialog", bookingDetails); + } + ); + case "GetWeather": + // We haven't implemented the GetWeatherDialog so we just display a TODO message. + String getWeatherMessageText = "TODO: get weather flow here"; + Activity getWeatherMessage = MessageFactory + .text( + getWeatherMessageText, getWeatherMessageText, + InputHints.IGNORING_INPUT + ); + return stepContext.getContext().sendActivity(getWeatherMessage) + .thenCompose(resourceResponse -> stepContext.next(null)); + + default: + // Catch all for unhandled intents + String didntUnderstandMessageText = String.format( + "Sorry, I didn't get that. Please " + + " try asking in a different way (intent was %s)", + luisResult.getTopScoringIntent().intent + ); + Activity didntUnderstandMessage = MessageFactory + .text( + didntUnderstandMessageText, didntUnderstandMessageText, + InputHints.IGNORING_INPUT + ); + return stepContext.getContext().sendActivity(didntUnderstandMessage) + .thenCompose(resourceResponse -> stepContext.next(null)); + } + }); + } + + /** + * Shows a warning if the requested From or To cities are recognized as entities but they are + * not in the Airport entity list. In some cases LUIS will recognize the From and To composite + * entities as a valid cities but the From and To Airport values will be empty if those entity + * values can't be mapped to a canonical item in the Airport. + * + * @param turnContext A {@link WaterfallStepContext} + * @param fromEntities An ObjectNode with the entities of From object + * @param toEntities An ObjectNode with the entities of To object + * @return A task + */ + private static CompletableFuture showWarningForUnsupportedCities( + TurnContext turnContext, + ObjectNode fromEntities, + ObjectNode toEntities + ) { + List unsupportedCities = new ArrayList(); + + if (StringUtils.isNotBlank(fromEntities.get("from").asText()) + && StringUtils.isBlank(fromEntities.get("airport").asText())) { + unsupportedCities.add(fromEntities.get("from").asText()); + } + + if (StringUtils.isNotBlank(toEntities.get("to").asText()) + && StringUtils.isBlank(toEntities.get("airport").asText())) { + unsupportedCities.add(toEntities.get("to").asText()); + } + + if (!unsupportedCities.isEmpty()) { + String messageText = String.format( + "Sorry but the following airports are not supported: %s", + String.join(", ", unsupportedCities) + ); + Activity message = MessageFactory + .text(messageText, messageText, InputHints.IGNORING_INPUT); + return turnContext.sendActivity(message) + .thenApply(sendResult -> null); + } + + return CompletableFuture.completedFuture(null); + } + + /** + * This is the final step in the main waterfall dialog. It wraps up the sample "book a flight" + * interaction with a simple confirmation. + * + * @param stepContext A {@link WaterfallStepContext} + * @return A {@link DialogTurnResult} + */ + private CompletableFuture finalStep(WaterfallStepContext stepContext) { + CompletableFuture stepResult = CompletableFuture.completedFuture(null); + + // If the child dialog ("BookingDialog") was cancelled, + // the user failed to confirm or if the intent wasn't BookFlight + // the Result here will be null. + if (stepContext.getResult() instanceof BookingDetails) { + // Now we have all the booking details call the booking service. + // If the call to the booking service was successful tell the user. + BookingDetails result = (BookingDetails) stepContext.getResult(); + TimexProperty timeProperty = new TimexProperty(result.getTravelDate()); + String travelDateMsg = timeProperty.toNaturalLanguage(LocalDateTime.now()); + String messageText = String.format("I have you booked to %s from %s on %s", + result.getDestination(), result.getOrigin(), travelDateMsg + ); + Activity message = MessageFactory + .text(messageText, messageText, InputHints.IGNORING_INPUT); + stepResult = stepContext.getContext().sendActivity(message).thenApply(sendResult -> null); + } + + // Restart the main dialog with a different message the second time around + String promptMessage = "What else can I do for you?"; + return stepResult + .thenCompose(result -> stepContext.replaceDialog(getInitialDialogId(), promptMessage)); + } +} diff --git a/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/package-info.java b/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/package-info.java new file mode 100644 index 000000000..037f6f74c --- /dev/null +++ b/samples/21.corebot-app-insights/src/main/java/com/microsoft/bot/sample/corebot/app/insights/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. + +/** + * This package contains the classes for the corebot-app-insights sample. + */ +package com.microsoft.bot.sample.corebot.app.insights; diff --git a/samples/21.corebot-app-insights/src/main/resources/application.properties b/samples/21.corebot-app-insights/src/main/resources/application.properties new file mode 100644 index 000000000..cd458c966 --- /dev/null +++ b/samples/21.corebot-app-insights/src/main/resources/application.properties @@ -0,0 +1,8 @@ +MicrosoftAppId= +MicrosoftAppPassword= +LuisAppId= +LuisAPIKey= +LuisAPIHostName= +# Specifies my instrumentation key for Application Insights. +ApplicationInsights.InstrumentationKey= +server.port=3978 diff --git a/samples/21.corebot-app-insights/src/main/resources/cards/welcomeCard.json b/samples/21.corebot-app-insights/src/main/resources/cards/welcomeCard.json new file mode 100644 index 000000000..9b6389e39 --- /dev/null +++ b/samples/21.corebot-app-insights/src/main/resources/cards/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": true, + "text": "Now that you have successfully run your bot, follow the links in this Adaptive Card to expand your knowledge of Bot Framework.", + "wrap": true, + "maxLines": 0 + } + ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "Get an overview", + "url": "https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0" + }, + { + "type": "Action.OpenUrl", + "title": "Ask a question", + "url": "https://stackoverflow.com/questions/tagged/botframework" + }, + { + "type": "Action.OpenUrl", + "title": "Learn how to deploy", + "url": "https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-deploy-azure?view=azure-bot-service-4.0" + } + ] +} diff --git a/samples/21.corebot-app-insights/src/main/webapp/META-INF/MANIFEST.MF b/samples/21.corebot-app-insights/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 000000000..254272e1c --- /dev/null +++ b/samples/21.corebot-app-insights/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Class-Path: + diff --git a/samples/21.corebot-app-insights/src/main/webapp/WEB-INF/web.xml b/samples/21.corebot-app-insights/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..383c19004 --- /dev/null +++ b/samples/21.corebot-app-insights/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,12 @@ + + + dispatcher + + org.springframework.web.servlet.DispatcherServlet + + + contextConfigLocation + /WEB-INF/spring/dispatcher-config.xml + + 1 + \ No newline at end of file diff --git a/samples/21.corebot-app-insights/src/main/webapp/index.html b/samples/21.corebot-app-insights/src/main/webapp/index.html new file mode 100644 index 000000000..730d2c453 --- /dev/null +++ b/samples/21.corebot-app-insights/src/main/webapp/index.html @@ -0,0 +1,417 @@ + + + + + + + Core Bot Sample + + + + + +
+
+
+
Core Bot Sample
+
+
+
+
+
Your bot is ready!
+
You can test your bot in the Bot Framework Emulator
+ by connecting to http://localhost:3978/api/messages.
+ +
Visit Azure + Bot Service to register your bot and add it to
+ various channels. The bot's endpoint URL typically looks + like this:
+
https://your_bots_hostname/api/messages
+
+
+
+
+ +
+ + diff --git a/samples/21.corebot-app-insights/src/test/java/com/microsoft/bot/sample/corebot/app/insights/ApplicationTest.java b/samples/21.corebot-app-insights/src/test/java/com/microsoft/bot/sample/corebot/app/insights/ApplicationTest.java new file mode 100644 index 000000000..de794a90c --- /dev/null +++ b/samples/21.corebot-app-insights/src/test/java/com/microsoft/bot/sample/corebot/app/insights/ApplicationTest.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.corebot.app.insights; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class ApplicationTest { + + @Test + public void contextLoads() { + } + +}