Skip to content

Commit

Permalink
Update import document tool to new auth model (microsoft#239)
Browse files Browse the repository at this point in the history
### Motivation and Context
The recent changes to the authentication model in Chat Copilot
effiectively broke the import document tool. This change fixes it, by
enabling users to:
- Use the tool with an unauthenticated local instance of Chat Copilot
without signing in
- Use the tool with an Azure-deployed instance of Chat Copilot with
Azure AD authentication enabled

Fixes microsoft#212, fixes microsoft#231

### Description

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

- Adds code paths to authenticate user (or not) depending on
configuration
- Removes user name/info from document import form
- Updates README with instructions for setting up the app registration
for import document tool and configuring appsettings.json

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [Contribution
Guidelines](https://github.com/microsoft/copilot-chat/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/copilot-chat/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [x] All unit tests pass, and I have added new tests where possible -
N/A, but both supported scenarios have been tested
- [x] I didn't break anyone 😄
  • Loading branch information
gitri-ms authored Aug 23, 2023
1 parent 57c72d0 commit a16f5e5
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 50 deletions.
25 changes: 25 additions & 0 deletions tools/importdocument/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,33 @@ public sealed class Config
/// <summary>
/// Client ID for the app as registered in Azure AD.
/// </summary>
public string AuthenticationType { get; set; } = "None";

/// <summary>
/// Client ID for the import document tool as registered in Azure AD.
/// </summary>
public string ClientId { get; set; } = string.Empty;

/// <summary>
/// Client ID for the backend web api as registered in Azure AD.
/// </summary>
public string BackendClientId { get; set; } = string.Empty;

/// <summary>
/// Tenant ID against which to authenticate users in Azure AD.
/// </summary>
public string TenantId { get; set; } = string.Empty;

/// <summary>
/// Azure AD cloud instance for authenticating users.
/// </summary>
public string Instance { get; set; } = string.Empty;

/// <summary>
/// Scopes that the client app requires to access the API.
/// </summary>
public string Scopes { get; set; } = string.Empty;

/// <summary>
/// Redirect URI for the app as registered in Azure AD.
/// </summary>
Expand Down
52 changes: 21 additions & 31 deletions tools/importdocument/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,33 +59,23 @@ public static void Main(string[] args)
/// Acquires a user account from Azure AD.
/// </summary>
/// <param name="config">The App configuration.</param>
/// <param name="setAccount">Sets the account to the first account found.</param>
/// <param name="setAccessToken">Sets the access token to the first account found.</param>
/// <returns>True if the user account was acquired.</returns>
private static async Task<bool> AcquireUserAccountAsync(
private static async Task<bool> AcquireTokenAsync(
Config config,
Action<IAccount> setAccount,
Action<string> 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<IAccount>? 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;
}
Expand Down Expand Up @@ -113,15 +103,16 @@ private static async Task ImportFilesAsync(IEnumerable<FileInfo> 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<StreamContent> filesContent = files.Select(file => new StreamContent(file.OpenRead())).ToList();
Expand All @@ -130,11 +121,6 @@ private static async Task ImportFilesAsync(IEnumerable<FileInfo> 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}...");
Expand All @@ -144,7 +130,7 @@ private static async Task ImportFilesAsync(IEnumerable<FileInfo> 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
{
Expand All @@ -153,7 +139,7 @@ private static async Task ImportFilesAsync(IEnumerable<FileInfo> 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.
Expand All @@ -170,7 +156,7 @@ private static async Task ImportFilesAsync(IEnumerable<FileInfo> files, Config c
/// <param name="config">Configuration.</param>
private static async Task UploadAsync(
MultipartFormDataContent multipartFormDataContent,
string accessToken,
string? accessToken,
Config config)
{
// Create a HttpClient instance and set the timeout to infinite since
Expand All @@ -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
{
Expand Down
83 changes: 66 additions & 17 deletions tools/importdocument/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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`
9 changes: 7 additions & 2 deletions tools/importdocument/appsettings.json
Original file line number Diff line number Diff line change
@@ -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
}
}

0 comments on commit a16f5e5

Please sign in to comment.