From d58b085750179d350e700340cf55c6868a7cdb23 Mon Sep 17 00:00:00 2001 From: Joel Mut Date: Tue, 22 Apr 2025 16:44:19 +0100 Subject: [PATCH 1/3] Add support for MSI in the CQA sample --- .../Dialogs/RootDialog.cs | 73 ++++++++++++++----- .../48.customQABot-all-features/README.md | 40 ++++++++-- .../appsettings.json | 1 + 3 files changed, 90 insertions(+), 24 deletions(-) diff --git a/samples/csharp_dotnetcore/48.customQABot-all-features/Dialogs/RootDialog.cs b/samples/csharp_dotnetcore/48.customQABot-all-features/Dialogs/RootDialog.cs index 1e178275fa..14485bda1c 100644 --- a/samples/csharp_dotnetcore/48.customQABot-all-features/Dialogs/RootDialog.cs +++ b/samples/csharp_dotnetcore/48.customQABot-all-features/Dialogs/RootDialog.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.Builder; @@ -55,10 +54,11 @@ private QnAMakerDialog CreateQnAMakerDialog(IConfiguration configuration) throw new ArgumentException(string.Format(missingConfigError, "LanguageEndpointHostName")); } + var managedIdentityClientId = configuration["LanguageManagedIdentityClientId"]; var endpointKey = configuration["LanguageEndpointKey"]; - if (string.IsNullOrEmpty(endpointKey)) + if (string.IsNullOrEmpty(managedIdentityClientId) && string.IsNullOrEmpty(endpointKey)) { - throw new ArgumentException(string.Format(missingConfigError, "LanguageEndpointKey")); + throw new ArgumentException(string.Format(missingConfigError, "Either LanguageManagedIdentityClientId or LanguageEndpointKey")); } var knowledgeBaseId = configuration["ProjectName"]; @@ -73,23 +73,60 @@ private QnAMakerDialog CreateQnAMakerDialog(IConfiguration configuration) // Create a new instance of QnAMakerDialog with dialogOptions initialized. var noAnswer = MessageFactory.Text(configuration["DefaultAnswer"] ?? string.Empty); - var qnamakerDialog = new QnAMakerDialog(nameof(QnAMakerDialog), knowledgeBaseId, endpointKey, hostname, noAnswer: noAnswer, cardNoMatchResponse: MessageFactory.Text(ActiveLearningCardNoMatchResponse), useTeamsAdaptiveCard: useTeamsAdaptiveCard) + + QnAMakerDialog dialog; + if (!string.IsNullOrEmpty(managedIdentityClientId)) + { + dialog = new QnAMakerDialog( + dialogId: nameof(QnAMakerDialog), + managedIdentityClientId: managedIdentityClientId, + knowledgeBaseId: knowledgeBaseId, + hostName: new Uri(hostname), + noAnswer: noAnswer, + cardNoMatchResponse: MessageFactory.Text(ActiveLearningCardNoMatchResponse), + useTeamsAdaptiveCard: useTeamsAdaptiveCard) + { + Threshold = ScoreThreshold, + ActiveLearningCardTitle = ActiveLearningCardTitle, + CardNoMatchText = ActiveLearningCardNoMatchText, + Top = TopAnswers, + Filters = { }, + QnAServiceType = ServiceType.Language, + EnablePreciseAnswer = enablePreciseAnswer, + DisplayPreciseAnswerOnly = displayPreciseAnswerOnly, + IncludeUnstructuredSources = IncludeUnstructuredSources, + RankerType = RankerType, + IsTest = IsTest, + UseTeamsAdaptiveCard = useTeamsAdaptiveCard + }; + } + else { - Threshold = ScoreThreshold, - ActiveLearningCardTitle = ActiveLearningCardTitle, - CardNoMatchText = ActiveLearningCardNoMatchText, - Top = TopAnswers, - Filters = { }, - QnAServiceType = ServiceType.Language, - EnablePreciseAnswer = enablePreciseAnswer, - DisplayPreciseAnswerOnly = displayPreciseAnswerOnly, - IncludeUnstructuredSources = IncludeUnstructuredSources, - RankerType = RankerType, - IsTest = IsTest, - UseTeamsAdaptiveCard = useTeamsAdaptiveCard - }; + dialog = new QnAMakerDialog( + dialogId: nameof(QnAMakerDialog), + endpointKey: endpointKey, + knowledgeBaseId: knowledgeBaseId, + hostName: hostname, + noAnswer: noAnswer, + cardNoMatchResponse: MessageFactory.Text(ActiveLearningCardNoMatchResponse), + useTeamsAdaptiveCard: useTeamsAdaptiveCard) + { + Threshold = ScoreThreshold, + ActiveLearningCardTitle = ActiveLearningCardTitle, + CardNoMatchText = ActiveLearningCardNoMatchText, + Top = TopAnswers, + Filters = { }, + QnAServiceType = ServiceType.Language, + EnablePreciseAnswer = enablePreciseAnswer, + DisplayPreciseAnswerOnly = displayPreciseAnswerOnly, + IncludeUnstructuredSources = IncludeUnstructuredSources, + RankerType = RankerType, + IsTest = IsTest, + UseTeamsAdaptiveCard = useTeamsAdaptiveCard + }; + } - return qnamakerDialog; + return dialog; } private async Task InitialStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) diff --git a/samples/csharp_dotnetcore/48.customQABot-all-features/README.md b/samples/csharp_dotnetcore/48.customQABot-all-features/README.md index 67db964483..9c62141e26 100644 --- a/samples/csharp_dotnetcore/48.customQABot-all-features/README.md +++ b/samples/csharp_dotnetcore/48.customQABot-all-features/README.md @@ -23,11 +23,35 @@ 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 [appsettings.json](appsettings.json). -- 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 [appsettings.json](appsettings.json). -- `ProjectName` is the name of the project created in [Language Studio][LS]. +There are two ways the bot could authenticate to the Language resource. + +Pick one and follow these steps to update [appsettings.json](appsettings.json) 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 the Language 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 [appsettings.json](appsettings.json). + - `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 [appsettings.json](appsettings.json). + +1. 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 [appsettings.json](appsettings.json). + - 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 comunicate to the Language resource using the User Managed Identity. + - In the [Azure Portal][Azure], go to the Language resource. + - Assign the following role, so the User Managed Identity can access the keys of the Cognitive Service. More information on role assignment can be found [here][language-custom-role]. + - `Cognitive Services User` + - In the Language resource, go to `Keys and Endpoint` under Resource Management. + - Copy the `Endpoint` as value of `LanguageEndpointHostName` in [appsettings.json](appsettings.json). + - `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 [appsettings.json](appsettings.json). + +> [!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 @@ -181,4 +205,8 @@ If you are new to Microsoft Azure, please refer to [Getting started with Azure][ [BF]: https://dev.botframework.com/ [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 \ No newline at end of file +[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 \ No newline at end of file diff --git a/samples/csharp_dotnetcore/48.customQABot-all-features/appsettings.json b/samples/csharp_dotnetcore/48.customQABot-all-features/appsettings.json index 7af5be2af2..cb51f6efca 100644 --- a/samples/csharp_dotnetcore/48.customQABot-all-features/appsettings.json +++ b/samples/csharp_dotnetcore/48.customQABot-all-features/appsettings.json @@ -5,6 +5,7 @@ "MicrosoftAppTenantId": "", "ProjectName": "", "LanguageEndpointKey": "", + "LanguageManagedIdentityClientId": "", "LanguageEndpointHostName": "", "DefaultAnswer": "", "DefaultWelcomeMessage": "", From 8639f22bcf113ea414bde10179f587f3c9a73d11 Mon Sep 17 00:00:00 2001 From: Joel Mut Date: Fri, 25 Apr 2025 11:58:48 +0100 Subject: [PATCH 2/3] Fix typos --- .../48.customQABot-all-features/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/csharp_dotnetcore/48.customQABot-all-features/README.md b/samples/csharp_dotnetcore/48.customQABot-all-features/README.md index 9c62141e26..31ff18ceb3 100644 --- a/samples/csharp_dotnetcore/48.customQABot-all-features/README.md +++ b/samples/csharp_dotnetcore/48.customQABot-all-features/README.md @@ -38,13 +38,13 @@ Pick one and follow these steps to update [appsettings.json](appsettings.json) a - Create a [User Managed Identity][create-msi] resource in the same region as the Language resource. - Copy the `ClientId` as value of `LanguageManagedIdentityClientId` in [appsettings.json](appsettings.json). - 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). + - 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 comunicate to the Language resource using the User Managed Identity. + - 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, so the User Managed Identity can access the keys of the Cognitive Service. More information on role assignment can be found [here][language-custom-role]. - - `Cognitive Services User` + - 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 [appsettings.json](appsettings.json). - `ProjectName` is the name of the project created in [Language Studio][LS]. From abb618e1e00f83a5d80a3201b24c856c33beae9b Mon Sep 17 00:00:00 2001 From: CeciliaAvila Date: Thu, 8 May 2025 11:39:57 -0300 Subject: [PATCH 3/3] Update QnAMakerDialog constructor call --- .../Dialogs/RootDialog.cs | 64 +++++++------------ 1 file changed, 22 insertions(+), 42 deletions(-) diff --git a/samples/csharp_dotnetcore/48.customQABot-all-features/Dialogs/RootDialog.cs b/samples/csharp_dotnetcore/48.customQABot-all-features/Dialogs/RootDialog.cs index 14485bda1c..4befec0211 100644 --- a/samples/csharp_dotnetcore/48.customQABot-all-features/Dialogs/RootDialog.cs +++ b/samples/csharp_dotnetcore/48.customQABot-all-features/Dialogs/RootDialog.cs @@ -74,56 +74,36 @@ private QnAMakerDialog CreateQnAMakerDialog(IConfiguration configuration) // Create a new instance of QnAMakerDialog with dialogOptions initialized. var noAnswer = MessageFactory.Text(configuration["DefaultAnswer"] ?? string.Empty); - QnAMakerDialog dialog; - if (!string.IsNullOrEmpty(managedIdentityClientId)) - { - dialog = new QnAMakerDialog( + var dialog = new QnAMakerDialog( dialogId: nameof(QnAMakerDialog), - managedIdentityClientId: managedIdentityClientId, + endpointKey: null, knowledgeBaseId: knowledgeBaseId, - hostName: new Uri(hostname), + hostName: hostname, noAnswer: noAnswer, cardNoMatchResponse: MessageFactory.Text(ActiveLearningCardNoMatchResponse), useTeamsAdaptiveCard: useTeamsAdaptiveCard) - { - Threshold = ScoreThreshold, - ActiveLearningCardTitle = ActiveLearningCardTitle, - CardNoMatchText = ActiveLearningCardNoMatchText, - Top = TopAnswers, - Filters = { }, - QnAServiceType = ServiceType.Language, - EnablePreciseAnswer = enablePreciseAnswer, - DisplayPreciseAnswerOnly = displayPreciseAnswerOnly, - IncludeUnstructuredSources = IncludeUnstructuredSources, - RankerType = RankerType, - IsTest = IsTest, - UseTeamsAdaptiveCard = useTeamsAdaptiveCard - }; + { + Threshold = ScoreThreshold, + ActiveLearningCardTitle = ActiveLearningCardTitle, + CardNoMatchText = ActiveLearningCardNoMatchText, + Top = TopAnswers, + Filters = { }, + QnAServiceType = ServiceType.Language, + EnablePreciseAnswer = enablePreciseAnswer, + DisplayPreciseAnswerOnly = displayPreciseAnswerOnly, + IncludeUnstructuredSources = IncludeUnstructuredSources, + RankerType = RankerType, + IsTest = IsTest, + UseTeamsAdaptiveCard = useTeamsAdaptiveCard + }; + + if (!string.IsNullOrWhiteSpace(managedIdentityClientId)) + { + dialog.WithManagedIdentityClientId(managedIdentityClientId); } else { - dialog = new QnAMakerDialog( - dialogId: nameof(QnAMakerDialog), - endpointKey: endpointKey, - knowledgeBaseId: knowledgeBaseId, - hostName: hostname, - noAnswer: noAnswer, - cardNoMatchResponse: MessageFactory.Text(ActiveLearningCardNoMatchResponse), - useTeamsAdaptiveCard: useTeamsAdaptiveCard) - { - Threshold = ScoreThreshold, - ActiveLearningCardTitle = ActiveLearningCardTitle, - CardNoMatchText = ActiveLearningCardNoMatchText, - Top = TopAnswers, - Filters = { }, - QnAServiceType = ServiceType.Language, - EnablePreciseAnswer = enablePreciseAnswer, - DisplayPreciseAnswerOnly = displayPreciseAnswerOnly, - IncludeUnstructuredSources = IncludeUnstructuredSources, - RankerType = RankerType, - IsTest = IsTest, - UseTeamsAdaptiveCard = useTeamsAdaptiveCard - }; + dialog.WithEndpointKey(endpointKey); } return dialog;