diff --git a/tools/importdocument/Config.cs b/tools/importdocument/Config.cs index 82da99c4d..091d92f41 100644 --- a/tools/importdocument/Config.cs +++ b/tools/importdocument/Config.cs @@ -12,8 +12,33 @@ public sealed class Config /// /// Client ID for the app as registered in Azure AD. /// + public string AuthenticationType { get; set; } = "None"; + + /// + /// Client ID for the import document tool as registered in Azure AD. + /// public string ClientId { get; set; } = string.Empty; + /// + /// Client ID for the backend web api as registered in Azure AD. + /// + public string BackendClientId { get; set; } = string.Empty; + + /// + /// Tenant ID against which to authenticate users in Azure AD. + /// + public string TenantId { get; set; } = string.Empty; + + /// + /// Azure AD cloud instance for authenticating users. + /// + public string Instance { get; set; } = string.Empty; + + /// + /// Scopes that the client app requires to access the API. + /// + public string Scopes { get; set; } = string.Empty; + /// /// Redirect URI for the app as registered in Azure AD. /// diff --git a/tools/importdocument/Program.cs b/tools/importdocument/Program.cs index a1e342446..3c0170a5c 100644 --- a/tools/importdocument/Program.cs +++ b/tools/importdocument/Program.cs @@ -59,33 +59,23 @@ public static void Main(string[] args) /// Acquires a user account from Azure AD. /// /// The App configuration. - /// Sets the account to the first account found. /// Sets the access token to the first account found. /// True if the user account was acquired. - private static async Task AcquireUserAccountAsync( + private static async Task AcquireTokenAsync( Config config, - Action setAccount, Action setAccessToken) { - Console.WriteLine("Requesting User Account ID..."); + Console.WriteLine("Attempting to authenticate user..."); - string[] scopes = { "User.Read" }; + var webApiScope = $"api://{config.BackendClientId}/{config.Scopes}"; + string[] scopes = { webApiScope }; try { var app = PublicClientApplicationBuilder.Create(config.ClientId) .WithRedirectUri(config.RedirectUri) + .WithAuthority(config.Instance, config.TenantId) .Build(); var result = await app.AcquireTokenInteractive(scopes).ExecuteAsync(); - IEnumerable? accounts = await app.GetAccountsAsync(); - IAccount? first = accounts.FirstOrDefault(); - - if (first is null) - { - Console.WriteLine("Error: No accounts found"); - return false; - } - - setAccount(first); setAccessToken(result.AccessToken); return true; } @@ -113,15 +103,16 @@ private static async Task ImportFilesAsync(IEnumerable files, Config c } } - IAccount? userAccount = null; string? accessToken = null; - - if (await AcquireUserAccountAsync(config, v => { userAccount = v; }, v => { accessToken = v; }) == false) + if (config.AuthenticationType == "AzureAd") { - Console.WriteLine("Error: Failed to acquire user account."); - return; + if (await AcquireTokenAsync(config, v => { accessToken = v; }) == false) + { + Console.WriteLine("Error: Failed to acquire access token."); + return; + } + Console.WriteLine($"Successfully acquired access token. Continuing..."); } - Console.WriteLine($"Successfully acquired User ID. Continuing..."); using var formContent = new MultipartFormDataContent(); List filesContent = files.Select(file => new StreamContent(file.OpenRead())).ToList(); @@ -130,11 +121,6 @@ private static async Task ImportFilesAsync(IEnumerable files, Config c formContent.Add(filesContent[i], "formFiles", files.ElementAt(i).Name); } - var userId = userAccount!.HomeAccountId.Identifier; - var userName = userAccount.Username; - using var userNameContent = new StringContent(userName); - formContent.Add(userNameContent, "userName"); - if (chatCollectionId != Guid.Empty) { Console.WriteLine($"Uploading and parsing file to chat {chatCollectionId}..."); @@ -144,7 +130,7 @@ private static async Task ImportFilesAsync(IEnumerable files, Config c formContent.Add(chatCollectionIdContent, "chatId"); // Calling UploadAsync here to make sure disposable objects are still in scope. - await UploadAsync(formContent, accessToken!, config); + await UploadAsync(formContent, accessToken, config); } else { @@ -153,7 +139,7 @@ private static async Task ImportFilesAsync(IEnumerable files, Config c formContent.Add(globalScopeContent, "documentScope"); // Calling UploadAsync here to make sure disposable objects are still in scope. - await UploadAsync(formContent, accessToken!, config); + await UploadAsync(formContent, accessToken, config); } // Dispose of all the file streams. @@ -170,7 +156,7 @@ private static async Task ImportFilesAsync(IEnumerable files, Config c /// Configuration. private static async Task UploadAsync( MultipartFormDataContent multipartFormDataContent, - string accessToken, + string? accessToken, Config config) { // Create a HttpClient instance and set the timeout to infinite since @@ -183,8 +169,12 @@ private static async Task UploadAsync( { Timeout = Timeout.InfiniteTimeSpan }; - // Add required properties to the request header. - httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {accessToken}"); + + if (config.AuthenticationType == "AzureAd") + { + // Add required properties to the request header. + httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {accessToken!}"); + } try { diff --git a/tools/importdocument/README.md b/tools/importdocument/README.md index 1670f160e..cc294a2bf 100644 --- a/tools/importdocument/README.md +++ b/tools/importdocument/README.md @@ -10,24 +10,14 @@ relevant information from memories to provide more meaningful answers to users t Memories can be generated from conversations as well as imported from external sources, such as documents. Importing documents enables Copilot Chat to have up-to-date knowledge of specific contexts, such as enterprise and personal data. -## Configure your environment -1. A registered App in Azure Portal (https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app) - - Select Mobile and desktop applications as platform type, and the Redirect URI will be `http://localhost` - - Select **`Accounts in any organizational directory (Any Azure AD directory - Multitenant) -and personal Microsoft accounts (e.g. Skype, Xbox)`** as the supported account - type for this sample. - - Note the **`Application (client) ID`** from your app registration. -2. Make sure the service is running. To start the service, see [here](../../webapi/README.md). - -## Running the app +## Running the app against a local Chat Copilot instance 1. Ensure the web api is running at `https://localhost:40443/`. -2. Configure the appsettings.json file under this folder root with the following variables and fill - in with your information, where - `ClientId` is the GUID copied from the **Application (client) ID** from your app registration in the Azure Portal, - `RedirectUri` is the Redirect URI also from the app registration in the Azure Portal, and - `ServiceUri` is the address the web api is running at. +2. Configure the appsettings.json file under this folder root with the following variables: + - `ServiceUri` is the address the web api is running at + - `AuthenticationType` should be set to "None" + - The remaining variables can be left blank or with their default values 3. Change directory to this folder root. 4. **Run** the following command to import a document to the app under the global document collection where @@ -40,8 +30,6 @@ and personal Microsoft accounts (e.g. Skype, Xbox)`** as the supported account `dotnet run --files .\sample-docs\ms10k.txt --chat-id [chatId]` - > Note that this will open a browser window for you to sign in to retrieve your user id to make sure you have access to the chat session. - > Currently only supports txt and pdf files. A sample file is provided under ./sample-docs. Importing may take some time to generate embeddings for each piece/chunk of a document. @@ -61,3 +49,64 @@ and personal Microsoft accounts (e.g. Skype, Xbox)`** as the supported account With [Microsoft Responsible AI Standard v2 General Requirements.pdf](./sample-docs/Microsoft-Responsible-AI-Standard-v2-General-Requirements.pdf): ![Document-Memory-Sample-2](https://github.com/microsoft/chat-copilot/assets/52973358/f0e95104-72ca-4a0a-9555-ee335d8df696) + + +## Running the app against a deployed Chat Copilot instance + +### Configure your environment + +1. Create a registered app in Azure Portal (https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app). + + > Note that this needs to be a separate app registration from those you created when deploying Chat Copilot. + + - Select Mobile and desktop applications as platform type, and the Redirect URI will be `http://localhost` + - Select **`Accounts in this organizational directory only (Microsoft only - Single tenant)`** as the supported account + type for this sample. + + > **IMPORTANT:** The supported account type should match that of the backend's app registration. If you changed this setting to allow allow multitenant and personal Microsoft accounts access to your Chat Copilot application, you should change it here as well. + + - Note the **`Application (client) ID`** from your app registration. + +2. Update the API permissions in the app registration you just created. + + - From the left-hand menu, select **API permissions** and then **Add a permission**. + - Select **My APIs** and then select the application corresponding to your backend web api. + - Check the box next to `access_as_user` and then press **Add permissions**. + +3. Update the authorized client applications for your backend web api. + - In the Azure portal, navigate to your backend web api's app registration. + - From the left-hand menu, select **Expose an API** and then **Add a client application**. + - Enter the client ID of the app registration you just created and check the box under **Authorized scopes**. Then press **Add application**. + +4. Configure the appsettings.json file under this folder root with the following variables: + - `ServiceUri` is the address the web api is running at + - `AuthenticationType` should be set to "AzureAd" + - `ClientId` is the **Application (client) ID** GUID from the app registration you just created in the Azure Portal + - `RedirectUri` is the Redirect URI also from the app registration you just created in the Azure Portal (e.g. `http://localhost`) + - `BackendClientId` is the **Application (client) ID** GUID from your backend web api's app registration in the Azure Portal, + - `TenantId` is the Azure AD tenant ID that you want to authenticate users against. For single-tenant applications, this is the same as the **Directory (tenant) ID** from your app registration in the Azure Portal. + - `Instance` is the Azure AD cloud instance to authenticate users against. For most users, this is `https://login.microsoftonline.com`. + - `Scopes` should be set to "access_as_user" + +### Run the app + +1. Change directory to this folder root. +2. **Run** the following command to import a document to the app under the global document collection where + all users will have access to: + + `dotnet run --files .\sample-docs\ms10k.txt` + + Or **Run** the following command to import a document to the app under a chat isolated document collection where + only the chat session will have access to: + + `dotnet run --files .\sample-docs\ms10k.txt --chat-id [chatId]` + + > Note that both of these commands will open a browser window for you to sign in to ensure you have access to the Chat Copilot service. + + > Currently only supports txt and pdf files. A sample file is provided under ./sample-docs. + + Importing may take some time to generate embeddings for each piece/chunk of a document. + + To import multiple files, specify multiple files. For example: + + `dotnet run --files .\sample-docs\ms10k.txt .\sample-docs\Microsoft-Responsible-AI-Standard-v2-General-Requirements.pdf` diff --git a/tools/importdocument/appsettings.json b/tools/importdocument/appsettings.json index a1619724a..8409065c4 100644 --- a/tools/importdocument/appsettings.json +++ b/tools/importdocument/appsettings.json @@ -1,7 +1,12 @@ { "Config": { + "ServiceUri": "https://localhost:40443", + "AuthenticationType": "None", // Supported authentication types are "None" or "AzureAd" "ClientId": "", - "RedirectUri": "", - "ServiceUri": "https://localhost:40443" + "RedirectUri": "http://localhost", + "BackendClientId": "", + "TenantId": "", + "Instance": "https://login.microsoftonline.com", + "Scopes": "access_as_user" // Scopes that the client app requires to access the API } } \ No newline at end of file