diff --git a/README.md b/README.md index faa8edd72e..8ea7be811f 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Samples are designed to illustrate functionality you'll need to implement to bui |19|Custom dialogs | Demonstrates complex conversation flow using the Dialogs library. |[.NET Core][cs#19]|[JavaScript][js#19]|[Python][py#19]|[Java][java#19] |21|Application Insights | Demonstrates how to add telemetry logging to your bot, storing telemetry within Application Insights.|[.NET Core][cs#21] |[JavaScript][js#21] ||[Java][java#21] |23|Facebook events | Integrate and consume Facebook specific payloads, such as post-backs, quick replies and opt-in events.|[.NET Core][cs#23] |[JavaScript][js#23] |[Python][py#23]|[Java][java#23] -|42|Scale out | Demonstrates how you can build your own state solution from the ground up that supports scaled out deployment with ETag based optimistic locking. |[.NET Core][cs#42] | |[Python][py#42]|[Java][java#42] +|42|Scale out | Demonstrates how you can build your own state solution from the ground up that supports scaled out deployment with ETag based optimistic locking. |[.NET Core][cs#42] |[JavaScript][js#42] |[Python][py#42]|[Java][java#42] |44|Basic custom prompts | Demonstrates how to implement your own _basic_ prompts to ask the user for information. |[.NET Core][cs#44]|[JavaScript][js#44]|[Python][py#44]|[Java][java#44] |47|Inspection middleware | Demonstrates how to use middleware to allow the Bot Framework Emulator to debug traffic into and out of the bot in addition to looking at the current state of the bot. | [.NET Core][cs#47] | [JavaScript][js#47] |[Python][py#47]|[Java][java#47] |70|Styling webchat | This sample shows how to create a web page with custom Web Chat component.| | [ECMAScript 6][es#70] | @@ -179,6 +179,7 @@ A [collection of **experimental** samples](./experimental) exist, intended to pr [js#23]:samples/javascript_nodejs/23.facebook-events [js#24]:samples/javascript_nodejs/24.bot-authentication-msgraph [js#40]:samples/javascript_nodejs/40.timex-resolution +[js#42]:samples/javascript_nodejs/42.scale-out [js#43]:samples/javascript_nodejs/43.complex-dialog [js#44]:samples/javascript_nodejs/44.prompt-for-user-input [js#45]:samples/javascript_nodejs/45.state-management diff --git a/samples/javascript_nodejs/42.scale-out/.eslintrc.js b/samples/javascript_nodejs/42.scale-out/.eslintrc.js new file mode 100644 index 0000000000..ac85640edb --- /dev/null +++ b/samples/javascript_nodejs/42.scale-out/.eslintrc.js @@ -0,0 +1,15 @@ +/* eslint-disable */ +module.exports = { + "extends": "standard", + "rules": { + "semi": [2, "always"], + "indent": [2, 4], + "no-return-await": 0, + "space-before-function-paren": [2, { + "named": "never", + "anonymous": "never", + "asyncArrow": "always" + }], + "template-curly-spacing": [2, "always"] + } +}; diff --git a/samples/javascript_nodejs/42.scale-out/README.md b/samples/javascript_nodejs/42.scale-out/README.md new file mode 100644 index 0000000000..4ae7827e02 --- /dev/null +++ b/samples/javascript_nodejs/42.scale-out/README.md @@ -0,0 +1,73 @@ +# Scale Out + +Bot Framework v4 bot Scale Out sample + +This bot has been created using [Bot Framework](https://dev.botframework.com), is shows how to use a custom storage solution that supports a deployment scaled out across multiple machines. + +The custom storage solution is implemented against memory for testing purposes and against Azure Blob Storage. The sample shows how storage solutions with different policies can be implemented and integrated with the framework. The solution makes use of the standard HTTP ETag/If-Match mechanisms commonly found on cloud storage technologies. + +## Prerequisites + +- [Node.js](https://nodejs.org) version 16.16.0 or higher + + ```bash + # determine node version + node --version + ``` +- Update `.env` with required configuration settings + - MicrosoftAppId + - MicrosoftAppPassword + - ConnectionName + +## To try this sample + +- Clone the repository + + ```bash + git clone https://github.com/microsoft/botbuilder-samples.git + ``` + +- In a terminal, navigate to `samples/javascript_nodejs/42.scale-out` + + ```bash + cd samples/javascript_nodejs/42.scale-out + ``` + +- Install modules + + ```bash + npm install + ``` + +- Start the bot + + ```bash + npm start + ``` + +## 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` + +## 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) +- [Implementing custom storage for you bot](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-custom-storage?view=azure-bot-service-4.0) +- [Bot Storage](https://docs.microsoft.com/en-us/azure/bot-service/dotnet/bot-builder-dotnet-state?view=azure-bot-service-3.0&viewFallbackFrom=azure-bot-service-4.0) +- [HTTP ETag](https://en.wikipedia.org/wiki/HTTP_ETag) +- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) +- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) +- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) +- [.NET Core CLI tools](https://docs.microsoft.com/en-us/dotnet/core/tools/?tabs=netcore2x) +- [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) diff --git a/samples/javascript_nodejs/42.scale-out/blobStore.js b/samples/javascript_nodejs/42.scale-out/blobStore.js new file mode 100644 index 0000000000..bbe6a41a5e --- /dev/null +++ b/samples/javascript_nodejs/42.scale-out/blobStore.js @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const { BlobServiceClient, StorageSharedKeyCredential } = require('@azure/storage-blob'); + +class BlobStore { + constructor(accountName, accountKey, containerName) { + if (!accountName) { + throw new Error('accountName is required'); + } + + if (!accountKey) { + throw new Error('accountKey is required'); + } + + if (!containerName) { + throw new Error('containerName is required'); + } + + const sharedKeyCredential = new StorageSharedKeyCredential(accountName, accountKey); + const blobServiceClient = new BlobServiceClient(`https://${ accountName }.blob.core.windows.net`, sharedKeyCredential); + this.containerClient = blobServiceClient.getContainerClient(containerName); + } + + async loadAsync(key) { + if (!key) { + throw new Error('key is required'); + } + + const blobClient = this.containerClient.getBlockBlobClient(key); + try { + const downloadBlockBlobResponse = await blobClient.download(); + const content = await streamToString(downloadBlockBlobResponse.readableStreamBody); + const obj = JSON.parse(content); + const etag = downloadBlockBlobResponse.properties.etag; + return { content: obj, etag: etag }; + } catch (error) { + if (error.statusCode === 404) { + return { content: {}, etag: null }; + } + throw error; + } + } + + async saveAsync(key, obj, etag) { + if (!key) { + throw new Error('key is required'); + } + + if (!obj) { + throw new Error('obj is required'); + } + + const blobClient = this.containerClient.getBlockBlobClient(key); + blobClient.properties.contentType = 'application/json'; + const content = JSON.stringify(obj); + if (etag) { + try { + await blobClient.upload(content, content.length, { conditions: { ifMatch: etag } }); + } catch (error) { + if (error.statusCode === 412) { + return false; + } + throw error; + } + } else { + await blobClient.upload(content, content.length); + } + + return true; + } +} + +async function streamToString(readableStream) { + return new Promise((resolve, reject) => { + const chunks = []; + readableStream.on('data', (data) => { + chunks.push(data.toString()); + }); + readableStream.on('end', () => { + resolve(chunks.join('')); + }); + readableStream.on('error', reject); + }); +} + +module.exports.BlobStore = BlobStore; diff --git a/samples/javascript_nodejs/42.scale-out/bots/scaleoutBot.js b/samples/javascript_nodejs/42.scale-out/bots/scaleoutBot.js new file mode 100644 index 0000000000..dd5413a3be --- /dev/null +++ b/samples/javascript_nodejs/42.scale-out/bots/scaleoutBot.js @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const { ActivityHandler } = require('botbuilder'); +const { MemoryStore } = require('../memoryStore'); +const { DialogHost } = require('../dialogHost'); + +class ScaleoutBot extends ActivityHandler { + /** + * + * @param {Dialog} dialog + */ + constructor(dialog) { + super(); + if (!dialog) throw new Error('[ScaleoutBot]: Missing parameter. dialog is required'); + + this.dialog = dialog; + + this.onMessage(async (context, next) => { + // Create the storage key for this conversation. + const key = `${ context.activity.channelId }/conversations/${ context.activity.conversation?.id }`; + + var store = new MemoryStore(); + var dialogHost = new DialogHost(); + + // The execution sits in a loop because there might be a retry if the save operation fails. + while (true) { + // Load any existing state associated with this key + const { oldState, etag } = await store.loadAsync(key); + + // Run the dialog system with the old state and inbound activity, the result is a new state and outbound activities. + const { activities, newState } = await dialogHost.runAsync(this.dialog, context.activity, oldState); + + // Save the updated state associated with this key. + const success = await store.saveAsync(key, newState, etag); + + // Following a successful save, send any outbound Activities, otherwise retry everything. + if (success) { + if (activities.length > 0) { + // This is an actual send on the TurnContext we were given and so will actual do a send this time. + await context.sendActivities(activities); + } + + break; + } + } + + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + } +} + +module.exports.ScaleoutBot = ScaleoutBot; diff --git a/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json b/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json new file mode 100644 index 0000000000..c2c03ef307 --- /dev/null +++ b/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "azureBotId": { + "value": "" + }, + "azureBotSku": { + "value": "S1" + }, + "azureBotRegion": { + "value": "global" + }, + "botEndpoint": { + "value": "" + }, + "appType": { + "value": "MultiTenant" + }, + "appId": { + "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" + }, + "tenantId": { + "value": "" + } + } +} \ No newline at end of file diff --git a/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json b/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json new file mode 100644 index 0000000000..c4b2909008 --- /dev/null +++ b/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appServiceName": { + "value": "" + }, + "existingAppServicePlanName": { + "value": "" + }, + "existingAppServicePlanLocation": { + "value": "" + }, + "newAppServicePlanName": { + "value": "" + }, + "newAppServicePlanLocation": { + "value": "" + }, + "newAppServicePlanSku": { + "value": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + } + }, + "appType": { + "value": "MultiTenant" + }, + "appId": { + "value": "" + }, + "appSecret": { + "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" + }, + "tenantId": { + "value": "" + } + } +} \ No newline at end of file diff --git a/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployUseExistResourceGroup/readme.md b/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployUseExistResourceGroup/readme.md new file mode 100644 index 0000000000..628f0a9546 --- /dev/null +++ b/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployUseExistResourceGroup/readme.md @@ -0,0 +1,48 @@ +# Usage +The BotApp must be deployed prior to AzureBot. + +Command line: +- az login +- az deployment group create --resource-group --template-file --parameters @ + +# parameters-for-template-BotApp-with-rg: + +- **appServiceName**:(required) The Name of the Bot App Service. + +- (choose an existingAppServicePlan or create a new AppServicePlan) + - **existingAppServicePlanName**: The name of the App Service Plan. + - **existingAppServicePlanLocation**: The location of the App Service Plan. + - **newAppServicePlanName**: The name of the App Service Plan. + - **newAppServicePlanLocation**: The location of the App Service Plan. + - **newAppServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. + +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** + +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. + +- **appSecret**:(required for MultiTenant and SingleTenant) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. + +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. + +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. + +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . + +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource + + + +# parameters-for-template-AzureBot-with-rg: + +- **azureBotId**:(required) The globally unique and immutable bot ID. +- **azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. +- **azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. +- **botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. + +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . + +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource \ No newline at end of file diff --git a/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json b/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json new file mode 100644 index 0000000000..103ca19e27 --- /dev/null +++ b/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json @@ -0,0 +1,119 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "azureBotId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID." + } + }, + "azureBotSku": { + "defaultValue": "S1", + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Allowed values are: F0, S1(default)." + } + }, + "azureBotRegion": { + "type": "string", + "defaultValue": "global", + "metadata": { + "description": "Specifies the location of the new AzureBot. Allowed values are: global(default), westeurope." + } + }, + "botEndpoint": { + "type": "string", + "metadata": { + "description": "Use to handle client messages, Such as https://.azurewebsites.net/api/messages." + } + }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant", + "UserAssignedMSI" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "UMSIName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource used for the Bot's Authentication." + } + }, + "UMSIResourceGroupName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource Group used for the Bot's Authentication." + } + }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } + } + }, + "variables": { + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", + "msiResourceId": "[concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName'))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "msiResourceId": "" + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "msiResourceId": "" + }, + "UserAssignedMSI": { + "tenantId": "[variables('tenantId')]", + "msiResourceId": "[variables('msiResourceId')]" + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]", + "msiResourceId": "[variables('appTypeDef')[parameters('appType')].msiResourceId]" + } + }, + "resources": [ + { + "apiVersion": "2021-05-01-preview", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('azureBotId')]", + "location": "[parameters('azureBotRegion')]", + "kind": "azurebot", + "sku": { + "name": "[parameters('azureBotSku')]" + }, + "properties": { + "displayName": "[parameters('azureBotId')]", + "iconUrl": "https://docs.botframework.com/static/devportal/client/images/bot-framework-default.png", + "endpoint": "[parameters('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "msaAppTenantId": "[variables('appType').tenantId]", + "msaAppMSIResourceId": "[variables('appType').msiResourceId]", + "msaAppType": "[parameters('appType')]", + "luisAppIds": [], + "schemaTransformationVersion": "1.3", + "isCmekEnabled": false, + "isIsolated": false + }, + "dependsOn": [] + } + ] +} \ No newline at end of file diff --git a/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json b/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json new file mode 100644 index 0000000000..be94279ecc --- /dev/null +++ b/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json @@ -0,0 +1,189 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appServiceName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App." + } + }, + "existingAppServicePlanName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the existing App Service Plan used to create the Web App for the bot." + } + }, + "existingAppServicePlanLocation": { + "type": "string", + "metadata": { + "description": "The location of the App Service Plan." + } + }, + "newAppServicePlanName": { + "type": "string", + "metadata": { + "description": "The name of the new App Service Plan." + } + }, + "newAppServicePlanLocation": { + "type": "string", + "metadata": { + "description": "The location of the App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant", + "UserAssignedMSI" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Required for MultiTenant and SingleTenant app types. Defaults to \"\"." + } + }, + "UMSIName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource used for the Bot's Authentication. Defaults to \"\"." + } + }, + "UMSIResourceGroupName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. Defaults to \"\"." + } + }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } + } + }, + "variables": { + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", + "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlanName')), 'createNewAppServicePlan', parameters('existingAppServicePlanName'))]", + "useExistingServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "servicePlanName": "[if(variables('useExistingServicePlan'), parameters('existingAppServicePlanName'), parameters('newAppServicePlanName'))]", + "servicePlanLocation": "[if(variables('useExistingServicePlan'), parameters('existingAppServicePlanLocation'), parameters('newAppServicePlanLocation'))]", + "msiResourceId": "[concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName'))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "identity": { "type": "None" } + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "identity": { "type": "None" } + }, + "UserAssignedMSI": { + "tenantId": "[variables('tenantId')]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[variables('msiResourceId')]": {} + } + } + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]", + "identity": "[variables('appTypeDef')[parameters('appType')].identity]" + } + }, + "resources": [ + { + "comments": "Create a new App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "condition": "[not(variables('useExistingServicePlan'))]", + "name": "[variables('servicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[parameters('newAppServicePlanLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "properties": { + "name": "[variables('servicePlanName')]" + } + }, + { + "comments": "Create a Web App using an App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2015-08-01", + "location": "[variables('servicePlanLocation')]", + "kind": "app", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]" + ], + "name": "[parameters('appServiceName')]", + "identity": "[variables('appType').identity]", + "properties": { + "name": "[parameters('appServiceName')]", + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]", + "siteConfig": { + "appSettings": [ + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "10.14.1" + }, + { + "name": "MicrosoftAppType", + "value": "[parameters('appType')]" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + }, + { + "name": "MicrosoftAppTenantId", + "value": "[variables('appType').tenantId]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + }, + "webSocketsEnabled": true + } + } + } + ] +} \ No newline at end of file diff --git a/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json b/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json new file mode 100644 index 0000000000..44f169e4d5 --- /dev/null +++ b/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "groupName": { + "value": "" + }, + "groupLocation": { + "value": "" + }, + "azureBotId": { + "value": "" + }, + "azureBotSku": { + "value": "S1" + }, + "azureBotRegion": { + "value": "global" + }, + "botEndpoint": { + "value": "" + }, + "appType": { + "value": "MultiTenant" + }, + "appId": { + "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" + }, + "tenantId": { + "value": "" + } + } +} \ No newline at end of file diff --git a/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json b/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json new file mode 100644 index 0000000000..8abb03d597 --- /dev/null +++ b/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "groupName": { + "value": "" + }, + "groupLocation": { + "value": "" + }, + "appServiceName": { + "value": "" + }, + "appServicePlanName": { + "value": "" + }, + "appServicePlanLocation": { + "value": "" + }, + "appServicePlanSku": { + "value": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + } + }, + "appType": { + "value": "MultiTenant" + }, + "appId": { + "value": "" + }, + "appSecret": { + "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" + }, + "tenantId": { + "value": "" + } + } +} \ No newline at end of file diff --git a/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployWithNewResourceGroup/readme.md b/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployWithNewResourceGroup/readme.md new file mode 100644 index 0000000000..23bf7a5a51 --- /dev/null +++ b/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployWithNewResourceGroup/readme.md @@ -0,0 +1,45 @@ +# Usage +The BotApp must be deployed prior to AzureBot. + +Command line: +- az login +- az deployment sub create --template-file --location --parameters @ + +# parameters-for-template-BotApp-new-rg: + +- **groupName**:(required) Specifies the name of the new Resource Group. +- **groupLocation**:(required) Specifies the location of the new Resource Group. + +- **appServiceName**:(required) The location of the App Service Plan. +- **appServicePlanName**:(required) The name of the App Service Plan. +- **appServicePlanLocation**: The location of the App Service Plan. Defaults to use groupLocation. +- **appServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. + +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. +- **appSecret**:(required for MultiTenant and SingleTenant) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . + +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource + + + +# parameters-for-template-AzureBot-new-rg: + +- **groupName**:(required) Specifies the name of the new Resource Group. +- **groupLocation**:(required) Specifies the location of the new Resource Group. + +- **azureBotId**:(required) The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable. +- **azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. +- **azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. +- **botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. + +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . + +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource \ No newline at end of file diff --git a/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json b/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json new file mode 100644 index 0000000000..da68591251 --- /dev/null +++ b/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json @@ -0,0 +1,157 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "groupName": { + "type": "string", + "metadata": { + "description": "Specifies the name of the Resource Group." + } + }, + "groupLocation": { + "type": "string", + "metadata": { + "description": "Specifies the location of the Resource Group." + } + }, + "azureBotId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID." + } + }, + "azureBotSku": { + "type": "string", + "defaultValue": "S1", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "azureBotRegion": { + "type": "string", + "defaultValue": "global", + "metadata": { + "description": "" + } + }, + "botEndpoint": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Use to handle client messages, Such as https://.azurewebsites.net/api/messages." + } + }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant", + "UserAssignedMSI" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } + }, + "UMSIName": { + "type": "string", + "metadata": { + "description": "The User-Assigned Managed Identity Resource used for the Bot's Authentication." + } + }, + "UMSIResourceGroupName": { + "type": "string", + "metadata": { + "description": "The User-Assigned Managed Identity Resource Group used for the Bot's Authentication." + } + } + }, + "variables": { + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", + "msiResourceId": "[concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName'))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "msiResourceId": "" + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "msiResourceId": "" + }, + "UserAssignedMSI": { + "tenantId": "[variables('tenantId')]", + "msiResourceId": "[variables('msiResourceId')]" + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]", + "msiResourceId": "[variables('appTypeDef')[parameters('appType')].msiResourceId]" + } + }, + "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/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "variables": {}, + "resources": [ + { + "apiVersion": "2021-03-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('azureBotId')]", + "location": "[parameters('azureBotRegion')]", + "kind": "azurebot", + "sku": { + "name": "[parameters('azureBotSku')]" + }, + "properties": { + "name": "[parameters('azureBotId')]", + "displayName": "[parameters('azureBotId')]", + "iconUrl": "https://docs.botframework.com/static/devportal/client/images/bot-framework-default.png", + "endpoint": "[parameters('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "msaAppTenantId": "[variables('appType').tenantId]", + "msaAppMSIResourceId": "[variables('appType').msiResourceId]", + "msaAppType": "[parameters('appType')]", + "luisAppIds": [], + "schemaTransformationVersion": "1.3", + "isCmekEnabled": false, + "isIsolated": false + } + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json b/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json new file mode 100644 index 0000000000..ad06507010 --- /dev/null +++ b/samples/javascript_nodejs/42.scale-out/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json @@ -0,0 +1,211 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "groupName": { + "type": "string", + "metadata": { + "description": "Specifies the name of the Resource Group." + } + }, + "groupLocation": { + "type": "string", + "metadata": { + "description": "Specifies the location of the Resource Group." + } + }, + "appServiceName": { + "type": "string", + "metadata": { + "description": "The globally unique name of the Web App." + } + }, + "appServicePlanName": { + "type": "string", + "metadata": { + "description": "The name of the App Service Plan." + } + }, + "appServicePlanLocation": { + "type": "string", + "metadata": { + "description": "The location of the App Service Plan." + } + }, + "appServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } + }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant", + "UserAssignedMSI" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID or User-Assigned Managed Identity Client 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. Required for MultiTenant and SingleTenant app types." + } + }, + "UMSIName": { + "type": "string", + "metadata": { + "description": "The User-Assigned Managed Identity Resource used for the Bot's Authentication." + } + }, + "UMSIResourceGroupName": { + "type": "string", + "metadata": { + "description": "The User-Assigned Managed Identity Resource Group used for the Bot's Authentication." + } + } + }, + "variables": { + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", + "appServicePlanName": "[parameters('appServicePlanName')]", + "resourcesLocation": "[if(empty(parameters('appServicePlanLocation')), parameters('groupLocation'), parameters('appServicePlanLocation'))]", + "appServiceName": "[parameters('appServiceName')]", + "resourceGroupId": "[concat(subscription().id, '/resourceGroups/', parameters('groupName'))]", + "msiResourceId": "[concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName'))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "identity": { "type": "None" } + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "identity": { "type": "None" } + }, + "UserAssignedMSI": { + "tenantId": "[variables('tenantId')]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[variables('msiResourceId')]": {} + } + } + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]", + "identity": "[variables('appTypeDef')[parameters('appType')].identity]" + } + }, + "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/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "variables": {}, + "resources": [ + { + "comments": "Create a new App Service Plan", + "type": "Microsoft.Web/serverfarms", + "name": "[variables('appServicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('appServicePlanSku')]", + "properties": { + "name": "[variables('appServicePlanName')]" + } + }, + { + "comments": "Create a Web App using the new App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2015-08-01", + "location": "[variables('resourcesLocation')]", + "kind": "app", + "dependsOn": [ + "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" + ], + "name": "[variables('appServiceName')]", + "identity": "[variables('appType').identity]", + "properties": { + "name": "[variables('appServiceName')]", + "serverFarmId": "[variables('appServicePlanName')]", + "siteConfig": { + "appSettings": [ + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "10.14.1" + }, + { + "name": "MicrosoftAppType", + "value": "[parameters('appType')]" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + }, + { + "name": "MicrosoftAppTenantId", + "value": "[variables('appType').tenantId]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + }, + "webSocketsEnabled": true + } + } + } + ], + "outputs": {} + } + } + } + ] +} \ No newline at end of file diff --git a/samples/javascript_nodejs/42.scale-out/dialogHost.js b/samples/javascript_nodejs/42.scale-out/dialogHost.js new file mode 100644 index 0000000000..06d87a0404 --- /dev/null +++ b/samples/javascript_nodejs/42.scale-out/dialogHost.js @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const { TurnContext } = require('botbuilder-core'); +const { DialogHostAdapter } = require('./dialogHostAdapter'); +const { RefAccessor } = require('./refAccessor'); + +/** + * The essential code for running a dialog. The execution of the dialog is treated here as a pure function call. + * The input being the existing (or old) state and the inbound Activity and the result being the updated (or new) state + * and the Activities that should be sent. The assumption is that this code can be re-run without causing any + * unintended or harmful side-effects, for example, any outbound service calls made directly from the + * dialog implementation should be idempotent. + */ +class DialogHost { + /** + * A function to run a dialog while buffering the outbound Activities. + * @param {Dialog} dialog The dialog to run. + * @param {IMessageActivity} activity The inbound Activity to run it with. + * @param {JObject} oldState The existing or old state. + * @returns {Array} An array of Activities 'sent' from the dialog as it executed. And the updated or new state. + */ + async runAsync(dialog, activity, oldState) { + // A custom adapter and corresponding TurnContext that buffers any messages sent. + const adapter = new DialogHostAdapter(); + const turnContext = new TurnContext(adapter, activity); + + // Run the dialog using this TurnContext with the existing state. + const newState = await this.runTurnAsync(dialog, turnContext, oldState); + + // The result is a set of activities to send and a replacement state. + return [adapter.activities.toArray(), newState]; + } + + /** + * Execute the turn of the bot. The functionality here closely resembles that which is found in the + * IBot.OnTurnAsync method in an implementation that is using the regular BotFrameworkAdapter. + * Also here in this example the focus is explicitly on Dialogs but the pattern could be adapted + * to other conversation modeling abstractions. + * @param {Dialog} dialog The dialog to be run. + * @param {ITurnContext} turnContext The ITurnContext instance to use. Note this is not the one passed into the IBot OnTurnAsync. + * @param {JObject} state The existing or old state of the dialog. + * @returns {JObject} The updated or new state of the dialog. + */ + async runTurnAsync(dialog, turnContext, state) { + // If we have some state, deserialize it. (This mimics the shape produced by BotState.cs.) + const dialogStateProperty = state?.DialogState; + const dialogState = dialogStateProperty ? JSON.parse(dialogStateProperty) : undefined; + // const dialogState = dialogStateProperty ? StateJsonSerializer.toObject(dialogStateProperty, StateJsonSerializer) : null; + + // A custom accessor is used to pass a handle on the state to the dialog system. + const accessor = new RefAccessor(dialogState); + + // Run the dialog. + await dialog.runAsync(turnContext, accessor); + + // Serialize the result (available as Value on the accessor), and put its value back into a new JObject. + return { DialogState: JSON.stringify(accessor.value) }; + } +} + +module.exports.DialogHost = DialogHost; diff --git a/samples/javascript_nodejs/42.scale-out/dialogHostAdapter.js b/samples/javascript_nodejs/42.scale-out/dialogHostAdapter.js new file mode 100644 index 0000000000..8dbee60424 --- /dev/null +++ b/samples/javascript_nodejs/42.scale-out/dialogHostAdapter.js @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +class DialogHostAdapter { + constructor() { + this._response = []; + } + + get Activities() { + return this._response; + } + + async sendActivities(turnContext, activities, cancellationToken) { + for (const activity of activities) { + this._response.push(activity); + } + + return []; + } + + // Not Implemented + async deleteActivity(turnContext, reference, cancellationToken) { + throw new Error('Not Implemented'); + } + + async updateActivity(turnContext, activity, cancellationToken) { + throw new Error('Not Implemented'); + } +} + +module.exports.DialogHostAdapter = DialogHostAdapter; diff --git a/samples/javascript_nodejs/42.scale-out/dialogs/rootDialog.js b/samples/javascript_nodejs/42.scale-out/dialogs/rootDialog.js new file mode 100644 index 0000000000..fcf82987db --- /dev/null +++ b/samples/javascript_nodejs/42.scale-out/dialogs/rootDialog.js @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const { MessageFactory } = require('botbuilder'); +const { ComponentDialog, DialogSet, DialogTurnStatus, NumberPrompt, WaterfallDialog } = require('botbuilder-dialogs'); + +const NUMBER_PROMPT = 'NUMBER_PROMPT'; +const WATERFALL_DIALOG = 'WATERFALL_DIALOG'; + +class RootDialog extends ComponentDialog { + constructor() { + super('RootDialog'); + // Define the main dialog and its related components. + this.addDialog(new NumberPrompt(NUMBER_PROMPT)); + this.addDialog(new WaterfallDialog(WATERFALL_DIALOG, [ + this.firstStep.bind(this), + this.secondStep.bind(this), + this.finalStep.bind(this) + ])); + + this.initialDialogId = WATERFALL_DIALOG; + } + + /** + * The run method handles the incoming activity (in the form of a TurnContext) and passes it through the dialog system. + * If no dialog is active, it will start the default dialog. + * @param {*} turnContext + * @param {*} accessor + */ + async run(turnContext, accessor) { + const dialogSet = new DialogSet(accessor); + dialogSet.add(this); + + const dialogContext = await dialogSet.createContext(turnContext); + const results = await dialogContext.continueDialog(); + if (results.status === DialogTurnStatus.empty) { + await dialogContext.beginDialog(this.id); + } + } + + /** + * First step in the waterfall dialog. Prompts the user for a command. + */ + async firstStep(stepContext) { + // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog. + const promptOptions = { prompt: 'Enter a number.' }; + + return await stepContext.prompt(NUMBER_PROMPT, promptOptions); + } + + /** + * Second step in the waterfall. + */ + async secondStep(stepContext) { + const first = stepContext.result; + stepContext.values.first = first; + const promptOptions = { prompt: `I have ${ first } now enter another number` }; + return await stepContext.prompt(NUMBER_PROMPT, promptOptions); + } + + /** + * This is the final step in the main waterfall dialog. + */ + async finalStep(stepContext) { + const first = stepContext.values.first; + const second = stepContext.result; + stepContext.values.second = second; + + const msg = `The result of the first minus the second is ${ first - second }.`; + await stepContext.context.sendActivity(MessageFactory.text(msg, msg)); + return await stepContext.endDialog(); + } +} + +module.exports.RootDialog = RootDialog; diff --git a/samples/javascript_nodejs/42.scale-out/index.js b/samples/javascript_nodejs/42.scale-out/index.js new file mode 100644 index 0000000000..89ab2a190f --- /dev/null +++ b/samples/javascript_nodejs/42.scale-out/index.js @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const restify = require('restify'); +const path = require('path'); + +// Read botFilePath and botFileSecret from .env file. +const ENV_FILE = path.join(__dirname, '.env'); +require('dotenv').config({ path: ENV_FILE }); + +// Import required bot services. +// See https://aka.ms/bot-services to learn more about the different parts of a bot. +const { + CloudAdapter, + ConversationState, + UserState, + ConfigurationBotFrameworkAuthentication +} = require('botbuilder'); + +const { MemoryStore } = require('./memoryStore'); +const { ScaleoutBot } = require('./bots/scaleoutBot'); +const { RootDialog } = require('./dialogs/rootDialog'); + +// Create HTTP server. +const server = restify.createServer(); +server.use(restify.plugins.bodyParser()); + +server.listen(process.env.port || process.env.PORT || 3978, function() { + console.log(`\n${ server.name } listening to ${ server.url }`); + console.log('\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator'); + console.log('\nTo talk to your bot, open the emulator select "Open Bot"'); +}); + +const botFrameworkAuthentication = new ConfigurationBotFrameworkAuthentication(process.env); + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about adapters. +const adapter = new CloudAdapter(botFrameworkAuthentication); + +// Define the state store for your bot. See https://aka.ms/about-bot-state to learn more about using MemoryStorage. +// A bot requires a state storage system to persist the dialog and user state between messages. +const memoryStorage = new MemoryStore(); + +// Create conversation and user state with in-memory storage provider. +const conversationState = new ConversationState(memoryStorage); +const userState = new UserState(memoryStorage); + +// Create the main dialog. +const dialog = new RootDialog(userState); + +// Create the bot's main handler. +const bot = new ScaleoutBot(conversationState, userState, dialog); + +// Listen for incoming requests. +server.post('/api/messages', async (req, res) => { + // Route received a request to adapter for processing + await adapter.process(req, res, (context) => bot.run(context)); +}); + +// Catch-all for errors. +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + // NOTE: In production environment, you should consider logging this to Azure + // application insights. See https://aka.ms/bottelemetry for telemetry + // configuration instructions. + console.error(`\n [onTurnError] unhandled error: ${ error }`); + + // Send a trace activity, which will be displayed in Bot Framework Emulator + await context.sendTraceActivity( + 'OnTurnError Trace', + `${ error }`, + 'https://www.botframework.com/schemas/error', + 'TurnError' + ); + + // Send a message to the user + await context.sendActivity('The bot encountered an error or bug.'); + await context.sendActivity('To continue to run this bot, please fix the bot source code.'); + // Clear out state + await conversationState.clear(context); +}; diff --git a/samples/javascript_nodejs/42.scale-out/memoryStore.js b/samples/javascript_nodejs/42.scale-out/memoryStore.js new file mode 100644 index 0000000000..1ecde7931b --- /dev/null +++ b/samples/javascript_nodejs/42.scale-out/memoryStore.js @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +class MemoryStore { + constructor() { + this._store = new Map(); + } + + async loadAsync(key) { + if (this._store.has(key)) { + return this._store.get(key); + } + + return [null, null]; + } + + async saveAsync(key, content, eTag) { + if (eTag !== null && this._store.has(key)) { + if (eTag !== this._store.get(key)[1]) { + return false; + } + } + + this._store.set(key, [content, Math.random().toString()]); + return true; + } +} + +module.exports.MemoryStore = MemoryStore; diff --git a/samples/javascript_nodejs/42.scale-out/package.json b/samples/javascript_nodejs/42.scale-out/package.json new file mode 100644 index 0000000000..5992803772 --- /dev/null +++ b/samples/javascript_nodejs/42.scale-out/package.json @@ -0,0 +1,36 @@ +{ + "name": "scale-out", + "version": "1.0.0", + "description": "Demonstrates how you can build your own state solution from the ground up that supports scaled out deployment with ETag based locking.", + "author": "Generated using Microsoft Bot Builder Yeoman generator v4.22.1", + "license": "MIT", + "main": "index.js", + "scripts": { + "start": "node ./index.js", + "watch": "nodemon ./index.js", + "lint": "eslint .", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/BotBuilder-Samples.git" + }, + "dependencies": { + "botbuilder": "~4.22.1", + "botbuilder-ai": "~4.22.1", + "botbuilder-dialogs": "~4.22.1", + "dotenv": "~8.2.0", + "restify": "~11.1.0" + }, + "devDependencies": { + "eslint": "^7.0.0", + "eslint-config-standard": "^14.1.1", + "eslint-plugin-import": "^2.20.2", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-standard": "^4.0.1", + "mocha": "^7.1.2", + "nodemon": "^2.0.4", + "nyc": "^15.0.1" + } +} diff --git a/samples/javascript_nodejs/42.scale-out/refAccessor.js b/samples/javascript_nodejs/42.scale-out/refAccessor.js new file mode 100644 index 0000000000..4cb0648fa3 --- /dev/null +++ b/samples/javascript_nodejs/42.scale-out/refAccessor.js @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* +This is an accessor for any object. By definition objects (as opposed to values) + are returned by reference in the GetAsync call on the accessor. As such the SetAsync + call is never used. The actual act of saving any state to an external store therefore + cannot be encapsulated in the Accessor implementation itself. And so to facilitate this + the state itself is available as a public property on this class. The reason its here is + because the caller of the constructor could pass in null for the state, in which case + the factory provided on the GetAsync call will be used. +*/ +class RefAccessor { + constructor(value) { + this.Value = value; + } + + get Name() { + return typeof (T); + } + + async getAsync(TurnContext, defaultValueFactory = null) { + if (this.Value == null) { + if (defaultValueFactory == null) { + throw new Error('KeyNotFoundException'); + } + this.Value = defaultValueFactory(); + } + + return this.Value; + } + + // Not Implemented + async deleteAsync(TurnContext) { + throw new Error('NotImplementedException'); + } + + async setAsync(TurnContext, value) { + throw new Error('NotImplementedException'); + } +} + +module.exports.RefAccessor = { RefAccessor }; diff --git a/samples/javascript_nodejs/42.scale-out/store.js b/samples/javascript_nodejs/42.scale-out/store.js new file mode 100644 index 0000000000..9928f32d05 --- /dev/null +++ b/samples/javascript_nodejs/42.scale-out/store.js @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * An ETag aware store definition. + * The interface is defined in terms of JObject to move serialization out of the storage layer + * while still indicating it is JSON, a fact the store may choose to make use of. + */ +class Store { + async loadAsync(key) { + throw new Error('Not implemented'); + } + + async saveAsync(key, content, etag) { + throw new Error('Not implemented'); + } +} + +module.exports.Store = { Store };