diff --git a/samples/javascript_nodejs/48.customQABot-all-features/.env b/samples/javascript_nodejs/48.customQABot-all-features/.env index 5a6a3fe61b..e365e7caf1 100644 --- a/samples/javascript_nodejs/48.customQABot-all-features/.env +++ b/samples/javascript_nodejs/48.customQABot-all-features/.env @@ -5,9 +5,10 @@ MicrosoftAppTenantId= ProjectName= LanguageEndpointKey= +LanguageManagedIdentityClientId= LanguageEndpointHostName= DefaultAnswer= DefaultWelcomeMessage= EnablePreciseAnswer= true DisplayPreciseAnswerOnly= false -UseTeamsAdaptiveCard= false \ No newline at end of file +UseTeamsAdaptiveCard= false diff --git a/samples/javascript_nodejs/48.customQABot-all-features/README.md b/samples/javascript_nodejs/48.customQABot-all-features/README.md index 702d3101a5..aa231b7a56 100644 --- a/samples/javascript_nodejs/48.customQABot-all-features/README.md +++ b/samples/javascript_nodejs/48.customQABot-all-features/README.md @@ -29,11 +29,36 @@ This bot was created using [Bot Framework][BF]. - Go to `Deploy knowledge base` and click on `Deploy`. ### Connect your bot to the project. -Follow these steps to update [.env file](.env). +There are two ways the bot could authenticate to the Language resource. + +Pick one and follow these steps to update [.env file](.env) accordingly. + +1. Using an `Endpoint Key`: _provides an easier configuration by using a secret. Great way to test the bot locally_. + - In the [Azure Portal][Azure], go to your resource. - Go to `Keys and Endpoint` under Resource Management. - Copy one of the keys as value of `LanguageEndpointKey` and Endpoint as value of `LanguageEndpointHostName` in [.env file](.env). - `ProjectName` is the name of the project created in [Language Studio][LS]. +- `LanguageManagedIdentityClientId` is not needed when using an Endpoint Key, so you can remove it from [.env file](.env). + +2. Using a `User Managed Identity` resource: _provides a more complex configuration by using a User Managed Identity resource. Great way to authenticate without the need of a secret_. +- Create a [User Managed Identity][create-msi] resource in the same region as the Language resource. + - Copy the `ClientId` as value of `LanguageManagedIdentityClientId` in [.env file](.env). +- In the [Azure Portal][Azure], go to the WebApp resource, where the bot is hosted. +- Go to `Identity` under Settings and select `User assigned`. More information on Identity assignment can be found [here][webapp-msi]. +- Click on `Add` and select the User Managed Identity created in the previous step. +- Click `Save` to assign the User Managed Identity to the WebApp resource. + - This will allow the WebApp to communicate with the Language resource using the User Managed Identity. +- In the [Azure Portal][Azure], go to the Language resource. +- Assign the following role in the `Access Control (IAM)` section. More information on role assignment can be found [here][language-custom-role]. + - `Cognitive Services User`: _this role is required so the Managed Identity can access the keys of the Cognitive Service resource_. +- In the Language resource, go to `Keys and Endpoint` under Resource Management. +- Copy the `Endpoint` as value of `LanguageEndpointHostName` in [.env file](.env). +- `ProjectName` is the name of the project created in [Language Studio][LS]. +- `LanguageEndpointKey` is not needed when using a User Managed Identity, so you can remove it from [.env file](.env). + +> [!NOTE] +> This method requires [the bot to be deployed in Azure][deploy-bot], so the User Managed Identity can authenticate to the Language resource to get access to the keys. ## To try this sample @@ -188,3 +213,7 @@ If you are new to Microsoft Azure, please refer to [Getting started with Azure][ [Quickstart]: https://docs.microsoft.com/azure/cognitive-services/language-service/question-answering/quickstart/sdk [Azure]: https://portal.azure.com/ [BFE]: https://github.com/Microsoft/BotFramework-Emulator/releases +[deploy-bot]: #deploy-the-bot-to-azure +[create-msi]: https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-manage-user-assigned-managed-identities?pivots=identity-mi-methods-azp#create-a-user-assigned-managed-identity +[language-custom-role]: https://learn.microsoft.com/en-us/azure/operator-service-manager/how-to-create-user-assigned-managed-identity#assign-custom-role-1 +[webapp-msi]: https://learn.microsoft.com/en-us/azure/app-service/overview-managed-identity?tabs=portal%2Chttp diff --git a/samples/javascript_nodejs/48.customQABot-all-features/dialogs/rootDialog.js b/samples/javascript_nodejs/48.customQABot-all-features/dialogs/rootDialog.js index 3d628f5109..9562118aa3 100644 --- a/samples/javascript_nodejs/48.customQABot-all-features/dialogs/rootDialog.js +++ b/samples/javascript_nodejs/48.customQABot-all-features/dialogs/rootDialog.js @@ -28,7 +28,7 @@ const INCLUDE_UNSTRUCTURED_SOURCES = true; /** * Creates QnAMakerDialog instance with provided configuraton values. */ -const createQnAMakerDialog = (knowledgeBaseId, endpointKey, endpointHostName, defaultAnswer, enablePreciseAnswerRaw, displayPreciseAnswerOnlyRaw, useTeamsAdaptiveCard) => { +const createQnAMakerDialog = (knowledgeBaseId, endpointKey, managedIdentityClientId, endpointHostName, defaultAnswer, enablePreciseAnswerRaw, displayPreciseAnswerOnlyRaw, useTeamsAdaptiveCard) => { let noAnswerActivity; if (typeof defaultAnswer === 'string' && defaultAnswer !== '') { noAnswerActivity = MessageFactory.text(defaultAnswer); @@ -36,7 +36,7 @@ const createQnAMakerDialog = (knowledgeBaseId, endpointKey, endpointHostName, de const qnaMakerDialog = new QnAMakerDialog( knowledgeBaseId, - endpointKey, + undefined, endpointHostName, // @ts-ignore noAnswerActivity, @@ -48,6 +48,12 @@ const createQnAMakerDialog = (knowledgeBaseId, endpointKey, endpointHostName, de RANKER_TYPE ); + if (managedIdentityClientId) { + qnaMakerDialog.withManagedIdentityClientId(managedIdentityClientId); + } else { + qnaMakerDialog.withEndpointKey(endpointKey); + } + qnaMakerDialog.enablePreciseAnswer = enablePreciseAnswerRaw === 'true'; qnaMakerDialog.displayPreciseAnswerOnly = displayPreciseAnswerOnlyRaw === 'true'; qnaMakerDialog.qnaServiceType = ServiceType.language; @@ -63,21 +69,22 @@ class RootDialog extends ComponentDialog { /** * Root dialog for this bot. Creates a QnAMakerDialog. * @param {string} knowledgeBaseId Knowledge Base ID of the QnA Maker instance. - * @param {string} endpointKey Endpoint key needed to query QnA Maker. + * @param {any} endpointKey (optional) Endpoint key needed to query QnA Maker. + * @param {any} managedIdentityClientId (optional) Managed identity client ID to use for authentication. * @param {string} endpointHostName Host name of the QnA Maker instance. * @param {string} defaultAnswer (optional) Text used to create a fallback response when QnA Maker doesn't have an answer for a question. * @param {string} enablePreciseAnswer * @param {string} displayPreciseAnswerOnly */ - constructor(knowledgeBaseId, endpointKey, endpointHostName, defaultAnswer, enablePreciseAnswer, displayPreciseAnswerOnly, useTeamsAdaptiveCard) { + constructor(knowledgeBaseId, endpointKey, managedIdentityClientId, endpointHostName, defaultAnswer, enablePreciseAnswer, displayPreciseAnswerOnly, useTeamsAdaptiveCard) { super(ROOT_DIALOG); // Initial waterfall dialog. this.addDialog(new WaterfallDialog(INITIAL_DIALOG, [ this.startInitialDialog.bind(this) ])); - this.addDialog(createQnAMakerDialog(knowledgeBaseId, endpointKey, endpointHostName, defaultAnswer, enablePreciseAnswer, displayPreciseAnswerOnly, useTeamsAdaptiveCard)); + this.addDialog(createQnAMakerDialog(knowledgeBaseId, endpointKey, managedIdentityClientId, endpointHostName, defaultAnswer, enablePreciseAnswer, displayPreciseAnswerOnly, useTeamsAdaptiveCard)); this.initialDialogId = INITIAL_DIALOG; } diff --git a/samples/javascript_nodejs/48.customQABot-all-features/index.js b/samples/javascript_nodejs/48.customQABot-all-features/index.js index 57f8967b51..641e6fa064 100644 --- a/samples/javascript_nodejs/48.customQABot-all-features/index.js +++ b/samples/javascript_nodejs/48.customQABot-all-features/index.js @@ -16,17 +16,16 @@ const restify = require('restify'); // Import required bot services. // See https://aka.ms/bot-services to learn more about the different parts of a bot. -const { BotFrameworkAdapter, ConversationState, MemoryStorage, UserState } = require('botbuilder'); +const { CloudAdapter, ConversationState, MemoryStorage, UserState, ConfigurationBotFrameworkAuthentication } = require('botbuilder'); const { CustomQABot } = require('./bots/CustomQABot'); const { RootDialog } = require('./dialogs/rootDialog'); +const botFrameworkAuthentication = new ConfigurationBotFrameworkAuthentication(process.env); + // Create adapter. -// See https://aka.ms/about-bot-adapter to learn more about adapters. -const adapter = new BotFrameworkAdapter({ - appId: process.env.MicrosoftAppId, - appPassword: process.env.MicrosoftAppPassword -}); +// See https://aka.ms/about-bot-adapter to learn more about how bots work. +const adapter = new CloudAdapter(botFrameworkAuthentication); // Catch-all for errors. adapter.onTurnError = async (context, error) => { @@ -65,13 +64,20 @@ if (!endpointHostName?.startsWith('https://')) { endpointHostName = 'https://' + endpointHostName; } +const managedIdentityClientId = process.env.LanguageManagedIdentityClientId; + // To support backward compatibility for Key Names, fallback to process.env.QnAAuthKey. const endpointKey = process.env.LanguageEndpointKey || process.env.QnAAuthKey; +if (!managedIdentityClientId?.trim() && !endpointKey?.trim()) { + throw new Error('Either LanguageManagedIdentityClientId or LanguageEndpointKey should be set.'); +} + // Create the main dialog. const dialog = new RootDialog( process.env.ProjectName ?? '', - endpointKey ?? '', + endpointKey, + managedIdentityClientId, endpointHostName, process.env.DefaultAnswer ?? '', process.env.EnablePreciseAnswer?.toLowerCase() ?? 'false', @@ -83,6 +89,8 @@ const bot = new CustomQABot(conversationState, userState, dialog); // 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'); @@ -91,8 +99,6 @@ server.listen(process.env.port || process.env.PORT || 3978, function() { // Listen for incoming requests. server.post('/api/messages', async (req, res) => { - adapter.processActivity(req, res, async (turnContext) => { - // Route the message to the bot's main handler. - await bot.run(turnContext); - }); + // Route received a request to adapter for processing + await adapter.process(req, res, (context) => bot.run(context)); });