From e126ec3e24b9a4452cbbe37d33fdc46939fd2072 Mon Sep 17 00:00:00 2001 From: Martin Battaglino Date: Fri, 5 Mar 2021 16:56:24 -0300 Subject: [PATCH 01/13] Add main pom for core-bot sample --- samples/13.core-bot/pom.xml | 253 ++++++++++++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 samples/13.core-bot/pom.xml diff --git a/samples/13.core-bot/pom.xml b/samples/13.core-bot/pom.xml new file mode 100644 index 000000000..a75393538 --- /dev/null +++ b/samples/13.core-bot/pom.xml @@ -0,0 +1,253 @@ + + + + 4.0.0 + + com.microsoft.bot.sample + bot-core + sample + jar + + ${project.groupId}:${project.artifactId} + This package contains a Java Core Bot 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.core.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-qna + 4.6.0-preview9 + + + com.microsoft.bot + bot-ai-luis-v3 + 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.core.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 + + + + + + + + + From 45c6b86f6e1335e3cec4bcfaf3b388d21959a565 Mon Sep 17 00:00:00 2001 From: Martin Battaglino Date: Fri, 5 Mar 2021 16:56:43 -0300 Subject: [PATCH 02/13] Add deploymentTemplates folder --- .../template-with-new-rg.json | 291 ++++++++++++++++++ .../template-with-preexisting-rg.json | 259 ++++++++++++++++ 2 files changed, 550 insertions(+) create mode 100644 samples/13.core-bot/deploymentTemplates/template-with-new-rg.json create mode 100644 samples/13.core-bot/deploymentTemplates/template-with-preexisting-rg.json diff --git a/samples/13.core-bot/deploymentTemplates/template-with-new-rg.json b/samples/13.core-bot/deploymentTemplates/template-with-new-rg.json new file mode 100644 index 000000000..ec2460d3a --- /dev/null +++ b/samples/13.core-bot/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/13.core-bot/deploymentTemplates/template-with-preexisting-rg.json b/samples/13.core-bot/deploymentTemplates/template-with-preexisting-rg.json new file mode 100644 index 000000000..024dcf08d --- /dev/null +++ b/samples/13.core-bot/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'))]" + ] + } + ] +} From ad2792f46125d572fda61c3f0fc3451f48046be5 Mon Sep 17 00:00:00 2001 From: Martin Battaglino Date: Fri, 5 Mar 2021 16:56:57 -0300 Subject: [PATCH 03/13] Add cognitiveModels folder --- .../cognitiveModels/FlightBooking.json | 339 ++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 samples/13.core-bot/cognitiveModels/FlightBooking.json diff --git a/samples/13.core-bot/cognitiveModels/FlightBooking.json b/samples/13.core-bot/cognitiveModels/FlightBooking.json new file mode 100644 index 000000000..89aad31ae --- /dev/null +++ b/samples/13.core-bot/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": [] +} From 6d43db8efda534fcfb09352cd2b78d0e46ac01bf Mon Sep 17 00:00:00 2001 From: Martin Battaglino Date: Fri, 5 Mar 2021 16:57:19 -0300 Subject: [PATCH 04/13] Add main documentation of the sample --- samples/13.core-bot/LICENSE | 21 +++ samples/13.core-bot/README-LUIS.md | 216 +++++++++++++++++++++++++++++ samples/13.core-bot/README.md | 67 +++++++++ 3 files changed, 304 insertions(+) create mode 100644 samples/13.core-bot/LICENSE create mode 100644 samples/13.core-bot/README-LUIS.md create mode 100644 samples/13.core-bot/README.md diff --git a/samples/13.core-bot/LICENSE b/samples/13.core-bot/LICENSE new file mode 100644 index 000000000..21071075c --- /dev/null +++ b/samples/13.core-bot/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/13.core-bot/README-LUIS.md b/samples/13.core-bot/README-LUIS.md new file mode 100644 index 000000000..12bc78ed0 --- /dev/null +++ b/samples/13.core-bot/README-LUIS.md @@ -0,0 +1,216 @@ +# Setting up LUIS via CLI: + +This README contains information on how to create and deploy a LUIS application. When the bot is ready to be deployed to production, we recommend creating a LUIS Endpoint Resource for usage with your LUIS App. + +> _For instructions on how to create a LUIS Application via the LUIS portal, see these Quickstart steps:_ +> 1. _[Quickstart: Create a new app in the LUIS portal][Quickstart-create]_ +> 2. _[Quickstart: Deploy an app in the LUIS portal][Quickstart-deploy]_ + + [Quickstart-create]: https://docs.microsoft.com/azure/cognitive-services/luis/get-started-portal-build-app + [Quickstart-deploy]:https://docs.microsoft.com/azure/cognitive-services/luis/get-started-portal-deploy-app + +## Table of Contents: + +- [Prerequisites](#Prerequisites) +- [Import a new LUIS Application using a local LUIS application](#Import-a-new-LUIS-Application-using-a-local-LUIS-application) +- [How to create a LUIS Endpoint resource in Azure and pair it with a LUIS Application](#How-to-create-a-LUIS-Endpoint-resource-in-Azure-and-pair-it-with-a-LUIS-Application) + +___ + +## [Prerequisites](#Table-of-Contents): + +#### Install Azure CLI >=2.0.61: + +Visit the following page to find the correct installer for your OS: +- https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest + +#### Install LUIS CLI >=2.4.0: + +Open a CLI of your choice and type the following: + +```bash +npm i -g luis-apis@^2.4.0 +``` + +#### LUIS portal account: + +You should already have a LUIS account with either https://luis.ai, https://eu.luis.ai, or https://au.luis.ai. To determine where to create a LUIS account, consider where you will deploy your LUIS applications, and then place them in [the corresponding region][LUIS-Authoring-Regions]. + +After you've created your account, you need your [Authoring Key][LUIS-AKey] and a LUIS application ID. + + [LUIS-Authoring-Regions]: https://docs.microsoft.com/azure/cognitive-services/luis/luis-reference-regions#luis-authoring-regions] + [LUIS-AKey]: https://docs.microsoft.com/azure/cognitive-services/luis/luis-concept-keys#authoring-key + +___ + +## [Import a new LUIS Application using a local LUIS application](#Table-of-Contents) + +### 1. Import the local LUIS application to luis.ai + +```bash +luis import application --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appName "FlightBooking" --in "./cognitiveModels/FlightBooking.json" +``` + +Outputs the following JSON: + +```json +{ + "id": "########-####-####-####-############", + "name": "FlightBooking", + "description": "A LUIS model that uses intent and entities.", + "culture": "en-us", + "usageScenario": "", + "domain": "", + "versionsCount": 1, + "createdDateTime": "2019-03-29T18:32:02Z", + "endpoints": {}, + "endpointHitsCount": 0, + "activeVersion": "0.1", + "ownerEmail": "bot@contoso.com", + "tokenizerVersion": "1.0.0" +} +``` + +For the next step, you'll need the `"id"` value for `--appId` and the `"activeVersion"` value for `--versionId`. + +### 2. Train the LUIS Application + +```bash +luis train version --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appId "LuisAppId" --versionId "LuisAppversion" --wait +``` + +### 3. Publish the LUIS Application + +```bash +luis publish version --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appId "LuisAppId" --versionId "LuisAppversion" --publishRegion "LuisAppPublishRegion" +``` + +> `--region` corresponds to the region you _author_ your application in. The regions available for this are "westus", "westeurope" and "australiaeast".
+> These regions correspond to the three available portals, https://luis.ai, https://eu.luis.ai, or https://au.luis.ai.
+> `--publishRegion` corresponds to the region of the endpoint you're publishing to, (e.g. "westus", "southeastasia", "westeurope", "brazilsouth").
+> See the [reference docs][Endpoint-API] for a list of available publish/endpoint regions. + + [Endpoint-API]: https://westus.dev.cognitive.microsoft.com/docs/services/5819c76f40a6350ce09de1ac/operations/5819c77140a63516d81aee78 + +Outputs the following: + +```json + { + "versionId": "0.1", + "isStaging": false, + "endpointUrl": "https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/########-####-####-####-############", + "region": "westus", + "assignedEndpointKey": null, + "endpointRegion": "westus", + "failedRegions": "", + "publishedDateTime": "2019-03-29T18:40:32Z", + "directVersionPublish": false +} +``` + +To see how to create an LUIS Cognitive Service Resource in Azure, please see [the next README][README-LUIS]. This Resource should be used when you want to move your bot to production. The instructions will show you how to create and pair the resource with a LUIS Application. + + [README-LUIS]: ./README-LUIS.md + +___ + +## [How to create a LUIS Endpoint resource in Azure and pair it with a LUIS Application](#Table-of-Contents) + +### 1. Create a new LUIS Cognitive Services resource on Azure via Azure CLI + +> _Note:_
+> _If you don't have a Resource Group in your Azure subscription, you can create one through the Azure portal or through using:_ +> ```bash +> az group create --subscription "AzureSubscriptionGuid" --location "westus" --name "ResourceGroupName" +> ``` +> _To see a list of valid locations, use `az account list-locations`_ + + +```bash +# Use Azure CLI to create the LUIS Key resource on Azure +az cognitiveservices account create --kind "luis" --name "NewLuisResourceName" --sku "S0" --location "westus" --subscription "AzureSubscriptionGuid" -g "ResourceGroupName" +``` + +The command will output a response similar to the JSON below: + +```json +{ + "endpoint": "https://westus.api.cognitive.microsoft.com/luis/v2.0", + "etag": "\"########-####-####-####-############\"", + "id": "/subscriptions/########-####-####-####-############/resourceGroups/ResourceGroupName/providers/Microsoft.CognitiveServices/accounts/NewLuisResourceName", + "internalId": "################################", + "kind": "luis", + "location": "westus", + "name": "NewLuisResourceName", + "provisioningState": "Succeeded", + "resourceGroup": "ResourceGroupName", + "sku": { + "name": "S0", + "tier": null + }, + "tags": null, + "type": "Microsoft.CognitiveServices/accounts" +} +``` + + + +Take the output from the previous command and create a JSON file in the following format: + +```json +{ + "azureSubscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroup": "ResourceGroupName", + "accountName": "NewLuisResourceName" +} +``` + +### 2. Retrieve ARM access token via Azure CLI + +```bash +az account get-access-token --subscription "AzureSubscriptionGuid" +``` + +This will return an object that looks like this: + +```json +{ + "accessToken": "eyJ0eXAiOiJKVtokentokentokentokentokeng1dCI6Ik4tbEMwbi05REFMcXdodUhZbkhRNjNHZUNYYyIsItokenI6Ik4tbEMwbi05REFMcXdodUhZbkhRNjNHZUNYYyJ9.eyJhdWQiOiJodHRwczovL21hbmFnZW1lbnQuY29yZS53aW5kb3dzLm5ldC8iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNTUzODc3MTUwLCJuYmYiOjE1NTM4NzcxNTAsImV4cCI6MTU1Mzg4MTA1MCwiX2NsYWltX25hbWVzIjp7Imdyb3VwcyI6InNyYzEifSwiX2NsYWltX3NvdXJjZXMiOnsic3JjMSI6eyJlbmRwb2ludCI6Imh0dHBzOi8vZ3JhcGgud2luZG93cy5uZXQvNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3L3VzZXJzL2ZmZTQyM2RkLWJhM2YtNDg0Ny04NjgyLWExNTI5MDA4MjM4Ny9nZXRNZW1iZXJPYmplY3RzIn19LCJhY3IiOiIxIiwiYWlvIjoiQVZRQXEvOEtBQUFBeGVUc201NDlhVHg4RE1mMFlRVnhGZmxxOE9RSC9PODR3QktuSmRqV1FqTkkwbmxLYzB0bHJEZzMyMFZ5bWZGaVVBSFBvNUFFUTNHL0FZNDRjdk01T3M0SEt0OVJkcE5JZW9WU0dzd0kvSkk9IiwiYW1yIjpbIndpYSIsIm1mYSJdLCJhcHBpZCI6IjA0YjA3Nzk1LThkZGItNDYxYS1iYmVlLTAyZjllMWJmN2I0NiIsImFwcGlkYWNyIjoiMCIsImRldmljZWlkIjoiNDhmNDVjNjEtMTg3Zi00MjUxLTlmZWItMTllZGFkZmMwMmE3IiwiZmFtaWx5X25hbWUiOiJHdW0iLCJnaXZlbl9uYW1lIjoiU3RldmVuIiwiaXBhZGRyIjoiMTY3LjIyMC4yLjU1IiwibmFtZSI6IlN0ZXZlbiBHdW0iLCJvaWQiOiJmZmU0MjNkZC1iYTNmLTQ4NDctODY4Mi1hMTUyOTAwODIzODciLCJvbnByZW1fc2lkIjoiUy0xLTUtMjEtMjEyNzUyMTE4NC0xNjA0MDEyOTIwLTE4ODc5Mjc1MjctMjYwOTgyODUiLCJwdWlkIjoiMTAwMzdGRkVBMDQ4NjlBNyIsInJoIjoiSSIsInNjcCI6InVzZXJfaW1wZXJzb25hdGlvbiIsInN1YiI6Ik1rMGRNMWszN0U5ckJyMjhieUhZYjZLSU85LXVFQVVkZFVhNWpkSUd1Nk0iLCJ0aWQiOiI3MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDciLCJ1bmlxdWVfbmFtZSI6InN0Z3VtQG1pY3Jvc29mdC5jb20iLCJ1cG4iOiJzdGd1bUBtaWNyb3NvZnQuY29tIiwidXRpIjoiT2w2NGN0TXY4RVNEQzZZQWRqRUFtokenInZlciI6IjEuMCJ9.kFAsEilE0mlS1pcpqxf4rEnRKeYsehyk-gz-zJHUrE__oad3QjgDSBDPrR_ikLdweynxbj86pgG4QFaHURNCeE6SzrbaIrNKw-n9jrEtokenlosOxg_0l2g1LeEUOi5Q4gQREAU_zvSbl-RY6sAadpOgNHtGvz3Rc6FZRITfkckSLmsKAOFoh-aWC6tFKG8P52rtB0qVVRz9tovBeNqkMYL49s9ypduygbXNVwSQhm5JszeWDgrFuVFHBUP_iENCQYGQpEZf_KvjmX1Ur1F9Eh9nb4yI2gFlKncKNsQl-tokenK7-tokentokentokentokentokentokenatoken", + "expiresOn": "2200-12-31 23:59:59.999999", + "subscription": "AzureSubscriptionGuid", + "tenant": "tenant-guid", + "tokenType": "Bearer" +} +``` + +The value needed for the next step is the `"accessToken"`. + +### 3. Use `luis add appazureaccount` to pair your LUIS resource with a LUIS Application + +```bash +luis add appazureaccount --in "path/to/created/requestBody.json" --appId "LuisAppId" --authoringKey "LuisAuthoringKey" --armToken "accessToken" +``` + +If successful, it should yield a response like this: + +```json +{ + "code": "Success", + "message": "Operation Successful" +} +``` + +### 4. See the LUIS Cognitive Services' keys + +```bash +az cognitiveservices account keys list --name "NewLuisResourceName" --subscription "AzureSubscriptionGuid" -g "ResourceGroupName" +``` + +This will return an object that looks like this: + +```json +{ + "key1": "9a69####dc8f####8eb4####399f####", + "key2": "####f99e####4b1a####fb3b####6b9f" +} +``` diff --git a/samples/13.core-bot/README.md b/samples/13.core-bot/README.md new file mode 100644 index 000000000..5eb579ae9 --- /dev/null +++ b/samples/13.core-bot/README.md @@ -0,0 +1,67 @@ +# CoreBot + +Bot Framework v4 core bot sample. + +This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to: + +- Use [LUIS](https://www.luis.ai) to implement core AI capabilities +- Implement a multi-turn conversation using Dialogs +- Handle user interruptions for such things as `Help` or `Cancel` +- Prompt for and validate requests for information from the user + +## Prerequisites + +This sample **requires** prerequisites in order to run. + +### Overview + +This bot uses [LUIS](https://www.luis.ai), an AI based cognitive service, to implement language understanding. + +### Create a LUIS Application to enable language understanding + +The LUIS model for this example can be found under `cognitiveModels/FlightBooking.json` and the LUIS language model setup, training, and application configuration steps can be found [here](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-v4-luis?view=azure-bot-service-4.0&tabs=cs). + +Once you created the LUIS model, update `application.properties` with your `LuisAppId`, `LuisAPIKey` and `LuisAPIHostName`. + +``` + LuisAppId="Your LUIS App Id" + LuisAPIKey="Your LUIS Subscription key here" + LuisAPIHostName="Your LUIS App region here (i.e: westus.api.cognitive.microsoft.com)" +``` + +## 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-core-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) From 72545050c3ffeb2dce6bfffe70a119fb4463edef Mon Sep 17 00:00:00 2001 From: Martin Battaglino Date: Fri, 5 Mar 2021 16:57:39 -0300 Subject: [PATCH 05/13] Add dialogs classes --- .../bot/sample/core/BookingDialog.java | 128 +++++++++++ .../bot/sample/core/CancelAndHelpDialog.java | 75 +++++++ .../bot/sample/core/DateResolverDialog.java | 114 ++++++++++ .../microsoft/bot/sample/core/DialogBot.java | 120 ++++++++++ .../microsoft/bot/sample/core/MainDialog.java | 206 ++++++++++++++++++ 5 files changed, 643 insertions(+) create mode 100644 samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/BookingDialog.java create mode 100644 samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/CancelAndHelpDialog.java create mode 100644 samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/DateResolverDialog.java create mode 100644 samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/DialogBot.java create mode 100644 samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/MainDialog.java diff --git a/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/BookingDialog.java b/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/BookingDialog.java new file mode 100644 index 000000000..5af80feb0 --- /dev/null +++ b/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/BookingDialog.java @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.core; + +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(stepContext.getResult().toString()); + + 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(stepContext.getResult().toString()); + + 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(stepContext.getResult().toString()); + + 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/13.core-bot/src/main/java/com/microsoft/bot/sample/core/CancelAndHelpDialog.java b/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/CancelAndHelpDialog.java new file mode 100644 index 000000000..393c04c5b --- /dev/null +++ b/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/CancelAndHelpDialog.java @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.core; + +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/13.core-bot/src/main/java/com/microsoft/bot/sample/core/DateResolverDialog.java b/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/DateResolverDialog.java new file mode 100644 index 000000000..bbdbf8c0e --- /dev/null +++ b/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/DateResolverDialog.java @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.core; + +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() { + { + setTimex(timex); + } + }; + List dateTimeResolutions = new ArrayList() { + { + 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/13.core-bot/src/main/java/com/microsoft/bot/sample/core/DialogBot.java b/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/DialogBot.java new file mode 100644 index 000000000..3826b7f54 --- /dev/null +++ b/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/DialogBot.java @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.core; + +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/13.core-bot/src/main/java/com/microsoft/bot/sample/core/MainDialog.java b/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/MainDialog.java new file mode 100644 index 000000000..51a3bbe56 --- /dev/null +++ b/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/MainDialog.java @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.core; + +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 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. + */ + public MainDialog(FlightBookingRecognizer withLuisRecognizer, BookingDialog bookingDialog) { + super("MainDialog"); + + luisRecognizer = withLuisRecognizer; + + 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 appsettings.json 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); + stepContext.getContext().sendActivity(getWeatherMessage); + break; + 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); + stepContext.getContext().sendActivity(didntUnderstandMessage); + break; + } + return 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); + 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) { + // 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); + 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 stepContext.replaceDialog(getInitialDialogId(), promptMessage); + } +} From 7c8a445524122bf5e74295010a2eb6e2671af271 Mon Sep 17 00:00:00 2001 From: Martin Battaglino Date: Fri, 5 Mar 2021 16:57:49 -0300 Subject: [PATCH 06/13] Add resources folder --- .../src/main/resources/application.properties | 6 +++ .../src/main/resources/cards/welcomeCard.json | 46 +++++++++++++++++++ .../src/main/resources/log4j2.json | 18 ++++++++ 3 files changed, 70 insertions(+) create mode 100644 samples/13.core-bot/src/main/resources/application.properties create mode 100644 samples/13.core-bot/src/main/resources/cards/welcomeCard.json create mode 100644 samples/13.core-bot/src/main/resources/log4j2.json diff --git a/samples/13.core-bot/src/main/resources/application.properties b/samples/13.core-bot/src/main/resources/application.properties new file mode 100644 index 000000000..255d7cd56 --- /dev/null +++ b/samples/13.core-bot/src/main/resources/application.properties @@ -0,0 +1,6 @@ +MicrosoftAppId= +MicrosoftAppPassword= +LuisAppId= +LuisAPIKey= +LuisAPIHostName= +server.port=3978 diff --git a/samples/13.core-bot/src/main/resources/cards/welcomeCard.json b/samples/13.core-bot/src/main/resources/cards/welcomeCard.json new file mode 100644 index 000000000..9b6389e39 --- /dev/null +++ b/samples/13.core-bot/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/13.core-bot/src/main/resources/log4j2.json b/samples/13.core-bot/src/main/resources/log4j2.json new file mode 100644 index 000000000..67c0ad530 --- /dev/null +++ b/samples/13.core-bot/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"} + } + } + } +} From e27bcbb13ee7034dac8eb55836823d66fdb85aae Mon Sep 17 00:00:00 2001 From: Martin Battaglino Date: Fri, 5 Mar 2021 16:58:00 -0300 Subject: [PATCH 07/13] Add webapp folder --- .../src/main/webapp/META-INF/MANIFEST.MF | 3 + .../src/main/webapp/WEB-INF/web.xml | 12 + .../13.core-bot/src/main/webapp/index.html | 417 ++++++++++++++++++ 3 files changed, 432 insertions(+) create mode 100644 samples/13.core-bot/src/main/webapp/META-INF/MANIFEST.MF create mode 100644 samples/13.core-bot/src/main/webapp/WEB-INF/web.xml create mode 100644 samples/13.core-bot/src/main/webapp/index.html diff --git a/samples/13.core-bot/src/main/webapp/META-INF/MANIFEST.MF b/samples/13.core-bot/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 000000000..254272e1c --- /dev/null +++ b/samples/13.core-bot/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Class-Path: + diff --git a/samples/13.core-bot/src/main/webapp/WEB-INF/web.xml b/samples/13.core-bot/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..383c19004 --- /dev/null +++ b/samples/13.core-bot/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/13.core-bot/src/main/webapp/index.html b/samples/13.core-bot/src/main/webapp/index.html new file mode 100644 index 000000000..750c0f776 --- /dev/null +++ b/samples/13.core-bot/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
+
+
+
+
+ +
+ + From 0d47e1cc5e9285925eb158c3d0d9be37a1fb6a1b Mon Sep 17 00:00:00 2001 From: Martin Battaglino Date: Fri, 5 Mar 2021 16:58:27 -0300 Subject: [PATCH 08/13] Add bot for sample --- .../bot/sample/core/DialogAndWelcomeBot.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/DialogAndWelcomeBot.java diff --git a/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/DialogAndWelcomeBot.java b/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/DialogAndWelcomeBot.java new file mode 100644 index 000000000..5d15bf417 --- /dev/null +++ b/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/DialogAndWelcomeBot.java @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.core; + +import com.codepoetics.protonpack.collectors.CompletableFutures; +import com.microsoft.applicationinsights.core.dependencies.apachecommons.io.IOUtils; +import com.microsoft.applicationinsights.core.dependencies.apachecommons.lang3.StringUtils; +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 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()); + + return new Attachment() {{ + setContentType("application/vnd.microsoft.card.adaptive"); + setContent(Serialization.jsonToTree(adaptiveCardJson)); + }}; + + } catch (IOException e) { + e.printStackTrace(); + return new Attachment(); + } + } +} From ce7bcacaf8b8f87069d81ad3a6e7b113abf11231 Mon Sep 17 00:00:00 2001 From: Martin Battaglino Date: Fri, 5 Mar 2021 16:58:35 -0300 Subject: [PATCH 09/13] Add BookingDetails model --- .../bot/sample/core/BookingDetails.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/BookingDetails.java diff --git a/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/BookingDetails.java b/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/BookingDetails.java new file mode 100644 index 000000000..4e95a5807 --- /dev/null +++ b/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/BookingDetails.java @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.core; + +/** + * 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) { + this.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) { + this.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) { + this.travelDate = withTravelDate; + } +} From 32532a718a2066ca0d4ef45e52b3547c453a8f3d Mon Sep 17 00:00:00 2001 From: Martin Battaglino Date: Fri, 5 Mar 2021 16:58:43 -0300 Subject: [PATCH 10/13] Add recognizer for LUIS model --- .../sample/core/FlightBookingRecognizer.java | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/FlightBookingRecognizer.java diff --git a/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/FlightBookingRecognizer.java b/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/FlightBookingRecognizer.java new file mode 100644 index 000000000..da2533aac --- /dev/null +++ b/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/FlightBookingRecognizer.java @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +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.Recognizer; +import com.microsoft.bot.builder.RecognizerResult; +import com.microsoft.bot.builder.TurnContext; +import com.microsoft.bot.integration.Configuration; +import org.apache.commons.lang3.StringUtils; + +import java.util.concurrent.CompletableFuture; + +/** + * The class in charge of recognizing the booking information. + */ +public class FlightBookingRecognizer implements Recognizer { + private LuisRecognizer recognizer; + + /** + * The constructor of the FlightBookingRecognizer class. + * @param configuration The Configuration object to use. + */ + public FlightBookingRecognizer(Configuration configuration) { + 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"), + String.format("https://%s", 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) { + { + setIncludeInstanceData(true); + } + }; + + this.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); + } +} From c344e9b1004ef8fa31bdd5eb5163bac546191082 Mon Sep 17 00:00:00 2001 From: Martin Battaglino Date: Fri, 5 Mar 2021 16:58:53 -0300 Subject: [PATCH 11/13] Add Startup file --- .../bot/sample/core/Application.java | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/Application.java diff --git a/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/Application.java b/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/Application.java new file mode 100644 index 000000000..3b73661df --- /dev/null +++ b/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/Application.java @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.bot.sample.core; + +import com.microsoft.bot.builder.Bot; +import com.microsoft.bot.builder.ConversationState; +import com.microsoft.bot.builder.Storage; +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; +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.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. + *

+ * + * @return The Bot implementation for this application. + */ + @Bean + public Bot getBot( + ) { + Storage storage = this.getStorage(); + UserState userState = this.getUserState(storage); + ConversationState conversationState = this.getConversationState(storage); + Dialog rootDialog = this.getRootDialog(); + return new DialogAndWelcomeBot(conversationState, userState, rootDialog); + } + + /** + * Returns a FlightBookingRecognizer object. + * @return The FlightBookingRecognizer. + */ + @Bean + public FlightBookingRecognizer getFlightBookingRecognizer() { + Configuration configuration = this.getConfiguration(); + return new FlightBookingRecognizer(configuration); + } + + /** + * Returns a BookingDialog object. + * @return The BookingDialog. + */ + @Bean + public BookingDialog getBookingDialog() { + return new BookingDialog(); + } + + /** + * 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() { + FlightBookingRecognizer flightBookingRecognizer = this.getFlightBookingRecognizer(); + BookingDialog bookingDialog = this.getBookingDialog(); + return new MainDialog(flightBookingRecognizer, bookingDialog); + } + + /** + * 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); + } +} + From cebec66006d02351de3aa23ad62309b52e8e08de Mon Sep 17 00:00:00 2001 From: Martin Battaglino Date: Fri, 5 Mar 2021 16:58:59 -0300 Subject: [PATCH 12/13] Add package-info --- .../java/com/microsoft/bot/sample/core/package-info.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/package-info.java diff --git a/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/package-info.java b/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/package-info.java new file mode 100644 index 000000000..c08ec99b7 --- /dev/null +++ b/samples/13.core-bot/src/main/java/com/microsoft/bot/sample/core/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 core-bot sample. + */ +package com.microsoft.bot.sample.core; From 28c5c68fb34e3047a9ba5692f27a4b82035c8e20 Mon Sep 17 00:00:00 2001 From: Martin Battaglino Date: Fri, 5 Mar 2021 16:59:07 -0300 Subject: [PATCH 13/13] Add empty test for sample --- .../java/com/microsoft/bot/sample/core/ApplicationTest.java | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 samples/13.core-bot/src/test/java/com/microsoft/bot/sample/core/ApplicationTest.java diff --git a/samples/13.core-bot/src/test/java/com/microsoft/bot/sample/core/ApplicationTest.java b/samples/13.core-bot/src/test/java/com/microsoft/bot/sample/core/ApplicationTest.java new file mode 100644 index 000000000..d194bed47 --- /dev/null +++ b/samples/13.core-bot/src/test/java/com/microsoft/bot/sample/core/ApplicationTest.java @@ -0,0 +1,2 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License.