From 33eba7e41604ceeae95392ea189c667185b0039e Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Mon, 8 Feb 2021 10:14:48 -0600 Subject: [PATCH 1/2] Sample 43.complex-dialog --- .../microsoft/bot/dialogs/DialogState.java | 2 +- .../bot/dialogs/WaterfallDialog.java | 34 +- .../microsoft/bot/dialogs/prompts/Prompt.java | 42 +- samples/43.complex-dialog/LICENSE | 21 + samples/43.complex-dialog/README.md | 90 ++++ .../template-with-new-rg.json | 291 ++++++++++++ .../template-with-preexisting-rg.json | 259 +++++++++++ samples/43.complex-dialog/pom.xml | 244 ++++++++++ .../bot/sample/complexdialog/Application.java | 48 ++ .../complexdialog/DialogAndWelcome.java | 46 ++ .../bot/sample/complexdialog/DialogBot.java | 54 +++ .../bot/sample/complexdialog/MainDialog.java | 60 +++ .../complexdialog/ReviewSelectionDialog.java | 99 +++++ .../sample/complexdialog/TopLevelDialog.java | 95 ++++ .../bot/sample/complexdialog/UserProfile.java | 18 + .../src/main/resources/application.properties | 3 + .../src/main/resources/log4j2.json | 18 + .../src/main/webapp/META-INF/MANIFEST.MF | 3 + .../src/main/webapp/WEB-INF/web.xml | 12 + .../src/main/webapp/index.html | 418 ++++++++++++++++++ .../sample/complexdialog/ApplicationTest.java | 19 + 21 files changed, 1838 insertions(+), 38 deletions(-) create mode 100644 samples/43.complex-dialog/LICENSE create mode 100644 samples/43.complex-dialog/README.md create mode 100644 samples/43.complex-dialog/deploymentTemplates/template-with-new-rg.json create mode 100644 samples/43.complex-dialog/deploymentTemplates/template-with-preexisting-rg.json create mode 100644 samples/43.complex-dialog/pom.xml create mode 100644 samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/Application.java create mode 100644 samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/DialogAndWelcome.java create mode 100644 samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/DialogBot.java create mode 100644 samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/MainDialog.java create mode 100644 samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/ReviewSelectionDialog.java create mode 100644 samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/TopLevelDialog.java create mode 100644 samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/UserProfile.java create mode 100644 samples/43.complex-dialog/src/main/resources/application.properties create mode 100644 samples/43.complex-dialog/src/main/resources/log4j2.json create mode 100644 samples/43.complex-dialog/src/main/webapp/META-INF/MANIFEST.MF create mode 100644 samples/43.complex-dialog/src/main/webapp/WEB-INF/web.xml create mode 100644 samples/43.complex-dialog/src/main/webapp/index.html create mode 100644 samples/43.complex-dialog/src/test/java/com/microsoft/bot/sample/complexdialog/ApplicationTest.java diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogState.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogState.java index 29d1fdb01..257889980 100644 --- a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogState.java +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/DialogState.java @@ -13,7 +13,7 @@ */ public class DialogState { @JsonProperty(value = "dialogStack") - private List dialogStack = new ArrayList(); + private List dialogStack; /** * Initializes a new instance of the class with an empty stack. diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/WaterfallDialog.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/WaterfallDialog.java index 1ac77aef5..597f71ad9 100644 --- a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/WaterfallDialog.java +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/WaterfallDialog.java @@ -24,15 +24,15 @@ */ public class WaterfallDialog extends Dialog { - private final String persistedOptions = "options"; - private final String stepIndex = "stepIndex"; - private final String persistedValues = "values"; - private final String persistedInstanceId = "instanceId"; + private static final String PERSISTED_OPTIONS = "options"; + private static final String PERSISTED_VALUES = "values"; + private static final String PERSISTED_INSTANCEID = "instanceId"; + private static final String STEP_INDEX = "stepIndex"; private final List steps; /** - * Initializes a new instance of the {@link waterfallDialog} class. + * Initializes a new instance of the {@link WaterfallDialog} class. * * @param dialogId The dialog ID. * @param actions Optional actions to be defined by the caller. @@ -90,9 +90,9 @@ public CompletableFuture beginDialog(DialogContext dc, Object // Initialize waterfall state Map state = dc.getActiveDialog().getState(); String instanceId = UUID.randomUUID().toString(); - state.put(persistedOptions, options); - state.put(persistedValues, new HashMap()); - state.put(persistedInstanceId, instanceId); + state.put(PERSISTED_OPTIONS, options); + state.put(PERSISTED_VALUES, new HashMap()); + state.put(PERSISTED_INSTANCEID, instanceId); Map properties = new HashMap(); properties.put("DialogId", getId()); @@ -156,8 +156,8 @@ public CompletableFuture resumeDialog(DialogContext dc, Dialog // Increment step index and run step Map state = dc.getActiveDialog().getState(); int index = 0; - if (state.containsKey(stepIndex)) { - index = (int) state.get(stepIndex); + if (state.containsKey(STEP_INDEX)) { + index = (int) state.get(STEP_INDEX); } return runStep(dc, index + 1, reason, result); @@ -178,9 +178,9 @@ public CompletableFuture endDialog(TurnContext turnContext, DialogInstance HashMap state = new HashMap((Map) instance.getState()); // Create step context - int index = (int) state.get(stepIndex); + int index = (int) state.get(STEP_INDEX); String stepName = waterfallStepName(index); - String instanceId = (String) state.get(persistedInstanceId); + String instanceId = (String) state.get(PERSISTED_INSTANCEID); HashMap properties = new HashMap(); properties.put("DialogId", getId()); @@ -190,7 +190,7 @@ public CompletableFuture endDialog(TurnContext turnContext, DialogInstance getTelemetryClient().trackEvent("WaterfallCancel", properties); } else if (reason == DialogReason.END_CALLED) { HashMap state = new HashMap((Map) instance.getState()); - String instanceId = (String) state.get(persistedInstanceId); + String instanceId = (String) state.get(PERSISTED_INSTANCEID); HashMap properties = new HashMap(); properties.put("DialogId", getId()); @@ -210,7 +210,7 @@ public CompletableFuture endDialog(TurnContext turnContext, DialogInstance */ protected CompletableFuture onStep(WaterfallStepContext stepContext) { String stepName = waterfallStepName(stepContext.getIndex()); - String instanceId = (String) stepContext.getActiveDialog().getState().get(persistedInstanceId); + String instanceId = (String) stepContext.getActiveDialog().getState().get(PERSISTED_INSTANCEID); HashMap properties = new HashMap(); properties.put("DialogId", getId()); @@ -245,11 +245,11 @@ protected CompletableFuture runStep(DialogContext dc, int inde // Update persisted step index Map state = (Map) dc.getActiveDialog().getState(); - state.put(stepIndex, index); + state.put(STEP_INDEX, index); // Create step context - Object options = state.get(persistedOptions); - Map values = (Map) state.get(persistedValues); + Object options = state.get(PERSISTED_OPTIONS); + Map values = (Map) state.get(PERSISTED_VALUES); WaterfallStepContext stepContext = new WaterfallStepContext(this, dc, options, values, index, reason, result); diff --git a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/Prompt.java b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/Prompt.java index aced3a4ab..8188870f8 100644 --- a/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/Prompt.java +++ b/libraries/bot-dialogs/src/main/java/com/microsoft/bot/dialogs/prompts/Prompt.java @@ -32,13 +32,14 @@ * Defines the core behavior of prompt dialogs. * * When the prompt ends, it should return a Object that represents the value - * that was prompted for. Use {@link DialogSet#add(Dialog)} or - * {@link ComponentDialog#addDialog(Dialog)} to add a prompt to a dialog set or - * component dialog, respectively. Use + * that was prompted for. Use {@link com.microsoft.bot.dialogs.DialogSet#add(Dialog)} or + * {@link com.microsoft.bot.dialogs.ComponentDialog#addDialog(Dialog)} to add a prompt to + * a dialog set or component dialog, respectively. Use * {@link DialogContext#prompt(String, PromptOptions)} or * {@link DialogContext#beginDialog(String, Object)} to start the prompt. If you - * start a prompt from a {@link WaterfallStep} in a {@link WaterfallDialog} , - * then the prompt result will be available in the next step of the waterfall. + * start a prompt from a {@link com.microsoft.bot.dialogs.WaterfallStep} in a + * {@link com.microsoft.bot.dialogs.WaterfallDialog}, then the prompt result will be + * available in the next step of the waterfall. * * @param Type the prompt is created for. */ @@ -46,8 +47,8 @@ public abstract class Prompt extends Dialog { public static final String ATTEMPTCOUNTKEY = "AttemptCount"; - private final String persistedOptions = "options"; - private final String persistedState = "state"; + private static final String PERSISTED_OPTIONS = "options"; + private static final String PERSISTED_STATE = "state"; private final PromptValidator validator; /** @@ -58,8 +59,9 @@ public abstract class Prompt extends Dialog { * @param validator Optional, a {@link PromptValidator{T}} that contains * additional, custom validation for this prompt. * - * The value of {@link dialogId} must be unique within the - * {@link DialogSet} or {@link ComponentDialog} to which the + * The value of dialogId must be unique within the + * {@link com.microsoft.bot.dialogs.DialogSet} or + * {@link com.microsoft.bot.dialogs.ComponentDialog} to which the * prompt is added. */ public Prompt(String dialogId, PromptValidator validator) { @@ -114,16 +116,16 @@ public CompletableFuture beginDialog(DialogContext dc, Object // Initialize prompt state Map state = dc.getActiveDialog().getState(); - state.put(persistedOptions, opt); + state.put(PERSISTED_OPTIONS, opt); HashMap pState = new HashMap(); pState.put(ATTEMPTCOUNTKEY, 0); - state.put(persistedState, pState); + state.put(PERSISTED_STATE, pState); // Send initial prompt onPrompt(dc.getContext(), - (Map) state.get(persistedState), - (PromptOptions) state.get(persistedOptions), + (Map) state.get(PERSISTED_STATE), + (PromptOptions) state.get(PERSISTED_OPTIONS), false); return CompletableFuture.completedFuture(Dialog.END_OF_TURN); } @@ -157,8 +159,8 @@ public CompletableFuture continueDialog(DialogContext dc) { // Perform base recognition DialogInstance instance = dc.getActiveDialog(); - Map state = (Map) instance.getState().get(persistedState); - PromptOptions options = (PromptOptions) instance.getState().get(persistedOptions); + Map state = (Map) instance.getState().get(PERSISTED_STATE); + PromptOptions options = (PromptOptions) instance.getState().get(PERSISTED_OPTIONS); PromptRecognizerResult recognized = onRecognize(dc.getContext(), state, options).join(); state.put(ATTEMPTCOUNTKEY, (int) state.get(ATTEMPTCOUNTKEY) + 1); @@ -226,8 +228,8 @@ public CompletableFuture resumeDialog(DialogContext dc, Dialog */ @Override public CompletableFuture repromptDialog(TurnContext turnContext, DialogInstance instance) { - Map state = (Map) instance.getState().get(persistedState); - PromptOptions options = (PromptOptions) instance.getState().get(persistedOptions); + Map state = (Map) instance.getState().get(PERSISTED_STATE); + PromptOptions options = (PromptOptions) instance.getState().get(PERSISTED_OPTIONS); onPrompt(turnContext, state, options, false).join(); return CompletableFuture.completedFuture(null); } @@ -252,7 +254,7 @@ protected CompletableFuture onPreBubbleEvent(DialogContext dc, DialogEv // Perform base recognition Map state = dc.getActiveDialog().getState(); PromptRecognizerResult recognized = onRecognize(dc.getContext(), - (Map) state.get(persistedState), (PromptOptions) state.get(persistedOptions)).join(); + (Map) state.get(PERSISTED_STATE), (PromptOptions) state.get(PERSISTED_OPTIONS)).join(); return CompletableFuture.completedFuture(recognized.getSucceeded()); } @@ -272,8 +274,8 @@ protected CompletableFuture onPreBubbleEvent(DialogContext dc, DialogEv * @param isRetry true if this is the first time this prompt dialog instance * is on the stack is prompting the user for input; * otherwise, false. Determines whether - * {@link PromptOptions#prompt} or - * {@link PromptOptions#retryPrompt} should be used. + * {@link PromptOptions#getPrompt()} or + * {@link PromptOptions#getRetryPrompt()} should be used. * * @return A {@link CompletableFuture} representing the asynchronous operation. */ diff --git a/samples/43.complex-dialog/LICENSE b/samples/43.complex-dialog/LICENSE new file mode 100644 index 000000000..21071075c --- /dev/null +++ b/samples/43.complex-dialog/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/43.complex-dialog/README.md b/samples/43.complex-dialog/README.md new file mode 100644 index 000000000..39197e467 --- /dev/null +++ b/samples/43.complex-dialog/README.md @@ -0,0 +1,90 @@ +# Complex Dialog Sample + +This sample creates a complex conversation with dialogs. + +This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to use the prompts classes included in `botbuilder-dialogs`. This bot will ask for the user's name and age, then store the responses. It demonstrates a multi-turn dialog flow using a text prompt, a number prompt, and state accessors to store and retrieve values. + +## Prerequisites + +- Java 1.8+ +- Install [Maven](https://maven.apache.org/) +- An account on [Azure](https://azure.microsoft.com) if you want to deploy to Azure. + +## To try this sample locally +- From the root of this project folder: + - Build the sample using `mvn package` + - Run it by using `java -jar .\target\bot-complexdialog-sample.jar` + +- Test the bot using Bot Framework Emulator + + [Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. + + - Install the Bot Framework Emulator version 4.3.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) + + - Connect to the bot using Bot Framework Emulator + + - Launch Bot Framework Emulator + - File -> Open Bot + - Enter a Bot URL of `http://localhost:3978/api/messages` + +## Deploy the bot to Azure + +As described on [Deploy your bot](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-deploy-az-cli), you will perform the first 4 steps to setup the Azure app, then deploy the code using the azure-webapp Maven plugin. + +### 1. Login to Azure +From a command (or PowerShell) prompt in the root of the bot folder, execute: +`az login` + +### 2. Set the subscription +`az account set --subscription ""` + +If you aren't sure which subscription to use for deploying the bot, you can view the list of subscriptions for your account by using `az account list` command. + +### 3. Create an App registration +`az ad app create --display-name "" --password "" --available-to-other-tenants` + +Replace `` and `` with your own values. + +`` is the unique name of your bot. +`` is a minimum 16 character password for your bot. + +Record the `appid` from the returned JSON + +### 4. Create the Azure resources +Replace the values for ``, ``, ``, and `` in the following commands: + +#### To a new Resource Group +`az deployment sub create --name "multiTurnPromptBotDeploy" --location "westus" --template-file ".\deploymentTemplates\template-with-new-rg.json" --parameters appId="" appSecret="" botId="" botSku=S1 newAppServicePlanName="multiTurnPromptBotPlan" newWebAppName="multiTurnPromptBot" groupLocation="westus" newAppServicePlanLocation="westus"` + +#### To an existing Resource Group +`az deployment group create --resource-group "" --template-file ".\deploymentTemplates\template-with-preexisting-rg.json" --parameters appId="" appSecret="" botId="" newWebAppName="multiTurnPromptBot" newAppServicePlanName="multiTurnPromptBotPlan" appServicePlanLocation="westus" --name "multiTurnPromptBot"` + +### 5. Update app id and password +In src/main/resources/application.properties update + - `MicrosoftAppPassword` with the botsecret value + - `MicrosoftAppId` with the appid from the first step + +### 6. Deploy the code +- Execute `mvn clean package` +- Execute `mvn azure-webapp:deploy -Dgroupname="" -Dbotname=""` + +If the deployment is successful, you will be able to test it via "Test in Web Chat" from the Azure Portal using the "Bot Channel Registration" for the bot. + +After the bot is deployed, you only need to execute #6 if you make changes to the bot. + + +## 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) +- [Channels and Bot Connector Service](https://docs.microsoft.com/en-us/azure/bot-service/bot-concepts?view=azure-bot-service-4.0) +- [Maven Plugin for Azure App Service](https://docs.microsoft.com/en-us/java/api/overview/azure/maven/azure-webapp-maven-plugin/readme?view=azure-java-stable) +- [Spring Boot](https://spring.io/projects/spring-boot) +- [Azure for Java cloud developers](https://docs.microsoft.com/en-us/azure/java/?view=azure-java-stable) diff --git a/samples/43.complex-dialog/deploymentTemplates/template-with-new-rg.json b/samples/43.complex-dialog/deploymentTemplates/template-with-new-rg.json new file mode 100644 index 000000000..ec2460d3a --- /dev/null +++ b/samples/43.complex-dialog/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/43.complex-dialog/deploymentTemplates/template-with-preexisting-rg.json b/samples/43.complex-dialog/deploymentTemplates/template-with-preexisting-rg.json new file mode 100644 index 000000000..024dcf08d --- /dev/null +++ b/samples/43.complex-dialog/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/43.complex-dialog/pom.xml b/samples/43.complex-dialog/pom.xml new file mode 100644 index 000000000..7304e65aa --- /dev/null +++ b/samples/43.complex-dialog/pom.xml @@ -0,0 +1,244 @@ + + + + 4.0.0 + + com.microsoft.bot.sample + bot-complexdialog + sample + jar + + ${project.groupId}:${project.artifactId} + This package contains the Multi-turn Prompt sample 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.complexdialog.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-preview8 + compile + + + com.microsoft.bot + bot-dialogs + 4.6.0-preview8 + compile + + + + + + 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.complexdialog.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/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/Application.java b/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/Application.java new file mode 100644 index 000000000..c256d7b15 --- /dev/null +++ b/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/Application.java @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.complexdialog; + +import com.microsoft.bot.integration.AdapterWithErrorHandler; +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.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Import; + +/** + * This is the starting point of the Sprint Boot Bot application. + * + * This class also provides overrides for dependency injections. A class that + * extends the {@link com.microsoft.bot.builder.Bot} interface should be + * annotated with @Component. + * + * @see DialogAndWelcome + */ +@SpringBootApplication + +// Use the default BotController to receive incoming Channel messages. A custom +// controller could be used by eliminating this import and creating a new +// RestController. +// The default controller is created by the Spring Boot container using +// dependency injection. The default route is /api/messages. +@Import({BotController.class}) + +public class Application extends BotDependencyConfiguration { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + /** + * 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) { + return new AdapterWithErrorHandler(configuration); + } +} diff --git a/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/DialogAndWelcome.java b/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/DialogAndWelcome.java new file mode 100644 index 000000000..2eda16100 --- /dev/null +++ b/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/DialogAndWelcome.java @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.complexdialog; + +import com.codepoetics.protonpack.collectors.CompletableFutures; +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.schema.Activity; +import com.microsoft.bot.schema.ChannelAccount; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +@Component +public class DialogAndWelcome extends DialogBot { + + public DialogAndWelcome( + ConversationState withConversationState, + UserState withUserState, + MainDialog withDialog + ) { + super(withConversationState, withUserState, withDialog); + } + + @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 -> { + Activity reply = MessageFactory.text("Welcome to Complex Dialog. " + + "This bot provides a complex conversation, with multiple dialogs. " + + "Type anything to get started."); + + return turnContext.sendActivity(reply); + }) + .collect(CompletableFutures.toFutureList()) + .thenApply(resourceResponse -> null); + } +} diff --git a/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/DialogBot.java b/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/DialogBot.java new file mode 100644 index 000000000..be9aa60ce --- /dev/null +++ b/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/DialogBot.java @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.complexdialog; + +import com.microsoft.bot.builder.ActivityHandler; +import com.microsoft.bot.builder.BotState; +import com.microsoft.bot.builder.ConversationState; +import com.microsoft.bot.dialogs.Dialog; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.builder.UserState; +import java.util.concurrent.CompletableFuture; +import org.springframework.stereotype.Component; + +/** + * This IBot implementation can run any type of Dialog. The use of type parameterization is to + * allows multiple different bots to be run at different endpoints within the same project. This + * can be achieved by defining distinct Controller types each with dependency on distinct IBot + * types, this way ASP Dependency Injection can glue everything together without ambiguity. 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. + */ +public class DialogBot extends ActivityHandler { + protected Dialog dialog; + protected BotState conversationState; + protected BotState userState; + + public DialogBot( + ConversationState withConversationState, + UserState withUserState, + Dialog withDialog + ) { + dialog = withDialog; + conversationState = withConversationState; + userState = withUserState; + } + + @Override + public CompletableFuture onTurn( + TurnContext turnContext + ) { + return super.onTurn(turnContext) + .thenCompose(result -> conversationState.saveChanges(turnContext)) + .thenCompose(result -> userState.saveChanges(turnContext)); + } + + @Override + protected CompletableFuture onMessageActivity( + TurnContext turnContext + ) { + return Dialog.run(dialog, turnContext, conversationState.createProperty("DialogState")); + } +} diff --git a/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/MainDialog.java b/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/MainDialog.java new file mode 100644 index 000000000..bef2829ca --- /dev/null +++ b/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/MainDialog.java @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.complexdialog; + +import com.microsoft.bot.builder.StatePropertyAccessor; +import com.microsoft.bot.builder.UserState; +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 java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import org.springframework.stereotype.Component; + +@Component +public class MainDialog extends ComponentDialog { + private UserState userState; + + private StatePropertyAccessor userProfileAccessor; + + public MainDialog(UserState withUserState) { + super("MainDialog"); + + //userProfileAccessor = withUserState.createProperty("UserProfile"); + userState = withUserState; + + addDialog(new TopLevelDialog("TopLevelDialog")); + + WaterfallStep[] waterfallSteps = { + this::initialStep, + this::finalStep + }; + + addDialog(new WaterfallDialog("WaterfallDialog", Arrays.asList(waterfallSteps))); + + setInitialDialogId("WaterfallDialog"); + } + + private CompletableFuture initialStep(WaterfallStepContext stepContext) { + return stepContext.beginDialog("TopLevelDialog"); + } + + private CompletableFuture finalStep(WaterfallStepContext stepContext) { + UserProfile userInfo = (UserProfile) stepContext.getResult(); + + String status = String.format("You are signed up to review %s.", + userInfo.companiesToReview.size() == 0 + ? "no companies" + : String.join(",", userInfo.companiesToReview)); + + return stepContext.getContext().sendActivity(status) + .thenCompose(resourceResponse -> { + StatePropertyAccessor userProfileAccessor = userState.createProperty("UserProfile"); + return userProfileAccessor.set(stepContext.getContext(), userInfo); + }) + .thenCompose(setResult -> stepContext.endDialog()); + } +} diff --git a/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/ReviewSelectionDialog.java b/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/ReviewSelectionDialog.java new file mode 100644 index 000000000..749b403da --- /dev/null +++ b/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/ReviewSelectionDialog.java @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.complexdialog; + +import com.microsoft.bot.builder.MessageFactory; +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.choices.ChoiceFactory; +import com.microsoft.bot.dialogs.choices.FoundChoice; +import com.microsoft.bot.dialogs.prompts.ChoicePrompt; +import com.microsoft.bot.dialogs.prompts.PromptOptions; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.apache.commons.lang3.StringUtils; + +public class ReviewSelectionDialog extends ComponentDialog { + // Define a "done" response for the company selection prompt. + private static final String DONE_OPTION = "done"; + + // Define value names for values tracked inside the dialogs. + private static final String COMPANIES_SELECTED = "value-companiesSelected"; + + private static List companiesOptions = Arrays.asList( + "Adatum Corporation", "Contoso Suites", "Graphic Design Institute", "Wide World Importers" + ); + + public ReviewSelectionDialog(String withId) { + super(withId); + + addDialog(new ChoicePrompt("ChoicePrompt")); + + WaterfallStep[] waterfallSteps = { + this::selectionStep, + this::loopStep + }; + addDialog(new WaterfallDialog("WaterfallDialog", Arrays.asList(waterfallSteps))); + + // The initial child Dialog to run. + setInitialDialogId("WaterfallDialog"); + } + + private CompletableFuture selectionStep(WaterfallStepContext stepContext) { + // Continue using the same selection list, if any, from the previous iteration of this dialog. + List list = stepContext.getOptions() instanceof List + ? (List) stepContext.getOptions() + : new ArrayList<>(); + stepContext.getValues().put(COMPANIES_SELECTED, list); + + String message; + if (list.size() == 0) { + message = String.format("Please choose a company to review, or `%s` to finish.", DONE_OPTION); + } else { + message = String.format("You have selected **%s**. You can review an additional company, or choose `%s` to finish.", + list.get(0), + DONE_OPTION); + } + + // Create the list of options to choose from. + List options = new ArrayList<>(companiesOptions); + options.add(DONE_OPTION); + if (list.size() > 0) { + options.remove(list.get(0)); + } + + PromptOptions promptOptions = new PromptOptions(); + promptOptions.setPrompt(MessageFactory.text(message)); + promptOptions.setRetryPrompt(MessageFactory.text("Please choose an option from the list.")); + promptOptions.setChoices(ChoiceFactory.toChoices(options)); + + // Prompt the user for a choice. + return stepContext.prompt("ChoicePrompt", promptOptions); + } + + private CompletableFuture loopStep(WaterfallStepContext stepContext) { + // Retrieve their selection list, the choice they made, and whether they chose to finish. + List list = (List) stepContext.getValues().get(COMPANIES_SELECTED); + FoundChoice choice = (FoundChoice) stepContext.getResult(); + boolean done = StringUtils.equals(choice.getValue(), DONE_OPTION); + + // If they chose a company, add it to the list. + if (!done) { + list.add(choice.getValue()); + } + + // If they're done, exit and return their list. + if (done || list.size() >= 2) { + return stepContext.endDialog(list); + } + + // Otherwise, repeat this dialog, passing in the list from this iteration. + return stepContext.replaceDialog(getId(), list); + } +} diff --git a/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/TopLevelDialog.java b/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/TopLevelDialog.java new file mode 100644 index 000000000..0e8d03ca1 --- /dev/null +++ b/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/TopLevelDialog.java @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.complexdialog; + +import com.microsoft.bot.builder.MessageFactory; +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.NumberPrompt; +import com.microsoft.bot.dialogs.prompts.PromptOptions; +import com.microsoft.bot.dialogs.prompts.TextPrompt; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class TopLevelDialog extends ComponentDialog { + // Define a "done" response for the company selection prompt. + private static final String DONE_OPTION = "done"; + + // Define value names for values tracked inside the dialogs. + private static final String USER_INFO = "value-userInfo"; + + public TopLevelDialog(String withId) { + super(withId); + + addDialog(new TextPrompt("TextPrompt")); + addDialog(new NumberPrompt("NumberPrompt", Integer.class)); + + addDialog(new ReviewSelectionDialog("ReviewSelectionDialog")); + + WaterfallStep[] waterfallSteps = { + this::nameStep, + this::ageStep, + this::startSelectionStep, + this::acknowledgementStep + }; + addDialog(new WaterfallDialog("WaterfallDialog", Arrays.asList(waterfallSteps))); + + // The initial child Dialog to run. + setInitialDialogId("WaterfallDialog"); + } + + private CompletableFuture nameStep(WaterfallStepContext stepContext) { + // Create an object in which to collect the user's information within the dialog. + stepContext.getValues().put(USER_INFO, new UserProfile()); + + // Ask the user to enter their name. + PromptOptions promptOptions = new PromptOptions(); + promptOptions.setPrompt(MessageFactory.text("Please enter your name.")); + return stepContext.prompt("TextPrompt", promptOptions); + } + + private CompletableFuture ageStep(WaterfallStepContext stepContext) { + // Set the user's name to what they entered in response to the name prompt. + UserProfile userProfile = (UserProfile) stepContext.getValues().get(USER_INFO); + userProfile.name = (String) stepContext.getResult(); + + // Ask the user to enter their age. + PromptOptions promptOptions = new PromptOptions(); + promptOptions.setPrompt(MessageFactory.text("Please enter your age.")); + return stepContext.prompt("NumberPrompt", promptOptions); + } + + private CompletableFuture startSelectionStep(WaterfallStepContext stepContext) { + // Set the user's age to what they entered in response to the age prompt. + UserProfile userProfile = (UserProfile) stepContext.getValues().get(USER_INFO); + userProfile.age = (Integer) stepContext.getResult(); + + // If they are too young, skip the review selection dialog, and pass an empty list to the next step. + if (userProfile.age < 25) { + return stepContext.getContext().sendActivity(MessageFactory.text("You must be 25 or older to participate.")) + .thenCompose(resourceResponse -> stepContext.next(new ArrayList())); + } + + // Otherwise, start the review selection dialog. + return stepContext.beginDialog("ReviewSelectionDialog"); + } + + private CompletableFuture acknowledgementStep(WaterfallStepContext stepContext) { + // Set the user's company selection to what they entered in the review-selection dialog. + UserProfile userProfile = (UserProfile) stepContext.getValues().get(USER_INFO); + userProfile.companiesToReview = stepContext.getResult() instanceof List + ? (List) stepContext.getResult() + : new ArrayList<>(); + + // Thank them for participating. + return stepContext.getContext() + .sendActivity(MessageFactory.text(String.format("Thanks for participating, %s.", userProfile.name))) + .thenCompose(resourceResponse -> stepContext.endDialog(stepContext.getValues().get(USER_INFO))); + } +} diff --git a/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/UserProfile.java b/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/UserProfile.java new file mode 100644 index 000000000..6dbb95e90 --- /dev/null +++ b/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/UserProfile.java @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.complexdialog; + +import java.util.ArrayList; +import java.util.List; + +/** + * Contains information about a user. + */ +public class UserProfile { + public String name; + public Integer age; + + // The list of companies the user wants to review. + public List companiesToReview = new ArrayList<>(); +} diff --git a/samples/43.complex-dialog/src/main/resources/application.properties b/samples/43.complex-dialog/src/main/resources/application.properties new file mode 100644 index 000000000..d7d0ee864 --- /dev/null +++ b/samples/43.complex-dialog/src/main/resources/application.properties @@ -0,0 +1,3 @@ +MicrosoftAppId= +MicrosoftAppPassword= +server.port=3978 diff --git a/samples/43.complex-dialog/src/main/resources/log4j2.json b/samples/43.complex-dialog/src/main/resources/log4j2.json new file mode 100644 index 000000000..67c0ad530 --- /dev/null +++ b/samples/43.complex-dialog/src/main/resources/log4j2.json @@ -0,0 +1,18 @@ +{ + "configuration": { + "name": "Default", + "appenders": { + "Console": { + "name": "Console-Appender", + "target": "SYSTEM_OUT", + "PatternLayout": {"pattern": "[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n"} + } + }, + "loggers": { + "root": { + "level": "debug", + "appender-ref": {"ref": "Console-Appender","level": "debug"} + } + } + } +} diff --git a/samples/43.complex-dialog/src/main/webapp/META-INF/MANIFEST.MF b/samples/43.complex-dialog/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 000000000..254272e1c --- /dev/null +++ b/samples/43.complex-dialog/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Class-Path: + diff --git a/samples/43.complex-dialog/src/main/webapp/WEB-INF/web.xml b/samples/43.complex-dialog/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..383c19004 --- /dev/null +++ b/samples/43.complex-dialog/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/43.complex-dialog/src/main/webapp/index.html b/samples/43.complex-dialog/src/main/webapp/index.html new file mode 100644 index 000000000..d5ba5158e --- /dev/null +++ b/samples/43.complex-dialog/src/main/webapp/index.html @@ -0,0 +1,418 @@ + + + + + + + EchoBot + + + + + +
+
+
+
Spring Boot Bot
+
+
+
+
+
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/43.complex-dialog/src/test/java/com/microsoft/bot/sample/complexdialog/ApplicationTest.java b/samples/43.complex-dialog/src/test/java/com/microsoft/bot/sample/complexdialog/ApplicationTest.java new file mode 100644 index 000000000..6df04dbd5 --- /dev/null +++ b/samples/43.complex-dialog/src/test/java/com/microsoft/bot/sample/complexdialog/ApplicationTest.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.complexdialog; + +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() { + } + +} From 9ae808ba34fbd49a10630d80f84e66363bf85b33 Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Tue, 9 Feb 2021 12:40:05 -0600 Subject: [PATCH 2/2] Refactored to use @Bean instead of @Component to be consistent with other samples. --- .../bot/sample/complexdialog/Application.java | 58 +++++++++++++++---- .../complexdialog/DialogAndWelcome.java | 5 +- .../bot/sample/complexdialog/DialogBot.java | 1 - .../bot/sample/complexdialog/MainDialog.java | 5 -- 4 files changed, 50 insertions(+), 19 deletions(-) diff --git a/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/Application.java b/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/Application.java index c256d7b15..6b500f091 100644 --- a/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/Application.java +++ b/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/Application.java @@ -3,6 +3,10 @@ package com.microsoft.bot.sample.complexdialog; +import com.microsoft.bot.builder.Bot; +import com.microsoft.bot.builder.ConversationState; +import com.microsoft.bot.builder.UserState; +import com.microsoft.bot.dialogs.Dialog; import com.microsoft.bot.integration.AdapterWithErrorHandler; import com.microsoft.bot.integration.BotFrameworkHttpAdapter; import com.microsoft.bot.integration.Configuration; @@ -10,31 +14,65 @@ import com.microsoft.bot.integration.spring.BotDependencyConfiguration; 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. - * - * This class also provides overrides for dependency injections. A class that - * extends the {@link com.microsoft.bot.builder.Bot} interface should be - * annotated with @Component. - * - * @see DialogAndWelcome - */ +// +// 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 -// RestController. +// 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 { 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. + *

+ * + * @return The Bot implementation for this application. + */ + @Bean + public Bot getBot( + ConversationState conversationState, + UserState userState, + Dialog dialog + ) { + return new DialogAndWelcome(conversationState, userState, dialog); + } + + /** + * Returns the starting Dialog for this application. + * + *

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

+ * + * @return The Dialog implementation for this application. + */ + @Bean + public Dialog getRootDialog(UserState userState) { + return new MainDialog(userState); + } + /** * Returns a custom Adapter that provides error handling. * diff --git a/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/DialogAndWelcome.java b/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/DialogAndWelcome.java index 2eda16100..39d5badad 100644 --- a/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/DialogAndWelcome.java +++ b/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/DialogAndWelcome.java @@ -8,20 +8,19 @@ 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.ChannelAccount; import java.util.List; import java.util.concurrent.CompletableFuture; import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Component; -@Component public class DialogAndWelcome extends DialogBot { public DialogAndWelcome( ConversationState withConversationState, UserState withUserState, - MainDialog withDialog + Dialog withDialog ) { super(withConversationState, withUserState, withDialog); } diff --git a/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/DialogBot.java b/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/DialogBot.java index be9aa60ce..fc7ed3512 100644 --- a/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/DialogBot.java +++ b/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/DialogBot.java @@ -10,7 +10,6 @@ import com.microsoft.bot.builder.TurnContext; import com.microsoft.bot.builder.UserState; import java.util.concurrent.CompletableFuture; -import org.springframework.stereotype.Component; /** * This IBot implementation can run any type of Dialog. The use of type parameterization is to diff --git a/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/MainDialog.java b/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/MainDialog.java index bef2829ca..db1a5ff8f 100644 --- a/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/MainDialog.java +++ b/samples/43.complex-dialog/src/main/java/com/microsoft/bot/sample/complexdialog/MainDialog.java @@ -12,18 +12,13 @@ import com.microsoft.bot.dialogs.WaterfallStepContext; import java.util.Arrays; import java.util.concurrent.CompletableFuture; -import org.springframework.stereotype.Component; -@Component public class MainDialog extends ComponentDialog { private UserState userState; - private StatePropertyAccessor userProfileAccessor; - public MainDialog(UserState withUserState) { super("MainDialog"); - //userProfileAccessor = withUserState.createProperty("UserProfile"); userState = withUserState; addDialog(new TopLevelDialog("TopLevelDialog"));