diff --git a/.gitignore b/.gitignore index 83208207f..0909d22b1 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ next-env.d.ts .azure/ infra/aad_setup.sh .vscode +infra/main.parameters.example.json diff --git a/README.md b/README.md index 7f6f9914e..ab14c71bb 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,46 @@ +# What's new - January 2025 + +A new year brings some much requested feature updates to one of our most popular AI chat repos! + +- **[Managed Identity-based security](/docs/9-managed-identities.md)**. This uses Azure's underlying RBAC and removes (almost) all keys/secrets. +- `appreg_setup.ps1` helper script to **[create the App Registration for you](/docs/3-add-identity.md#entra-id-authentication-provider)** in Entra ID (if you have the permissions). Less copypasta means happier devs πŸ₯° + # Unleash the Power of Azure OpenAI 1. [Introduction](#introduction) -1. [Solution Overview](/docs/1-introduction.md) -1. [Deploy to Azure](#deploy-to-azure) -1. [Run from your local machine](/docs/3-run-locally.md) -1. [Deploy to Azure with GitHub Actions](/docs/4-deploy-to-azure.md) -1. [Add identity provider](/docs/5-add-identity.md) -1. [Chatting with your file](/docs/6-chat-over-file.md) -1. [Persona](/docs/6-persona.md) -1. [Extensions](/docs/8-extensions.md) -1. [Environment variables](/docs/9-environment-variables.md) -1. [Migration considerations](/docs/migration.md) +2. [Solution Overview](./docs/1-introduction.md) +3. [Run from your local machine](./docs/2-run-locally.md) +4. [Add identity provider](./docs/3-add-identity.md) +5. [Deploy to Azure](#deploy-to-azure) +6. [Deploy to Azure with GitHub Actions](./docs/4-deploy-to-azure.md) +7. [Chatting with your file](./docs/5-chat-over-file.md) +8. [Persona](./docs/6-persona.md) +9. [Extensions](./docs/7-extensions.md) +10. [Environment variables](./docs/8-environment-variables.md) +11. [Managed Identity-based deployment](./docs/9-managed-identities.md) +12. [Migration considerations](./docs/migration.md) # Introduction _Azure Chat Solution Accelerator powered by Azure OpenAI Service_ -![](/docs/images/intro.png) +![Intro Image](/docs/images/intro.png) _Azure Chat Solution Accelerator powered by Azure OpenAI Service_ is a solution accelerator that allows organisations to deploy a private chat tenant in their Azure Subscription, with a familiar user experience and the added capabilities of chatting over your data and files. Benefits are: -1. Private: Deployed in your Azure tenancy, allowing you to isolate it to your Azure tenant. +1. **Private:** Deployed in your Azure tenancy, allowing you to isolate it to your Azure tenant. -2. Controlled: Network traffic can be fully isolated to your network and other enterprise grade authentication security features are built in. +2. **Controlled:** Network traffic can be fully isolated to your network and other enterprise grade authentication security features are built in. -3. Value: Deliver added business value with your own internal data sources (plug and play) or integrate with your internal services (e.g., ServiceNow, etc). +3. **Value:** Deliver added business value with your own internal data sources (plug and play) or integrate with your internal services (e.g., ServiceNow, etc). # Deploy to Azure -You can provision Azure resources for the solution accelerator using either the Azure Developer CLI or the Deploy to Azure button below. Regardless of the method you chose you will still need set up an [identity provider and specify an admin user](/docs/5-add-identity.md) +You can provision Azure resources for the solution accelerator using either the Azure Developer CLI or the Deploy to Azure button below. Regardless of the method you chose you will still need set up an [identity provider and specify an admin user](/docs/3-add-identity.md). + +We recommend you also read the dedicated [Deploy to Azure](./docs/4-deploy-to-azure.md) documentation to understand how to deploy the application using GitHub Actions. ## Deployment Options @@ -66,9 +76,9 @@ Click on the Deploy to Azure button to deploy the Azure resources for the applic [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://aka.ms/anzappazurechatgpt) > [!IMPORTANT] -> The application is protected by an identity provider and follow the steps in [Add an identity provider](/docs/5-add-identity.md) section for adding authentication to your app. +> The application is protected by an identity provider, follow the steps in [Add an identity provider](/docs/3-add-identity.md) section for adding authentication to your app. -[Next](./docs/1-introduction.md) +[Next: Introduction](./docs/1-introduction.md) # Contributing diff --git a/azure.yaml b/azure.yaml index 26b77081b..93a558561 100644 --- a/azure.yaml +++ b/azure.yaml @@ -12,9 +12,9 @@ hooks: postdeploy: posix: shell: sh - run: echo -e "\n\033[0;36mTo complete the application setup you will need to configure an identity provider\033[0m\n(see the "Production App Setup" documentation at https://github.com/microsoft/azurechat/blob/main/docs/5-add-identity.md)\n" + run: echo -e "\n\033[0;36mTo complete the application setup you will need to configure an identity provider\033[0m\n(see the "Production App Setup" documentation at https://github.com/microsoft/azurechat/blob/main/docs/3-add-identity.md)\n" interactive: true continueOnError: false windows: shell: pwsh - run: Write-Host "`nTo complete the application setup you will need to configure an identity provider`n(see the 'Production App Setup' documentation at https://github.com/microsoft/azurechat/blob/main/docs/5-add-identity.md)`n" -ForegroundColor Cyan + run: Write-Host "`nTo complete the application setup you will need to configure an identity provider`n(see the 'Production App Setup' documentation at https://github.com/microsoft/azurechat/blob/main/docs/3-add-identity.md)`n" -ForegroundColor Cyan diff --git a/docs/1-introduction.md b/docs/1-introduction.md index 8ef292c24..f1a8981b0 100644 --- a/docs/1-introduction.md +++ b/docs/1-introduction.md @@ -2,31 +2,29 @@ Please make sure the following prerequisites are in place prior to deploying this accelerator: -1. [Azure OpenAI](https://azure.microsoft.com/en-us/products/cognitive-services/openai-service/): To deploy and run the solution accelerator, you'll need an Azure subscription with access to the Azure OpenAI service. Request access [here](https://customervoice.microsoft.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR7en2Ais5pxKtso_Pz4b1_xUOFA5Qk1UWDRBMjg0WFhPMkIzTzhKQ1dWNyQlQCN0PWcu). Once you have access, follow the instructions in this [link](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal) to deploy the gpt-35-turbo or gpt-4 models. - -2. Setup GitHub or Azure AD for Authentication: - The [add an identity provider](./5-add-identity.md) section below shows how to configure authentication providers. +1. Setup GitHub or Entra ID for authentication: + The [add an identity provider](./3-add-identity.md) section below shows how to configure authentication providers. > [!NOTE] > You can configure the authentication provider to your identity solution using [NextAuth providers](https://next-auth.js.org/providers/) ## πŸ‘‹πŸ» Introduction -_Azure Chat Solution Accelerator powered by Azure Open AI Service_ solution accelerator is built using the following technologies: +_Azure Chat Solution Accelerator powered by Azure OpenAI Service_ is built using the following technologies: -- [Node.js 18](https://nodejs.org/en): an open-source, cross-platform JavaScript runtime environment. +- [Node.js 22](https://nodejs.org/en): an open-source, cross-platform JavaScript runtime environment. -- [Next.js 13](https://nextjs.org/docs): enables you to create full-stack web applications by extending the latest React features +- [Next.js 14](https://nextjs.org/docs): enables you to create full-stack web applications by extending the latest React features. -- [NextAuth.js](https://next-auth.js.org/): configurable authentication framework for Next.js 13 +- [NextAuth.js](https://next-auth.js.org/): configurable authentication framework for Next.js. -- [OpenAI sdk](https://github.com/openai/openai-node) NodeJS library that simplifies building conversational UI +- [OpenAI SDK](https://github.com/openai/openai-node) NodeJS library that simplifies building conversational UI. -- [Tailwind CSS](https://tailwindcss.com/): is a utility-first CSS framework that provides a series of predefined classes that can be used to style each element by mixing and matching +- [Tailwind CSS](https://tailwindcss.com/): is a utility-first CSS framework that provides a series of predefined classes that can be used to style each element by mixing and matching. - [shadcn/ui](https://ui.shadcn.com/): re-usable components built using Radix UI and Tailwind CSS. -- [Azure Cosmos DB](https://learn.microsoft.com/en-GB/azure/cosmos-db/nosql/): fully managed platform-as-a-service (PaaS) NoSQL database to store chat history +- [Azure Cosmos DB](https://learn.microsoft.com/en-GB/azure/cosmos-db/nosql/): fully managed platform-as-a-service (PaaS) NoSQL database to store chat history. - [Azure OpenAI](https://learn.microsoft.com/en-us/azure/ai-services/openai/overview): Azure OpenAI Service provides REST API access to OpenAI's powerful language models including the GPT-4, GPT-35-Turbo, and Embeddings model series. @@ -36,19 +34,19 @@ _Azure Chat Solution Accelerator powered by Azure Open AI Service_ solution acce The following Azure services can be deployed to expand the feature set of your solution: -- [Azure Document Intelligence](https://learn.microsoft.com/en-GB/azure/ai-services/document-intelligence/) Microsoft Azure Form Recognizer is an automated data processing system that uses AI and OCR to quickly extract text and structure from documents. We use this service for extracting information from documents. +- [Azure AI Document Intelligence](https://learn.microsoft.com/en-GB/azure/ai-services/document-intelligence/): an automated data processing system that uses AI and OCR to quickly extract text and structure from documents. We use this service for extracting information from documents. -- [Azure AI Search ](https://learn.microsoft.com/en-GB/azure/search/) Azure AI Search is an AI-powered platform as a service (PaaS) that helps developers build rich search experiences for applications. We use this service for indexing and retrieving information. +- [Azure AI Search](https://learn.microsoft.com/en-GB/azure/search/): an AI-powered Platform-as-a-Service (PaaS) that helps developers build rich search experiences for applications. We use this service for indexing and retrieving information. -- [Azure OpenAI Embeddings](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/embeddings?tabs=console) for embed content extracted from files. +- [Azure OpenAI Embeddings](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/embeddings?tabs=console): to embed content extracted from files prior to indexing and during retrieval (vector search). -- [Azure Speech Service](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/): Speech recognition and generation with multi-lingual support and the ability to select and create custom voices. +- [Azure AI Speech](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/): speech recognition and generation with multi-lingual support and the ability to select and create custom voices. # Solution Architecture The following high-level diagram depicts the architecture of the solution accelerator: -![Architecture diagram](/docs/images/architecture.png) +![Architecture diagram](./images/architecture.png) # Azure Deployment Costs @@ -56,15 +54,17 @@ Pricing varies per region and usage, so it isn't possible to predict exact costs However, you can try the [Azure pricing calculator - Sample Estimate](https://azure.com/e/1f08b35661df4b5ea3663df112250b09) for the resources below. - Azure App Service: Premium V3 Tier 1 CPU core, 4 GB RAM, 250 GB Storage. Pricing per hour. [Pricing](https://azure.microsoft.com/pricing/details/app-service/linux/) -- Azure Open AI: Standard tier, ChatGPT and Embedding models. Pricing per 1K tokens used, and at least 1K tokens are used per question. [Pricing](https://azure.microsoft.com/en-us/pricing/details/cognitive-services/openai-service/) -- Form Recognizer: SO (Standard) tier using pre-built layout. Pricing per document page, sample documents have 261 pages total. [Pricing](https://azure.microsoft.com/pricing/details/form-recognizer/) -- Azure AI Search : Standard tier, 1 replica, free level of semantic search. Pricing per hour.[Pricing](https://azure.microsoft.com/pricing/details/search/) +- Azure OpenAI: Standard tier, ChatGPT and Embedding models. Pricing per 1K tokens used, and at least 1K tokens are used per question. [Pricing](https://azure.microsoft.com/en-us/pricing/details/cognitive-services/openai-service/) +- Azure AI Document Intelligence: SO (Standard) tier using pre-built layout. Pricing per document page, sample documents have 261 pages total. [Pricing](https://azure.microsoft.com/en-us/pricing/details/ai-document-intelligence/) +- Azure AI Search: Standard tier, 1 replica, free level of semantic search. Pricing per hour.[Pricing](https://azure.microsoft.com/pricing/details/search/) - Azure Cosmos DB: Standard provisioned throughput with ZRS (Zone-redundant storage). Pricing per storage and read operations. [Pricing](https://azure.microsoft.com/en-us/pricing/details/cosmos-db/autoscale-provisioned/) - Azure Monitor: Pay-as-you-go tier. Costs based on data ingested. [Pricing](https://azure.microsoft.com/pricing/details/monitor/) -To reduce costs, you can switch to free SKUs for Azure App Service, Azure AI Search , and Form Recognizer by changing the parameters file under the `./infra` folder. There are some limits to consider; for example, you can have up to 1 free Cognitive Search resource per subscription, and the free Form Recognizer resource only analyzes the first 2 pages of each document. You can also reduce costs associated with the Form Recognizer by reducing the number of documents you upload. +To reduce costs, you can switch to free SKUs for Azure App Service, Azure AI Search, and Azure AI Document Intelligence by changing the parameters file under the `./infra` folder. There are some limitations to consider; for example, you can have up to 1 free Azure AI Search resource per subscription, and the free Azure AI Document Intelligence resource which only analyzes 500 pages for free each month. You can also reduce costs associated with the Azure AI Document Intelligence service by reducing the number of documents you upload. > [!WARNING] > To avoid unnecessary costs, remember to destroy your provisioned resources by deleting the resource group. -[Next](/docs/2-provision-azure-resources.md) +## Continue to the next step... + +πŸ‘‰ [Next: Run Azure Chat Locally (development)](./2-run-locally.md) diff --git a/docs/2-run-locally.md b/docs/2-run-locally.md new file mode 100644 index 000000000..ec274b7a5 --- /dev/null +++ b/docs/2-run-locally.md @@ -0,0 +1,26 @@ +# πŸ‘¨πŸ»β€πŸ’» Run Locally + +Clone this repository locally or fork to your GitHub account. Run all of the the steps below from the `src` directory. + +## Prerequisites + +- **History Database**: If you don't want to [provision the Azure resources](./4-deploy-to-azure.md), you **must** at least deploy an instance of Azure Cosmos DB in your Azure Subscription to store chat history. + +- **Identity Provider**: For local development, you have the option of using a username / password to sign in. If you prefer to use an Identity Provider, follow the [instructions](./3-add-identity.md) in the next chapter to add one. + +## Steps to Run Locally + +1. Change directory to the `src` folder +2. Rename/copy the file `.env.example` to `.env.local` and populate the environment variables based on the deployed resources in Azure. +3. Install npm packages by running `npm install` +4. Start the app by running `npm run dev` +5. Access the app on [http://localhost:3000](http://localhost:3000) + +You should now be prompted to log in with your chosen authentication method (per the pre-requisite configuration). + +> **NOTE** +> If using Basic Auth (DEV ONLY), any username you enter will create a new user id (hash of username@localhost). You can use this to simulate multiple users. Once successfully logged in, you can start creating new conversations. + +## Continue to the next step... + +πŸ‘‰ [Next: Add an Identity Provider](./3-add-identity.md) diff --git a/docs/3-add-identity.md b/docs/3-add-identity.md new file mode 100644 index 000000000..f4106b884 --- /dev/null +++ b/docs/3-add-identity.md @@ -0,0 +1,125 @@ +# πŸͺͺ Add an Identity Provider + +You will need to add an identity provider to authenticate your app. For local development you have the additional option of using a username / password to sign in (less secure). To view reports and admin info throughout Azure Chat, you will also need to specify an admin user by their email address. + +> [!NOTE] +> Only one of the identity providers is required to be configured below. + +> [!IMPORTANT] +> We **strongly** recommend that you store client secrets in Azure Key Vault and reference the Azure Key Vault secrets in your App config settings. If you have created your environment using the templates in this repo, you will already have an Azure Key Vault service deployed which can be used to store a range of other secrets. Details on how to configure Azure App Service settings to use Azure Key Vault references are [here](https://learn.microsoft.com/en-us/azure/app-service/app-service-key-vault-references?tabs=azure-cli#source-app-settings-from-key-vault). Note that you will also need to give yourself appropriate permissions to create secrets in the Key Vault. + +Azure Chat uses [NextAuth.js](https://next-auth.js.org) for authentication. NextAuth supports a wide range of identity providers. In this guide, you will learn how to configure GitHub and/or Microsoft Entra ID as identity providers, but many others are also supported. + +## GitHub Authentication Provider + +We'll create two GitHub apps: one for testing locally and another for production. + +### 🟑 Development App Setup + +1. Navigate to GitHub OAuth Apps setup https://github.com/settings/developers +2. Create a `New OAuth App` https://github.com/settings/applications/new +3. Fill in the following details + ```default + Application name: DEV Environment + Homepage URL: http://localhost:3000 + Authorization callback URL: http://localhost:3000/api/auth/callback/github + ``` + +### 🟒 Production App Setup + +1. Navigate to GitHub OAuth Apps setup https://github.com/settings/developers +2. Create a `New OAuth App` https://github.com/settings/applications/new +3. Fill in the following details + ```default + Application name: Production + Homepage URL: https://YOUR-WEBSITE-NAME.azurewebsites.net + Authorization callback URL: https://YOUR-WEBSITE-NAME.azurewebsites.net/api/auth/callback/github + ``` + +> [!NOTE] +> After completing app setup, ensure that both your local environment variables as well as Azure Web App environment variables are up to date. + +```bash +# GitHub OAuth app configuration +AUTH_GITHUB_ID= +AUTH_GITHUB_SECRET= +``` + +## Entra ID Authentication Provider + +### 🟑 Development App Setup + +### Automated Approach πŸ†• + +You can use the helper script to create an Azure App Registration and populate the keys automatically, assuming you have the permissions for the manual approach (next section). + +1. In Powershell, run: + ```powershell + PS> .\scripts\appreg_setup.ps1 -webappname [-showsecret] + ``` + - The `webappname` is the resource name of the Azure Web App resource, e.g. `myenv-webapp-e6g73wtcmam74` + - `-showsecret` will display the app secret at the end of the script (only if you need it) +2. It might take a minute or two for the Web App to pickup the new config and restart +3. Enjoy automation! + +### Manual Approach + +1. Navigate to [Entra ID Apps setup](https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps) +2. Create a [New Registration](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/CreateApplicationBlade/quickStartType~/null/isMSAApp~/false) +3. Fill in the following details + ```default + Application name: DEV Environment + Supported account types: Accounts in this organizational directory only + Redirect URI Platform: Web + Redirect URI: http://localhost:3000/api/auth/callback/azure-ad + ``` + +### 🟒 Production App Setup + +1. Navigate to [Azure AD Apps setup](https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps) +2. Create a [New Registration](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/CreateApplicationBlade/quickStartType~/null/isMSAApp~/false) +3. Fill in the following details + ```default + Application name: Production + Supported account types: Accounts in this organizational directory only + Redirect URI Platform: Web + Redirect URI: https://YOUR-WEBSITE-NAME.azurewebsites.net/api/auth/callback/azure-ad + ``` + +> [!NOTE] +> After completing app setup, ensure your environment variables locally and on Azure App Service are up to date. + +> [!IMPORTANT] +> Please beware that while Microsoft has [renamed](https://learn.microsoft.com/en-us/entra/fundamentals/new-name) Azure AD to Microsoft Entra ID, the environment variables still use the old naming convention. We will update this in the future. + +Set environment variables: + +```bash +# Entra ID OAuth App Configuration +AZURE_AD_CLIENT_ID= +AZURE_AD_CLIENT_SECRET= +AZURE_AD_TENANT_ID= +``` + +## Other Identity Providers + +Please refer to the [NextAuth provider documentation](https://next-auth.js.org/providers) for more options. + +The identity provider can be appended to the `providers` array in the [auth-api.ts](src/features/auth-page/auth-api.ts) file and respective client id and secret added to the environment variables: `.env.local` and Azure App Service configuration. + +## Configure an admin user + +Azure Chat provides a reporting feature that allows admins to view chat sessions from users. The reporting pages in the application are only available to an admin user. To configure the admin user create or update the `ADMIN_EMAIL_ADDRESS` config setting locally and on Azure App Service with the email address or addresses of the user(s) who will use the admin report functionality. + +Multiple email addresses can be added here, separated by commas - but it is not possible to specify a security group. + +Example: + +```bash +# Update your admin email addresses - comma separated (add dev@localhost for local admin) +ADMIN_EMAIL_ADDRESS=PersonA@example.com,PersonB@example.com +``` + +## Continue to the next step... + +πŸ‘‰ [Next: Deploy to Azure](./4-deploy-to-azure.md) diff --git a/docs/3-run-locally.md b/docs/3-run-locally.md deleted file mode 100644 index cfbf39798..000000000 --- a/docs/3-run-locally.md +++ /dev/null @@ -1,24 +0,0 @@ -# πŸ‘¨πŸ»β€πŸ’» Run Locally - -Clone this repository locally or fork to your Github account. Run all of the the steps below from the `src` directory. - -## Prerequisites - -- **History Database**: If you didn't [provision the Azure resources](2-provision-azure-resources.md), you **must** at least deploy an instance of Cosmos DB in your Azure Subscription to store chat history. - -- **Identity Provider**: For local development, you have the option of using a username / password. If you prefer to use an Identity Provider, follow the [instructions](3-run-locally.md) to add one. - -## Steps - -1. Change directory to the `src` folder -2. Rename the file `.env.example` to `.env.local` and populate the environment variables based on the deployed resources in Azure. -3. Install npm packages by running `npm install` -4. Start the app by running `npm run dev` -5. Access the app on [http://localhost:3000](http://localhost:3000) - -You should now be prompted to login with your chosen OAuth provider. - -> [!NOTE] -> If using Basic Auth (DEV ONLY) any username you enter will create a new user id (hash of username@localhost). You can use this to simulate multiple users. Once successfully logged in, you can start creating new conversations. - -[Next](/docs/4-deploy-to-azure.md) diff --git a/docs/4-deploy-to-azure.md b/docs/4-deploy-to-azure.md index 240118633..3585a282a 100644 --- a/docs/4-deploy-to-azure.md +++ b/docs/4-deploy-to-azure.md @@ -4,7 +4,7 @@ The following steps describes how the application can be deployed to Azure App s ## 🧬 Fork the repository -Fork this repository to your own organisation so that you can execute GitHub Actions against your own Azure Subscription. +If you haven't already, fork this repository to your own organisation so that you can execute GitHub Actions against your own Azure Subscription. This allows you to edit the code, customise it to your needs, and maintain control over the deployment process. ## πŸ—οΈ Configure secrets in your GitHub repository @@ -21,7 +21,7 @@ The GitHub workflow requires a secret named `AZURE_CREDENTIALS` to authenticate 2. Copy the JSON output from the command. -3. In the GitHub repository, navigate to Settings > Secrets > Actions and select New repository secret. +3. In the GitHub repository, navigate to Settings > Secrets > Actions and select **New repository secret**. 4. Enter `AZURE_CREDENTIALS` as the name and paste the contents of the JSON output as the value. @@ -33,8 +33,10 @@ Under the same repository secrets add a new variable `AZURE_APP_SERVICE_NAME` to ### 3. Run GitHub Actions -Once the secrets are configured, the GitHub Actions will be triggered for every code push to the repository. Alternatively, you can manually run the workflow by clicking on the "Run Workflow" button in the Actions tab in GitHub. +Once the secrets are configured, the GitHub Actions will be triggered for every code push to the repository. Alternatively, you can manually run the workflow by clicking on the "Run Workflow" button in the Actions tab of your GitHub repository. ![Workflow screenshot](/docs/images/runworkflow.png) -[Next](/docs/5-add-identity.md) +## Continue to the next step... + +πŸ‘‰ [Next: Chatting with your file](./5-chat-over-file.md) diff --git a/docs/5-add-identity.md b/docs/5-add-identity.md deleted file mode 100644 index 2462779bf..000000000 --- a/docs/5-add-identity.md +++ /dev/null @@ -1,91 +0,0 @@ -# πŸͺͺ Add an Identity Provider - -Once the deployment is complete, you will need to add an identity provider to authenticate your app. You will also need to configure an admin user. - -> [!NOTE] -> Only one of the identity providers is required to be configured below. - -> [!IMPORTANT] -> We **strongly** recommend that you store client secrets in Azure Key Vault and use Kev Vault references in your App config settings. If you have created your environment using the templates in this repo you will already have a Key Vault that is being used to store a range of other secrets, and you will have Key Vault references in your app config. Details on how to configure App Service settings to use Key Vault are [here](https://learn.microsoft.com/en-us/azure/app-service/app-service-key-vault-references?tabs=azure-cli#source-app-settings-from-key-vault). Note that you will also need to give yourself appropriate permissions to create secrets in the Key Vault. - -## GitHub Authentication Provider - -We'll create two GitHub apps: one for testing locally and another for production. - -### 🟑 Development App Setup - -1. Navigate to GitHub OAuth Apps setup https://github.com/settings/developers -2. Create a `New OAuth App` https://github.com/settings/applications/new -3. Fill in the following details - - ```default - Application name: DEV Environment - Homepage URL: http://localhost:3000 - Authorization callback URL: http://localhost:3000/api/auth/callback/github - ``` - -### 🟒 Production App Setup - -1. Navigate to GitHub OAuth Apps setup https://github.com/settings/developers -2. Create a `New OAuth App` https://github.com/settings/applications/new -3. Fill in the following details - - ```default - Application name: Production - Homepage URL: https://YOUR-WEBSITE-NAME.azurewebsites.net - Authorization callback URL: https://YOUR-WEBSITE-NAME.azurewebsites.net/api/auth/callback/github - ``` - -> [!NOTE] -> After completing app setup, ensure that both your local environment variables as well as Azure Web App environment variables are up to date. - -```bash - # GitHub OAuth app configuration - AUTH_GITHUB_ID= - AUTH_GITHUB_SECRET= -``` - -## Azure AD Authentication Provider - -### 🟑 Development App Setup - -1. Navigate to [Azure AD Apps setup](https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps) -2. Create a [New Registration](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/CreateApplicationBlade/quickStartType~/null/isMSAApp~/false) -3. Fill in the following details - - ```default - Application name: DEV Environment - Supported account types: Accounts in this organizational directory only - Redirect URI Platform: Web - Redirect URI: http://localhost:3000/api/auth/callback/azure-ad - ``` - -### 🟒 Production App Setup - -1. Navigate to [Azure AD Apps setup](https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps) -2. Create a [New Registration](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/CreateApplicationBlade/quickStartType~/null/isMSAApp~/false) -3. Fill in the following details - - ```default - Application name: Production - Supported account types: Accounts in this organizational directory only - Redirect URI Platform: Web - Redirect URI: https://YOUR-WEBSITE-NAME.azurewebsites.net/api/auth/callback/azure-ad - ``` - -> [!NOTE] -> After completing app setup, ensure your environment variables locally and on Azure App Service are up to date. - -```bash -# Azure AD OAuth app configuration - -AZURE_AD_CLIENT_ID= -AZURE_AD_CLIENT_SECRET= -AZURE_AD_TENANT_ID= -``` - -## Configure an admin user - -The reporting pages in the application are only available to an admin user. To configure the admin user create or update the `ADMIN_EMAIL_ADDRESS` config setting locally and on Azure App Service with the email address of the user who will use reports. - -[Next](/docs/6-chat-over-file.md) diff --git a/docs/5-chat-over-file.md b/docs/5-chat-over-file.md new file mode 100644 index 000000000..5b7986a01 --- /dev/null +++ b/docs/5-chat-over-file.md @@ -0,0 +1,94 @@ +# πŸ“ƒ Chatting With Your File + +There are multiple ways you can integrate chat-with-your-data, in this guide you will learn how to enable users to upload a file through Azure Chat and engage in chat discussions related to the file contents. + +1. This approach is simple and easy to use. +2. File contents are indexed and maintained within the chat interface and are only available for the current chat session by the current user. + +Chat with your Data utilises the following Azure AI Services: + +1. [Azure AI Document Intelligence](https://learn.microsoft.com/en-GB/azure/ai-services/document-intelligence/) for extracting information from documents. +2. [Azure AI Search](https://learn.microsoft.com/en-GB/azure/search/) for indexing and retrieving information. +3. [Azure OpenAI Embeddings](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/embeddings?tabs=console) for embed content extracted from files + +## Understanding the RAG Pattern + +Once the file is uploaded, the content is extracted using Azure AI Document Intelligence. It is then used to generate embeddings using Azure OpenAI's embedding model (created during the initial deployment). These are stored in Azure AI Search as vectors. + +When a question is entered by the user, the embedding (vector) of the user's input is compared to those indexed in Azure AI Search to generate a similarity score. Relevant (or most similar) chunks (parts/paragraphs) from the uploaded document as determined during this retrieval and similarity scoring are passed to the language model as additional context alongside the user's question to generate responses grounded in the uploaded file. This is a simple description of the RAG (Retrieval-Augmented Generation) pattern. + +![Chat over file](/docs/images/chatover-file.png) + +## Bring your own Azure AI Search Index + +Chatting with a user-uploaded file works well for ad-hoc conversations. However, you may want to index and maintain your own organisational data outside of Azure Chat, making it available across multiple chat sessions and allowing your organisation to index larger datasets / documents / policies etc. + +With the help of the Extensions feature you can bring your own Azure AI Search index and integrate it with the chat interface. This will allow you to search and retrieve information from your own data source - not just the uploaded file in the current chat session. + +## Advantages of using this approach: + +1. Index and maintain your data outside of Azure Chat. +2. Re-use the index across multiple chat sessions. +3. As an admin, you can publish the index across the organisation. e.g. HR, Finance, IT etc. +4. Frequent updates or changes to the dataset (e.g. policies, procedures) can be centrally re-indexed (via an Azure AI Search indexer) on a customised schedule to ensure the latest information is available to users. + +## Integrating your own Azure AI Search + +1. Navigate to the Extensions page and click on the "Azure AI Search" button. +2. Fill in the first section with the following details: + ![New Extension](/docs/images/extensions/extension-azure-ai-search-1.png) + + - **Name**: Name of the extension e.g. "HR Search" + - **Description**: Description of the extension e.g. "Search HR documents" + - **Detail description**: Change the description to match your use case. However, the citation section must remain the same. + + ```markdown + You are an expert in searching internal documents using aisearch function. You must always include a citation at the end of your answer and don't include a full stop after the citations. + + Use the format for your citation {% citation items=[{name:\"filename 1\",id:\"file id\"}, {name:\"filename 2\",id:\"file id\"}] /%} + ``` + +3. Fill in the Headers section with the following details: + ![Configure Headers](/docs/images/extensions/extension-azure-ai-search-2.png) + + - **vectors**: Comma separated values of the vectors on the index e.g. "title, content" + - **apiKey**: API key for the Azure AI Search + - **searchName**: Name of the Azure AI Search service + - **indexName**: Name of the Azure AI Search index + +4. Update the function definition and publish the extension. + ![Publish and Save](/docs/images/extensions/extension-azure-ai-search-3.png) + + - **Method**: POST + - **URL**: `https://REPLACE_WITH_YOUR_DOMAIN.COM/api/document` + - **Function**: Update the description and parameters to match your use case. + + ```json + { + "name": "aisearch", + "parameters": { + "type": "object", + "properties": { + "body": { + "type": "object", + "description": "Body of search for relevant information", + "properties": { + "search": { + "type": "string", + "description": "The exact search value from the user" + } + }, + "required": ["search"] + } + }, + "required": ["body"] + }, + "description": "DESCRIBE YOUR SEARCH DESCRIPTION HERE" + } + ``` + +5. Save the function and publish the extension. + +## Continue to the next step... + +πŸ‘‰ [Next: Personas](./6-persona.md) diff --git a/docs/6-chat-over-file.md b/docs/6-chat-over-file.md deleted file mode 100644 index e3da9c3b1..000000000 --- a/docs/6-chat-over-file.md +++ /dev/null @@ -1,98 +0,0 @@ -# πŸ“ƒ Chatting With Your File - -There are multiple ways you can integrate chat with your data. - -# **Upload a file and chat with your file using the chat interface.** - -Users can utilise this functionality to upload their files through the portal and engage in chat discussions related to the content of those files. - -Advantages of using this approach: - -1. Simple and easy to use. -2. File content is indexed and maintained within the chat interface and it is only available for the current chat session. - -Chat with your data utilises the following Azure Services: - -Once the file is uploaded, the content is extracted and indexed using Azure AI Search. The content is then used to generate embeddings using Azure OpenAI Embeddings. The embeddings are then used to generate a similarity score between the uploaded file and the chat messages. The chat messages are then filtered based on the similarity score and displayed to the user. - -3. [Azure Document Intelligence](https://learn.microsoft.com/en-GB/azure/ai-services/document-intelligence/) for extracting information from documents. -4. [Azure AI Search ](https://learn.microsoft.com/en-GB/azure/search/) for indexing and retrieving information. -5. [Azure OpenAI Embeddings](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/embeddings?tabs=console) for embed content extracted from files - -![](/docs/images/chatover-file.png) - -# **Bring your own Azure AI Search.** - -With the help of Extensions feature you can bring your own Azure AI Search and integrate it with the chat interface. This will allow you to search and retrieve information from your own data source. - -Advantages of using this approach: - -1. Index and maintain your own data outside of Azure Chat. -2. Re-use the index across multiple chat sessions. -3. As an admin, you can publish the index across organisation. e.g. HR, Finance, IT etc. - -Steps to integrate your own Azure AI Search: - -1. Navigate to the Extensions page and click on the "Azure AI search" button. -2. Fill in the first section with the following details: - -![](/docs/images/extensions/extension-azure-ai-search-1.png) - -- **Name**: Name of the extension e.g. "HR Search" -- **Description**: Description of the extension e.g. "Search HR documents" -- **Detail description**: - -Change the description to match your use case. However, the citation section must remain the same. - -```markdown -You are an expert in searching internal documents using aisearch function. You must always include a citation at the end of your answer and don't include a full stop after the citations. - -Use the format for your citation {% citation items=[{name:\"filename 1\",id:\"file id\"}, {name:\"filename 2\",id:\"file id\"}] /%} -``` - -3. Fill in the Headers section with the following details: - -![](/docs/images/extensions/extension-azure-ai-search-2.png) - -- **vectors**: Comma separated values of the vectors on the index e.g. "title, content" -- **apiKey**: API key for the Azure AI Search -- **searchName**: Name of the Azure AI Search service -- **indexName**: Name of the Azure AI Search index - -4. Update the function definition and publish the extension. - -![](/docs/images/extensions/extension-azure-ai-search-3.png) - -- **Method**: POST -- **URL**: `https://REPLACE_WITH_YOUR_DOMAIN.COM/api/document` -- **Function**: - -Update the description and parameters to match your use case. - -```json -{ - "name": "aisearch", - "parameters": { - "type": "object", - "properties": { - "body": { - "type": "object", - "description": "Body of search for relevant information", - "properties": { - "search": { - "type": "string", - "description": "The exact search value from the user" - } - }, - "required": ["search"] - } - }, - "required": ["body"] - }, - "description": "DESCRIBE YOUR SEARCH DESCRIPTION HERE" -} -``` - -Save the function and publish the extension. - -[Next](/docs/6-persona.md) diff --git a/docs/6-persona.md b/docs/6-persona.md index e3b5878f5..b8a0b4a95 100644 --- a/docs/6-persona.md +++ b/docs/6-persona.md @@ -2,9 +2,15 @@ Persona helps you craft individual personas to bring personality and engagement into your conversations. -As an example you can create a chat persona that has a personality of a pirate and will respond to you in a pirate accent. +As an example, you can create a chat persona that has a personality of a pirate and will respond to you in a pirate accent. Or, in a more professional setting, you can create a persona that is an expert in ReactJS and Tailwind CSS and can help you write clean functional components -### Pirate a persona +In summary, personas can be customised and published for your organisation to set the expectation of the chat session, the model's personality, output, or any other context without needing to specify these manually in each new conversation - saving time and ensuring consistency between chats. + +## Creating a persona + +Azure Chat provides a simple interface to create and manage personas. You can edit or delete these personas as needed - or add your own! + +### Example 1: Pirate persona 1. **Name**: Talk Like a Pirate 2. **Description**: A persona that talks like a pirate @@ -12,39 +18,39 @@ As an example you can create a chat persona that has a personality of a pirate a You can now use this persona in your conversations. -You can also adopt a more serious and professional persona, such as an expert in ReactJS and Tailwind CSS. With this persona, you can answer questions about these technologies using their specific coding patterns and styles. - -### ReactJS and Tailwind CSS persona +### Example 2: ReactJS and Tailwind CSS persona 1. **Name**: ReactJS and Tailwind CSS 2. **Description**: An expert in ReactJS and Tailwind CSS 3. **Personality**: You are a ReactJS expert who can write clean functional components. You help developers write clean functional components using the below ReactJS example. -```jsx -Example: -import * as React from "react"; - -export interface InputProps - extends React.InputHTMLAttributes {} - -const Input = React.forwardRef( - ({ className, ...props }, ref) => { - return ( - - ); - } -); -Input.displayName = "Input"; - -export { Input }; -``` - -As you can see this persona provides a specific example of how to write a ReactJS component using Tailwind CSS. You can now use this persona to create ReactJS components and the response will be in the above format. - -[Next](/docs/8-extensions.md) + ```jsx + import * as React from "react"; + + export interface InputProps + extends React.InputHTMLAttributes {} + + const Input = React.forwardRef( + ({ className, ...props }, ref) => { + return ( + + ); + } + ); + + Input.displayName = "Input"; + + export { Input }; + ``` + +As you can see this persona provides a specific example of how to write a ReactJS component using Tailwind CSS. You can now use this persona to create ReactJS components and the response will be in line with the above format. + +## Continue to the next step... + +πŸ‘‰ [Next: Extensions](./7-extensions.md) diff --git a/docs/8-extensions.md b/docs/7-extensions.md similarity index 81% rename from docs/8-extensions.md rename to docs/7-extensions.md index 1b50fcecb..b740e1b2d 100644 --- a/docs/8-extensions.md +++ b/docs/7-extensions.md @@ -1,10 +1,10 @@ # πŸ’‘πŸ”— Extensions -With Extensions, you can enhance the functionality of Azure Chat by integrating it with your internal APIs or external resources.Extensions are created using OpenAI Tools, specifically through Function Calling. +With Extensions, you can enhance the functionality of Azure Chat by integrating it with your internal APIs or external resources. Extensions are created using Azure OpenAI tools, specifically through a process known as [Function Calling](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/function-calling). -As a user, you have the ability to create extensions that call your own internal APIs or external resources. However, if you are an admin, you can create extensions that can be utilised by all users within your organization. +As a user, you have the ability to create extensions that call your own internal APIs or external resources. However, if you are an admin, you can create extensions that can be utilised by all users within your organisation. -Refer to the [OpenAI Tools](https://platform.openai.com/docs/guides/function-calling) documentation for more information on how tools and functions call works. +Not all models support tool/function calling, only support a single tool/function, or allow for multiple functions to be called in parallel ([read more](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/function-calling#function-calling-support)). Azure Chat expects the following from the function definition: @@ -43,15 +43,15 @@ Azure Chat expects the following from the function definition: } ``` -As an example you can create an extension that calls Bing Search API to search for a specific topic and return the results to the user. - -In the example below only the `query` is required as Bing does not require a body parameter. - > [!NOTE] -> As header values specified for an extension often contain secrets (e.g. API keys) Azure Chat stores those values securely in Azure Key Vault. If you are deploying the solution to Azure using azd or the bicep templates the required Key Vault role assignment is automatically created. If you are running the solution locally you will need to manually add the "Key Vault Secrets Officer" role to identy that is running the solution (wh8ch will typically be the user logged into the Azure CLI) +> As header values specified for an extension often contain secrets (e.g. API keys) Azure Chat stores those values securely in Azure Key Vault. If you are deploying the solution to Azure using `azd` or the Bicep templates, the required Azure Key Vault role assignment is automatically created. If you are running the solution locally, you will need to manually add the "Key Vault Secrets Officer" role to the identity that is running the solution (which will typically be the user logged into the Azure CLI) # Bing Search Extension +As an example you can create an extension that calls Bing Search API to search for a specific topic over live internet data, and returns the results to the user. + +In the example below only the `query` is required as Bing does not require a body parameter. + 1. **Name**: `Bing Search` 2. **Short Description**: `Bring up to date information with Bing Search` 3. **Detail Description**: @@ -66,9 +66,9 @@ In the example below only the `query` is required as Bing does not require a bod 5. **Function**: - - API Endpoint: GET https://api.bing.microsoft.com/v7.0/search?q=BING_SEARCH_QUERY + - API Endpoint: **GET** `https://api.bing.microsoft.com/v7.0/search?q=BING_SEARCH_QUERY` - BIG_SEARCH_QUERY is a variable that will be replaced with the search query entered by the user. The BIG_SEARCH_QUERY will be automatically passed to the function as part of the request based on the function definition below. + BIG_SEARCH_QUERY is a variable that will be replaced with the search query entered by the user. In the function definition below, the BIG_SEARCH_QUERY will be automatically passed to the function as part of the request. - Function definition: @@ -100,13 +100,13 @@ In the example below only the `query` is required as Bing does not require a bod } ``` -6. **Publish**: Publish the extension to make it available to use in your conversations. Publish is an admin only feature. If you are not an admin you will not see the publish button. +6. **Publish**: Publish the extension to make it available to use in your conversations. Publish is an admin-only feature. If you are not an admin you will not see the publish button. # GitHub Issues Extension This example is much more complex as it is capable of invoking multiple APIs to create or update a GitHub Issue depending on the user question. -In this example you will be able to create and update GitHub Issues using the GitHub API. +In this example you will be able to create and update GitHub Issues using the [GitHub API](https://docs.github.com/rest). 1. **Name**: `GitHub Issues` 2. **Short Description**: `Create and update GitHub Issues` @@ -197,11 +197,11 @@ In this example you will be able to create and update GitHub Issues using the Gi POST https://api.github.com/repos/GITHUB_OWNER/GITHUB_REPO/issues/ISSUE_NUMBER ``` - The ISSUE_NUMBER will be automatically passed to the function as part of the request based on the function definition below. + The `ISSUE_NUMBER` will be automatically passed to the function as part of the request based on the function definition below. - The function definition for updating GitHub issue - The `body` parameter is the same scheme as CreateGitHubIssue function. However you will notice that the `query` parameter is added to the function definition. This is because Azure Chat will automatically pass the query parameters to the function as part of the request. In this case the query parameter is ISSUE_NUMBER.G + The `body` parameter is the same scheme as CreateGitHubIssue function. However, you will notice that the `query` parameter is added to the function definition. This is because Azure Chat will automatically pass the query parameters to the function as part of the request. In this case the query parameter is `ISSUE_NUMBER`. ```json { @@ -261,4 +261,8 @@ In this example you will be able to create and update GitHub Issues using the Gi } ``` - [Next](/docs/9-environment-variables.md) +6. **Publish**: Publish the extension to make it available to use in your conversations. Publish is an admin-only feature. If you are not an admin you will not see the publish button. + +## Continue to the next step... + +πŸ‘‰ [Next: Environment Variables](./8-environment-variables.md) diff --git a/docs/8-environment-variables.md b/docs/8-environment-variables.md new file mode 100644 index 000000000..e25e7b102 --- /dev/null +++ b/docs/8-environment-variables.md @@ -0,0 +1,7 @@ +# πŸ”‘ Environment Variables + +Refer to the [`.env.example`](../src/.env.example) file for the required environment variables. For local development, these should be copied to a new file named `.env.local` in the `src` directory. + +## Continue to the next step... + +πŸ‘‰ [Next: Managed Identities](./9-managed-identities.md) diff --git a/docs/9-environment-variables.md b/docs/9-environment-variables.md deleted file mode 100644 index 206779212..000000000 --- a/docs/9-environment-variables.md +++ /dev/null @@ -1,3 +0,0 @@ -# πŸ”‘ Environment Variables - -Refer to the [`.env.example`](../src/.env.example) for the required environment variables diff --git a/docs/9-managed-identities.md b/docs/9-managed-identities.md new file mode 100644 index 000000000..8207bed53 --- /dev/null +++ b/docs/9-managed-identities.md @@ -0,0 +1,61 @@ +# Using Managed Identities for Azure Chat Solution Accelerator + +## Introduction + +The Azure Chat Solution Accelerator powered by Azure OpenAI Service allows organizations to deploy a private chat tenant with enhanced security and control over their data. One of the new features is the support for [Managed Identities](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview), adding a layer of security by eliminating the need for managing service principals and secrets through the application, and leveraging Azure's built-in role-based access controls. + +### Security Advantages of Managed Identities + +[**Managed Identities**](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) for Azure resources provide the following benefits: + +1. **Improved Security**: + + - **No Secret Management**: Eliminates the need to manually store and manage credentials or keys. + - **Automatic Rotation**: Managed Identities’ credentials are rotated automatically, eliminating potential security risk from non-rotated credentials. + - **Scope Limited Access**: Access to Azure resources can be fine-grained, allowing least-privilege access policies. + +2. **Simplified Management**: + - **Platform Managed**: The Azure platform handles identity creation and lifecycle management. + - **Simplified Resource Access**: Applications can request tokens to access resources without handling secrets. + +## List of Services Using Managed Identities + +The following services within the Azure Chat Solution Accelerator use Managed Identities for authentication: + +1. **Azure OpenAI Service** +2. **Azure Cosmos DB** +3. **Azure AI Services (e.g., Document Intelligence, Azure OpenAI DALL-E)** +4. **Azure AI Search Service** +5. **Azure Storage Account** + +> **Note:** Currently, due to compatibility issues, the Azure AI Speech Service does not utilize Managed Identities. There is no available documentation for using Entra ID authentication with the Speech Service, making it a `TODO` item. + +## Preferred Production Deployment + +Using Managed Identities is preferred for production deployments due to: + +1. **Enhanced Security**: Eliminates risks associated with secret management such as accidental exposure or non-rotation of credentials. +2. **Compliance and Governance**: Managed Identities integrate with Azure's role-based access control (RBAC), facilitating easier audits and compliance management. +3. **Operational Efficiency**: Reduces the operational overhead of managing secrets, while also providing a more straightforward implementation. + +### Deploy to Azure with Managed Identities + +To deploy the application to Azure App Service with Managed Identities, follow the standard deployment instructions available in the [Deploy to Azure - GitHub Actions](https://github.com/microsoft/azurechat) section of the repository. Ensure to: + +1. **Update the Parameter**: + - Set the parameter `disableLocalAuth` to `true` in [`infra/main.bicep`](/infra/main.bicep) (or [`infra/main.json`](/infra/main.json) for ARM deployment) to use Managed Identities. +2. **Deploy as normal**: + - refer to the [README](../README.md) +3. **(Optional) Setup your local development environment**: + - Run this script to grant yourself RBAC permissions on the Azure resources so you can run AzureChat locally + - In Powershell: + ```powershell + PS> .\scripts\appreg_setup.ps1 + ``` + - You can now refer to the documentation to [run Azure Chat locally](2-run-locally.md). + +## Conclusion + +By leveraging Managed Identities, you enhance the security posture of your Azure Chat deployment while simplifying secret management and access control. This guide outlines the security advantages and highlights the necessary parameter changes to ensure a secure and efficient production setup. For more details, review the complete code and configurations available in the repository's `infra` directory. + +🏁 [Back to README](../README.md) diff --git a/infra/main.bicep b/infra/main.bicep index f15740ad5..9240a2c90 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -1,5 +1,8 @@ targetScope = 'subscription' +// Activates/Deactivates Authentication using keys. If true it will enforce RBAC using managed identities +param disableLocalAuth bool = false + @minLength(1) @maxLength(64) @description('Name of the the environment which is used to generate a short unique hash used in all resources.') @@ -88,9 +91,37 @@ module resources 'resources.bicep' = { storageServiceSku: storageServiceSku storageServiceImageContainerName: storageServiceImageContainerName location: location + disableLocalAuth:disableLocalAuth } } output APP_URL string = resources.outputs.url +output AZURE_WEBAPP_NAME string = resources.outputs.webapp_name output AZURE_LOCATION string = location output AZURE_TENANT_ID string = tenant().tenantId +output AZURE_RESOURCE_GROUP string = rg.name + +output AZURE_OPENAI_API_INSTANCE_NAME string = resources.outputs.openai_name +output AZURE_OPENAI_API_DEPLOYMENT_NAME string = chatGptDeploymentName +output AZURE_OPENAI_API_VERSION string = openAIApiVersion +output AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME string = embeddingDeploymentName + +output AZURE_OPENAI_DALLE_API_INSTANCE_NAME string = resources.outputs.openai_dalle_name +output AZURE_OPENAI_DALLE_API_DEPLOYMENT_NAME string = dalleDeploymentName +output AZURE_OPENAI_DALLE_API_VERSION string = dalleApiVersion + +output AZURE_COSMOSDB_ACCOUNT_NAME string = resources.outputs.cosmos_name +output AZURE_COSMOSDB_URI string = resources.outputs.cosmos_endpoint +output AZURE_COSMOSDB_DB_NAME string = resources.outputs.database_name +output AZURE_COSMOSDB_CONTAINER_NAME string = resources.outputs.history_container_name +output AZURE_COSMOSDB_CONFIG_CONTAINER_NAME string = resources.outputs.config_container_name + +output AZURE_SEARCH_NAME string = resources.outputs.search_name +output AZURE_SEARCH_INDEX_NAME string = searchServiceIndexName + +output AZURE_DOCUMENT_INTELLIGENCE_NAME string = resources.outputs.form_recognizer_name +output AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT string = 'https://${resources.outputs.form_recognizer_name}.cognitiveservices.azure.com/' + +output AZURE_SPEECH_REGION string = location +output AZURE_STORAGE_ACCOUNT_NAME string = resources.outputs.storage_name +output AZURE_KEY_VAULT_NAME string = resources.outputs.key_vault_name diff --git a/infra/main.json b/infra/main.json index 397308da6..d78e4ec32 100644 --- a/infra/main.json +++ b/infra/main.json @@ -4,11 +4,15 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "18214004695586675733" + "version": "0.32.4.45862", + "templateHash": "5984410763975349559" } }, "parameters": { + "disableLocalAuth": { + "type": "bool", + "defaultValue": false + }, "name": { "type": "string", "minLength": 1, @@ -28,12 +32,27 @@ "type": "string", "allowedValues": [ "australiaeast", + "brazilsouth", "canadaeast", + "eastus", + "eastus2", "francecentral", + "germanywestcentral", + "japaneast", + "koreacentral", + "northcentralus", + "norwayeast", + "polandcentral", + "spaincentral", + "southafricanorth", + "southcentralus", "southindia", - "uksouth", "swedencentral", - "westus" + "switzerlandnorth", + "uksouth", + "westeurope", + "westus", + "westus3" ], "metadata": { "azd": { @@ -48,7 +67,7 @@ }, "openAIApiVersion": { "type": "string", - "defaultValue": "2024-05-13" + "defaultValue": "2024-08-01-preview" }, "chatGptDeploymentCapacity": { "type": "int", @@ -81,7 +100,9 @@ "dalleLocation": { "type": "string", "allowedValues": [ - "swedencentral" + "swedencentral", + "eastus", + "australiaeast" ], "metadata": { "description": "Location for the OpenAI DALL-E 3 instance resource group" @@ -226,6 +247,9 @@ }, "location": { "value": "[parameters('location')]" + }, + "disableLocalAuth": { + "value": "[parameters('disableLocalAuth')]" } }, "template": { @@ -234,8 +258,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "18109441359842852578" + "version": "0.32.4.45862", + "templateHash": "1979473744682213387" } }, "parameters": { @@ -317,6 +341,10 @@ "type": "string", "defaultValue": "[resourceGroup().location]" }, + "disableLocalAuth": { + "type": "bool", + "defaultValue": false + }, "nextAuthHash": { "type": "securestring", "defaultValue": "[uniqueString(newGuid())]" @@ -324,6 +352,13 @@ "tags": { "type": "object", "defaultValue": {} + }, + "roleDefinitionName": { + "type": "string", + "defaultValue": "Azure Cosmos DB for NoSQL Data Plane Owner", + "metadata": { + "description": "Name of the role definition." + } } }, "variables": { @@ -335,7 +370,8 @@ "search_name": "[toLower(format('{0}search{1}', parameters('name'), parameters('resourceToken')))]", "webapp_name": "[toLower(format('{0}-webapp-{1}', parameters('name'), parameters('resourceToken')))]", "appservice_name": "[toLower(format('{0}-app-{1}', parameters('name'), parameters('resourceToken')))]", - "storage_prefix": "[take(parameters('name'), 8)]", + "clean_name": "[replace(replace(parameters('name'), '-', ''), '_', '')]", + "storage_prefix": "[take(variables('clean_name'), 8)]", "storage_name": "[toLower(format('{0}sto{1}', variables('storage_prefix'), parameters('resourceToken')))]", "kv_prefix": "[take(parameters('name'), 7)]", "keyVaultName": "[toLower(format('{0}-kv-{1}', variables('kv_prefix'), parameters('resourceToken')))]", @@ -368,7 +404,15 @@ }, "capacity": "[parameters('embeddingDeploymentCapacity')]" } - ] + ], + "cosmosDbContributorRoleId": "5bd9cd88-fe45-4216-938b-f97437e15450", + "cosmosDbOperatorRoleId": "230815da-be43-4aae-9cb4-875f7bd000aa", + "cognitiveServicesContributorRoleId": "25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68", + "cognitiveServicesUserRoleId": "a97b65f3-24c7-4388-baec-2e87135dc908", + "storageBlobDataContributorRoleId": "ba92f5b4-2d11-453d-a403-e96b0029c9fe", + "searchServiceContributorRoleId": "7ca78c08-252a-4471-8644-bb5ff32d4ba0", + "cognitiveServicesOpenAIContributorRoleId": "a001fd3d-188f-4b5d-821b-7da978bf7442", + "searchIndexDataContributorRoleId": "8ebe5a00-799e-43f5-93ac-243d3dce84a7" }, "resources": [ { @@ -573,6 +617,10 @@ "ftpsState": "Disabled", "minTlsVersion": "1.2", "appSettings": [ + { + "name": "USE_MANAGED_IDENTITIES", + "value": "[parameters('disableLocalAuth')]" + }, { "name": "AZURE_KEY_VAULT_NAME", "value": "[variables('keyVaultName')]" @@ -583,7 +631,7 @@ }, { "name": "AZURE_OPENAI_API_KEY", - "value": "[format('@Microsoft.KeyVault(VaultName={0};SecretName={1})', variables('keyVaultName'), 'AZURE-OPENAI-API-KEY')]" + "value": "[if(parameters('disableLocalAuth'), '', format('@Microsoft.KeyVault(VaultName={0};SecretName={1})', variables('keyVaultName'), 'AZURE-OPENAI-API-KEY'))]" }, { "name": "AZURE_OPENAI_API_INSTANCE_NAME", @@ -756,6 +804,7 @@ "kind": "GlobalDocumentDB", "properties": { "databaseAccountOfferType": "Standard", + "disableLocalAuth": "[parameters('disableLocalAuth')]", "locations": [ { "locationName": "[parameters('location')]", @@ -825,7 +874,8 @@ "kind": "FormRecognizer", "properties": { "customSubDomainName": "[variables('form_recognizer_name')]", - "publicNetworkAccess": "Enabled" + "publicNetworkAccess": "Enabled", + "disableLocalAuth": "[parameters('disableLocalAuth')]" }, "sku": { "name": "[parameters('formRecognizerSkuName')]" @@ -840,7 +890,8 @@ "properties": { "partitionCount": 1, "publicNetworkAccess": "enabled", - "replicaCount": 1 + "replicaCount": 1, + "disableLocalAuth": "[parameters('disableLocalAuth')]" }, "sku": { "name": "[parameters('searchServiceSkuName')]" @@ -855,7 +906,8 @@ "kind": "OpenAI", "properties": { "customSubDomainName": "[variables('openai_name')]", - "publicNetworkAccess": "Enabled" + "publicNetworkAccess": "Enabled", + "disableLocalAuth": "[parameters('disableLocalAuth')]" }, "sku": { "name": "[parameters('openAiSkuName')]" @@ -872,8 +924,7 @@ "apiVersion": "2023-05-01", "name": "[format('{0}/{1}', variables('openai_name'), variables('llmDeployments')[copyIndex()].name)]", "properties": { - "model": "[variables('llmDeployments')[copyIndex()].model]", - "raiPolicyName": "[if(contains(variables('llmDeployments')[copyIndex()], 'raiPolicyName'), variables('llmDeployments')[copyIndex()].raiPolicyName, null())]" + "model": "[variables('llmDeployments')[copyIndex()].model]" }, "sku": "[if(contains(variables('llmDeployments')[copyIndex()], 'sku'), variables('llmDeployments')[copyIndex()].sku, createObject('name', 'Standard', 'capacity', variables('llmDeployments')[copyIndex()].capacity))]", "dependsOn": [ @@ -889,7 +940,8 @@ "kind": "OpenAI", "properties": { "customSubDomainName": "[variables('openai_dalle_name')]", - "publicNetworkAccess": "Enabled" + "publicNetworkAccess": "Enabled", + "disableLocalAuth": "[parameters('disableLocalAuth')]" }, "sku": { "name": "[parameters('openAiSkuName')]" @@ -917,13 +969,223 @@ "location": "[parameters('location')]", "tags": "[parameters('tags')]", "kind": "StorageV2", - "sku": "[parameters('storageServiceSku')]" + "sku": "[parameters('storageServiceSku')]", + "properties": { + "allowSharedKeyAccess": "[not(parameters('disableLocalAuth'))]" + } + }, + { + "condition": "[parameters('disableLocalAuth')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "scope": "[format('Microsoft.DocumentDB/databaseAccounts/{0}', variables('cosmos_name'))]", + "name": "[guid(resourceId('Microsoft.DocumentDB/databaseAccounts', variables('cosmos_name')), variables('cosmosDbContributorRoleId'), 'role-assignment-cosmosDb')]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('webapp_name')), '2020-06-01', 'full').identity.principalId]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('cosmosDbContributorRoleId'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.DocumentDB/databaseAccounts', variables('cosmos_name'))]", + "[resourceId('Microsoft.Web/sites', variables('webapp_name'))]" + ] + }, + { + "condition": "[parameters('disableLocalAuth')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "scope": "[format('Microsoft.DocumentDB/databaseAccounts/{0}', variables('cosmos_name'))]", + "name": "[guid(resourceId('Microsoft.DocumentDB/databaseAccounts', variables('cosmos_name')), variables('cosmosDbOperatorRoleId'), 'role-assignment-cosmosDb')]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('webapp_name')), '2020-06-01', 'full').identity.principalId]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('cosmosDbOperatorRoleId'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.DocumentDB/databaseAccounts', variables('cosmos_name'))]", + "[resourceId('Microsoft.Web/sites', variables('webapp_name'))]" + ] + }, + { + "condition": "[parameters('disableLocalAuth')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', variables('openai_name')), variables('cognitiveServicesContributorRoleId'), 'role-assignment-cognitiveServices')]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('webapp_name')), '2020-06-01', 'full').identity.principalId]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('cognitiveServicesContributorRoleId'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts', variables('openai_name'))]", + "[resourceId('Microsoft.Web/sites', variables('webapp_name'))]" + ] + }, + { + "condition": "[parameters('disableLocalAuth')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', variables('openai_name'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', variables('openai_name')), variables('cognitiveServicesOpenAIContributorRoleId'), 'role-assignment-cognitiveServices')]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('webapp_name')), '2020-06-01', 'full').identity.principalId]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('cognitiveServicesOpenAIContributorRoleId'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts', variables('openai_name'))]", + "[resourceId('Microsoft.Web/sites', variables('webapp_name'))]" + ] + }, + { + "condition": "[parameters('disableLocalAuth')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', variables('form_recognizer_name')), variables('cognitiveServicesUserRoleId'), 'role-assignment-cognitiveServices')]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('webapp_name')), '2020-06-01', 'full').identity.principalId]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('cognitiveServicesUserRoleId'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts', variables('form_recognizer_name'))]", + "[resourceId('Microsoft.Web/sites', variables('webapp_name'))]" + ] + }, + { + "condition": "[parameters('disableLocalAuth')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storage_name'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', variables('storage_name')), variables('storageBlobDataContributorRoleId'), 'role-assignment-storage')]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('webapp_name')), '2020-06-01', 'full').identity.principalId]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('storageBlobDataContributorRoleId'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storage_name'))]", + "[resourceId('Microsoft.Web/sites', variables('webapp_name'))]" + ] + }, + { + "condition": "[parameters('disableLocalAuth')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "scope": "[format('Microsoft.Search/searchServices/{0}', variables('search_name'))]", + "name": "[guid(resourceId('Microsoft.Search/searchServices', variables('search_name')), variables('searchServiceContributorRoleId'), 'role-assignment-searchService')]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('webapp_name')), '2020-06-01', 'full').identity.principalId]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('searchServiceContributorRoleId'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Search/searchServices', variables('search_name'))]", + "[resourceId('Microsoft.Web/sites', variables('webapp_name'))]" + ] + }, + { + "condition": "[parameters('disableLocalAuth')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "scope": "[format('Microsoft.Search/searchServices/{0}', variables('search_name'))]", + "name": "[guid(resourceId('Microsoft.Search/searchServices', variables('search_name')), variables('searchIndexDataContributorRoleId'), 'role-assignment-searchService')]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('webapp_name')), '2020-06-01', 'full').identity.principalId]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('searchIndexDataContributorRoleId'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Search/searchServices', variables('search_name'))]", + "[resourceId('Microsoft.Web/sites', variables('webapp_name'))]" + ] + }, + { + "condition": "[parameters('disableLocalAuth')]", + "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions", + "apiVersion": "2024-05-15", + "name": "[format('{0}/{1}', variables('cosmos_name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', variables('cosmos_name')), parameters('roleDefinitionName')))]", + "properties": { + "roleName": "[parameters('roleDefinitionName')]", + "type": "CustomRole", + "assignableScopes": [ + "[resourceId('Microsoft.DocumentDB/databaseAccounts', variables('cosmos_name'))]" + ], + "permissions": [ + { + "dataActions": [ + "Microsoft.DocumentDB/databaseAccounts/readMetadata", + "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*", + "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*" + ] + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.DocumentDB/databaseAccounts', variables('cosmos_name'))]" + ] + }, + { + "condition": "[parameters('disableLocalAuth')]", + "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments", + "apiVersion": "2024-05-15", + "name": "[format('{0}/{1}', variables('cosmos_name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', variables('cosmos_name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', variables('cosmos_name')), parameters('roleDefinitionName'))), variables('webapp_name'), resourceId('Microsoft.DocumentDB/databaseAccounts', variables('cosmos_name'))))]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('webapp_name')), '2020-06-01', 'full').identity.principalId]", + "roleDefinitionId": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', variables('cosmos_name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', variables('cosmos_name')), parameters('roleDefinitionName')))]", + "scope": "[resourceId('Microsoft.DocumentDB/databaseAccounts', variables('cosmos_name'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.DocumentDB/databaseAccounts', variables('cosmos_name'))]", + "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', variables('cosmos_name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', variables('cosmos_name')), parameters('roleDefinitionName')))]", + "[resourceId('Microsoft.Web/sites', variables('webapp_name'))]" + ] } ], "outputs": { "url": { "type": "string", "value": "[format('https://{0}', reference(resourceId('Microsoft.Web/sites', variables('webapp_name')), '2020-06-01').defaultHostName)]" + }, + "webapp_name": { + "type": "string", + "value": "[variables('webapp_name')]" + }, + "openai_name": { + "type": "string", + "value": "[variables('openai_name')]" + }, + "openai_dalle_name": { + "type": "string", + "value": "[variables('openai_dalle_name')]" + }, + "cosmos_name": { + "type": "string", + "value": "[variables('cosmos_name')]" + }, + "cosmos_endpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.DocumentDB/databaseAccounts', variables('cosmos_name')), '2023-04-15').documentEndpoint]" + }, + "database_name": { + "type": "string", + "value": "[variables('databaseName')]" + }, + "history_container_name": { + "type": "string", + "value": "[variables('historyContainerName')]" + }, + "config_container_name": { + "type": "string", + "value": "[variables('configContainerName')]" + }, + "search_name": { + "type": "string", + "value": "[variables('search_name')]" + }, + "form_recognizer_name": { + "type": "string", + "value": "[variables('form_recognizer_name')]" + }, + "storage_name": { + "type": "string", + "value": "[variables('storage_name')]" + }, + "key_vault_name": { + "type": "string", + "value": "[variables('keyVaultName')]" } } } @@ -938,6 +1200,10 @@ "type": "string", "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('rg-{0}', parameters('name')))), 'Microsoft.Resources/deployments', 'all-resources'), '2022-09-01').outputs.url.value]" }, + "AZURE_WEBAPP_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('rg-{0}', parameters('name')))), 'Microsoft.Resources/deployments', 'all-resources'), '2022-09-01').outputs.webapp_name.value]" + }, "AZURE_LOCATION": { "type": "string", "value": "[parameters('location')]" @@ -945,6 +1211,86 @@ "AZURE_TENANT_ID": { "type": "string", "value": "[tenant().tenantId]" + }, + "AZURE_RESOURCE_GROUP": { + "type": "string", + "value": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('rg-{0}', parameters('name')))]" + }, + "AZURE_OPENAI_API_INSTANCE_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('rg-{0}', parameters('name')))), 'Microsoft.Resources/deployments', 'all-resources'), '2022-09-01').outputs.openai_name.value]" + }, + "AZURE_OPENAI_API_DEPLOYMENT_NAME": { + "type": "string", + "value": "[parameters('chatGptDeploymentName')]" + }, + "AZURE_OPENAI_API_VERSION": { + "type": "string", + "value": "[parameters('openAIApiVersion')]" + }, + "AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME": { + "type": "string", + "value": "[parameters('embeddingDeploymentName')]" + }, + "AZURE_OPENAI_DALLE_API_INSTANCE_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('rg-{0}', parameters('name')))), 'Microsoft.Resources/deployments', 'all-resources'), '2022-09-01').outputs.openai_dalle_name.value]" + }, + "AZURE_OPENAI_DALLE_API_DEPLOYMENT_NAME": { + "type": "string", + "value": "[parameters('dalleDeploymentName')]" + }, + "AZURE_OPENAI_DALLE_API_VERSION": { + "type": "string", + "value": "[parameters('dalleApiVersion')]" + }, + "AZURE_COSMOSDB_ACCOUNT_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('rg-{0}', parameters('name')))), 'Microsoft.Resources/deployments', 'all-resources'), '2022-09-01').outputs.cosmos_name.value]" + }, + "AZURE_COSMOSDB_URI": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('rg-{0}', parameters('name')))), 'Microsoft.Resources/deployments', 'all-resources'), '2022-09-01').outputs.cosmos_endpoint.value]" + }, + "AZURE_COSMOSDB_DB_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('rg-{0}', parameters('name')))), 'Microsoft.Resources/deployments', 'all-resources'), '2022-09-01').outputs.database_name.value]" + }, + "AZURE_COSMOSDB_CONTAINER_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('rg-{0}', parameters('name')))), 'Microsoft.Resources/deployments', 'all-resources'), '2022-09-01').outputs.history_container_name.value]" + }, + "AZURE_COSMOSDB_CONFIG_CONTAINER_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('rg-{0}', parameters('name')))), 'Microsoft.Resources/deployments', 'all-resources'), '2022-09-01').outputs.config_container_name.value]" + }, + "AZURE_SEARCH_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('rg-{0}', parameters('name')))), 'Microsoft.Resources/deployments', 'all-resources'), '2022-09-01').outputs.search_name.value]" + }, + "AZURE_SEARCH_INDEX_NAME": { + "type": "string", + "value": "[parameters('searchServiceIndexName')]" + }, + "AZURE_DOCUMENT_INTELLIGENCE_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('rg-{0}', parameters('name')))), 'Microsoft.Resources/deployments', 'all-resources'), '2022-09-01').outputs.form_recognizer_name.value]" + }, + "AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT": { + "type": "string", + "value": "[format('https://{0}.cognitiveservices.azure.com/', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('rg-{0}', parameters('name')))), 'Microsoft.Resources/deployments', 'all-resources'), '2022-09-01').outputs.form_recognizer_name.value)]" + }, + "AZURE_SPEECH_REGION": { + "type": "string", + "value": "[parameters('location')]" + }, + "AZURE_STORAGE_ACCOUNT_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('rg-{0}', parameters('name')))), 'Microsoft.Resources/deployments', 'all-resources'), '2022-09-01').outputs.storage_name.value]" + }, + "AZURE_KEY_VAULT_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('rg-{0}', parameters('name')))), 'Microsoft.Resources/deployments', 'all-resources'), '2022-09-01').outputs.key_vault_name.value]" } } } \ No newline at end of file diff --git a/infra/resources.bicep b/infra/resources.bicep index 48be71c66..64bbf60d2 100644 --- a/infra/resources.bicep +++ b/infra/resources.bicep @@ -31,6 +31,8 @@ param storageServiceImageContainerName string param location string = resourceGroup().location +param disableLocalAuth bool= false + @secure() param nextAuthHash string = uniqueString(newGuid()) @@ -118,6 +120,11 @@ resource webApp 'Microsoft.Web/sites@2020-06-01' = { ftpsState: 'Disabled' minTlsVersion: '1.2' appSettings: [ + { + name: 'USE_MANAGED_IDENTITIES' + value: disableLocalAuth + } + { name: 'AZURE_KEY_VAULT_NAME' value: keyVaultName @@ -128,7 +135,7 @@ resource webApp 'Microsoft.Web/sites@2020-06-01' = { } { name: 'AZURE_OPENAI_API_KEY' - value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_OPENAI_API_KEY.name})' + value: disableLocalAuth ? '' :'@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_OPENAI_API_KEY.name})' } { name: 'AZURE_OPENAI_API_INSTANCE_NAME' @@ -254,7 +261,7 @@ resource kvFunctionAppPermissions 'Microsoft.Authorization/roleAssignments@2020- name: guid(kv.id, webApp.name, keyVaultSecretsOfficerRole) scope: kv properties: { - principalId: webApp.identity.principalId + principalId: targetUserPrincipal principalType: 'ServicePrincipal' roleDefinitionId: keyVaultSecretsOfficerRole } @@ -347,6 +354,7 @@ resource cosmosDbAccount 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' = { kind: 'GlobalDocumentDB' properties: { databaseAccountOfferType: 'Standard' + disableLocalAuth: disableLocalAuth locations: [ { locationName: location @@ -407,6 +415,7 @@ resource formRecognizer 'Microsoft.CognitiveServices/accounts@2023-05-01' = { properties: { customSubDomainName: form_recognizer_name publicNetworkAccess: 'Enabled' + disableLocalAuth: disableLocalAuth } sku: { name: formRecognizerSkuName @@ -421,6 +430,7 @@ resource searchService 'Microsoft.Search/searchServices@2022-09-01' = { partitionCount: 1 publicNetworkAccess: 'enabled' replicaCount: 1 + disableLocalAuth: disableLocalAuth } sku: { name: searchServiceSkuName @@ -435,6 +445,7 @@ resource azureopenai 'Microsoft.CognitiveServices/accounts@2023-05-01' = { properties: { customSubDomainName: openai_name publicNetworkAccess: 'Enabled' + disableLocalAuth: disableLocalAuth } sku: { name: openAiSkuName @@ -447,7 +458,7 @@ resource llmdeployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05 name: deployment.name properties: { model: deployment.model - raiPolicyName: contains(deployment, 'raiPolicyName') ? deployment.raiPolicyName : null + /*raiPolicyName: contains(deployment, 'raiPolicyName') ? deployment.raiPolicyName : null*/ } sku: contains(deployment, 'sku') ? deployment.sku : { name: 'Standard' @@ -463,6 +474,7 @@ resource azureopenaidalle 'Microsoft.CognitiveServices/accounts@2023-05-01' = { properties: { customSubDomainName: openai_dalle_name publicNetworkAccess: 'Enabled' + disableLocalAuth: disableLocalAuth } sku: { name: openAiSkuName @@ -493,6 +505,7 @@ resource speechService 'Microsoft.CognitiveServices/accounts@2023-05-01' = { properties: { customSubDomainName: speech_service_name publicNetworkAccess: 'Enabled' + /* TODO: disableLocalAuth: disableLocalAuth*/ } sku: { name: speechServiceSkuName @@ -506,6 +519,9 @@ resource storage 'Microsoft.Storage/storageAccounts@2022-05-01' = { tags: tags kind: 'StorageV2' sku: storageServiceSku + properties:{ + allowSharedKeyAccess: !disableLocalAuth + } resource blobServices 'blobServices' = { name: 'default' @@ -518,4 +534,143 @@ resource storage 'Microsoft.Storage/storageAccounts@2022-05-01' = { } } + +//RBAC Roles for managed identity authentication + +var cosmosDbContributorRoleId = '5bd9cd88-fe45-4216-938b-f97437e15450' // Replace with actual role ID for Cosmos DB. +var cosmosDbOperatorRoleId= '230815da-be43-4aae-9cb4-875f7bd000aa' +var cognitiveServicesContributorRoleId = '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68' // Replace with actual role ID for Cognitive Services. +var cognitiveServicesUserRoleId='a97b65f3-24c7-4388-baec-2e87135dc908' +var storageBlobDataContributorRoleId = 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' // Replace with actual role ID for Blob Data Contributor. +var searchServiceContributorRoleId = '7ca78c08-252a-4471-8644-bb5ff32d4ba0' // Replace with actual role ID for Azure Search. +var cognitiveServicesOpenAIContributorRoleId='a001fd3d-188f-4b5d-821b-7da978bf7442' +var searchIndexDataContributorRoleId='8ebe5a00-799e-43f5-93ac-243d3dce84a7' + +var targetUserPrincipal = webApp.identity.principalId +// These are only deployed if local authentication has been disabled in the parameters + +resource cosmosDbRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = if (disableLocalAuth) { + name: guid(cosmosDbAccount.id, cosmosDbContributorRoleId, 'role-assignment-cosmosDb') + scope: cosmosDbAccount + properties: { + principalId: targetUserPrincipal + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', cosmosDbContributorRoleId) + } +} + + +resource cosmosDbRoleAssignmentOperator 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = if (disableLocalAuth) { + name: guid(cosmosDbAccount.id, cosmosDbOperatorRoleId, 'role-assignment-cosmosDb') + scope: cosmosDbAccount + properties: { + principalId: targetUserPrincipal + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', cosmosDbOperatorRoleId) + } +} + +resource cognitiveServicesRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = if (disableLocalAuth) { + name: guid(azureopenai.id, cognitiveServicesContributorRoleId, 'role-assignment-cognitiveServices') + scope: resourceGroup() + properties: { + principalId: targetUserPrincipal + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', cognitiveServicesContributorRoleId) + } +} + +resource cognitiveServicesOpenAIContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = if (disableLocalAuth) { + name: guid(azureopenai.id, cognitiveServicesOpenAIContributorRoleId, 'role-assignment-cognitiveServices') + scope: azureopenai + properties: { + principalId: targetUserPrincipal + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', cognitiveServicesOpenAIContributorRoleId) + } +} + +resource cognitiveServicesUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = if (disableLocalAuth) { + name: guid(formRecognizer.id, cognitiveServicesUserRoleId, 'role-assignment-cognitiveServices') + scope: resourceGroup() + properties: { + principalId: targetUserPrincipal + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', cognitiveServicesUserRoleId) + } +} + + + +resource storageBlobDataContributorRole 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = if (disableLocalAuth) { + name: guid(storage.id, storageBlobDataContributorRoleId, 'role-assignment-storage') + scope: storage + properties: { + principalId: targetUserPrincipal + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', storageBlobDataContributorRoleId) + } +} + +resource searchServiceContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = if (disableLocalAuth) { + name: guid(searchService.id, searchServiceContributorRoleId, 'role-assignment-searchService') + scope: searchService + properties: { + principalId: targetUserPrincipal + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', searchServiceContributorRoleId) + } +} +resource searchServiceIndexDataContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = if (disableLocalAuth) { + name: guid(searchService.id, searchIndexDataContributorRoleId, 'role-assignment-searchService') + scope: searchService + properties: { + principalId: targetUserPrincipal + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', searchIndexDataContributorRoleId) + } +} +//Special case for cosmosdb + + +@description('Name of the role definition.') +param roleDefinitionName string = 'Azure Cosmos DB for NoSQL Data Plane Owner' + + +resource definition 'Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions@2024-05-15'= if (disableLocalAuth) { + name: guid(cosmosDbAccount.id, roleDefinitionName) + parent: cosmosDbAccount + properties: { + roleName: roleDefinitionName + type: 'CustomRole' + assignableScopes: [ + cosmosDbAccount.id + ] + permissions: [ + { + dataActions: [ + 'Microsoft.DocumentDB/databaseAccounts/readMetadata' + 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*' + 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*' + ] + } + ] + } +} + +resource assignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2024-05-15'= if (disableLocalAuth) { + name: guid(definition.id, webApp.name, cosmosDbAccount.id) + parent: cosmosDbAccount + properties: { + principalId: targetUserPrincipal + roleDefinitionId: definition.id + scope: cosmosDbAccount.id + } + +} + output url string = 'https://${webApp.properties.defaultHostName}' +output webapp_name string = webapp_name +output openai_name string = openai_name +output openai_dalle_name string = openai_dalle_name +output cosmos_name string = cosmos_name +output cosmos_endpoint string = cosmosDbAccount.properties.documentEndpoint +output database_name string = databaseName +output history_container_name string = historyContainerName +output config_container_name string = configContainerName +output search_name string = search_name +output form_recognizer_name string = form_recognizer_name +output storage_name string = storage_name +output key_vault_name string = keyVaultName diff --git a/scripts/add_localdev_roles.ps1 b/scripts/add_localdev_roles.ps1 new file mode 100644 index 000000000..f4031f555 --- /dev/null +++ b/scripts/add_localdev_roles.ps1 @@ -0,0 +1,83 @@ +### +# This script adds a the required Cosmos DB Data Contributor role to the local user, +# so you can do local dev connecting to the deployed Azure resources. +# This will only work if you have used AZD to deploy the app, and have the required permissions to modify IAM. + +Write-Host "`nThis script will add the required IAM roles to allow the logged in user to run AzureChat locally." +Write-Host "This will only work if you have used AZD to deploy the app, and have the required permissions to modify IAM." + +Write-Host "`nLoading azd .env file from current environment..." +$output = azd env get-values +foreach ($line in $output) { + if (!$line.Contains('=')) { + continue + } + + $name, $value = $line.Split("=") + $value = $value -replace '^\"|\"$' + [Environment]::SetEnvironmentVariable($name, $value) +} + +$sub = $env:AZURE_SUBSCRIPTION_ID +$rg = $env:AZURE_RESOURCE_GROUP +$appName = $env:AZURE_WEBAPP_NAME +$cosmosAccName = $env:AZURE_COSMOSDB_ACCOUNT_NAME +$aillmName = $env:AZURE_OPENAI_API_INSTANCE_NAME +$searchName = $env:AZURE_SEARCH_NAME +$storageName = $env:AZURE_STORAGE_ACCOUNT_NAME +$dalleName = $env:AZURE_OPENAI_DALLE_API_INSTANCE_NAME +$docIntelName = $env:AZURE_DOCUMENT_INTELLIGENCE_NAME + +Write-Host "Resource Group: $rg" +Write-Host "App Host Name: $appName" +Write-Host "CosmosDB Account: $cosmosAccName" +Write-Host "OpenAI LLM Instance: $aillmName" +Write-Host "OpenAI DALL-E Instance: $dalleName" +Write-Host "Storage Account: $storageName" +Write-Host "Document Intelligence: $docIntelName" +Write-Host "Search Service: $searchName" + +# Get currently-logged in user +$userInfo = az ad signed-in-user show --query '{id: id, userPrincipalName: userPrincipalName}' | ConvertFrom-Json +$userId = $userInfo.id +$userPrincipalName = $userInfo.userPrincipalName + +Write-Host "`nLogged-in user: $userPrincipalName (ID: $userId)" + +$response = Read-Host -Prompt "`nDoes this look ok? `nEnter 'y' to continue, anything else to exit." +if ($response -ne "y") { + exit +} + +Write-Host "`nAdding 'Cosmos DB Built-in Data Contributor' role on $cosmosAccName" +az cosmosdb sql role assignment create --account-name $cosmosAccName ` + --resource-group $rg ` + --scope "/" ` + --principal-id $userId ` + --role-definition-id 00000000-0000-0000-0000-000000000002 + +$aillmNameScope = "/subscriptions/$sub/resourceGroups/$rg/providers/Microsoft.CognitiveServices/accounts/$aillmName" +$dalleScope = "/subscriptions/$sub/resourceGroups/$rg/providers/Microsoft.CognitiveServices/accounts/$dalleName" +$storageScope = "/subscriptions/$sub/resourceGroups/$rg/providers/Microsoft.Storage/storageAccounts/$storageName" +$searchScope = "/subscriptions/$sub/resourceGroups/$rg/providers/Microsoft.Search/searchServices/$searchName" +$docIntelScope = "/subscriptions/$sub/resourceGroups/$rg/providers/Microsoft.CognitiveServices/accounts/$docIntelName" + +Write-Host "`nAdding 'Cognitive Services OpenAI User' role on $aillmName" +az role assignment create --assignee $userId --role "Cognitive Services OpenAI User" --scope $aillmNameScope + +Write-Host "`nAdding 'Cognitive Services OpenAI User' role on $dalleName" +az role assignment create --assignee $userId --role "Cognitive Services OpenAI User" --scope $dalleScope + +Write-Host "`nAdding 'Storage Blob Data Contributor' role on $storageName" +az role assignment create --assignee $userId --role "Storage Blob Data Contributor" --scope $storageScope + +Write-Host "`nAdding 'Cognitive Services User' role on $docIntelName" +az role assignment create --assignee $userId --role "Cognitive Services User" --scope $docIntelScope + +Write-Host "`nAdding 'Search Service Contributor' role on $searchName" +az role assignment create --assignee $userId --role "Search Service Contributor" --scope $searchScope + +Write-Host "`nAdding 'Search Index Data Contributor' role on $searchName" +az role assignment create --assignee $userId --role "Search Index Data Contributor" --scope $searchScope + +Write-Host "All done!" \ No newline at end of file diff --git a/scripts/appreg_setup.ps1 b/scripts/appreg_setup.ps1 new file mode 100644 index 000000000..1af3221da --- /dev/null +++ b/scripts/appreg_setup.ps1 @@ -0,0 +1,55 @@ +# This script creates an App Registration in Entra ID for the AzureChat app and +# configures the required environment variables in the Web App. + +param ( + [string]$webappname +) + +if (-not $webappname) { + Write-Host "`n Usage: .\appreg_setup.ps1 -webappname [-showsecret] `n" + Write-Host "No arguments provided. Please provide the web app name from the Azure portal (e.g. azurechat-ulg3yy5ybjhdq)." + Write-Host "The -showsecret flag will display the client secret in the console output." + exit 1 +} + +$tenantid = (az account show --query tenantId --output tsv).Trim() + +Write-Host "About to create a new App Registration called $webappname-app in Microsoft Entra tenant $tenantid" +Write-Host "NOTE: This will only work if you have the necessary permissions in the tenant." + +$choice = Read-Host "Do you wish to proceed (y/n)?" +if ($choice -ne 'y' -and $choice -ne 'Y') { + Write-Host "exiting" + exit 1 +} + +$clientid = (az ad app create --display-name "$webappname-app" --sign-in-audience AzureADMyOrg --query appId --output tsv).Trim() +Write-Host "> Creating app registration with client id $clientid ..." +$objectid = (az ad app show --id $clientid --query id --output tsv).Trim() +Write-Host "Done. Object id is $objectid `n" + +Write-Host "> Creating client secret... (you can ignore credential warnings)" +$clientsecret = (az ad app credential reset --id $clientid --append --display-name mysecret --years 1 --query password --output tsv).Trim() +Write-Host "Done. `n" + +$redirecttype = "web" +$redirecturl = "https://$webappname.azurewebsites.net/api/auth/callback/azure-ad" +$graphurl = "https://graph.microsoft.com/v1.0/applications/$objectid" +Write-Host "> Updating redirect url to $redirecturl..." +az rest --method PATCH --uri $graphurl --body "{'$redirecttype':{'redirectUris':['$redirecturl']}}" +Write-Host "Done. `n" + +$rg = (az webapp list --query "[?name=='$webappname'].resourceGroup" --output tsv).Trim() +Write-Host "> Found the app resource group: $rg" + +Write-Host "> Updating app settings with client id, tenant id, and client secret..." +az webapp config appsettings set -n $webappname -g $rg --settings AZURE_AD_CLIENT_ID=$clientid AZURE_AD_TENANT_ID=$tenantid AZURE_AD_CLIENT_SECRET=$clientsecret --output none +Write-Host "Done. `n" + +Write-Host "AZURE_AD_CLIENT_ID=$clientid" +Write-Host "AZURE_AD_TENANT_ID=$tenantid" +if ($args -contains "-showsecret") { + Write-Host "AZURE_AD_CLIENT_SECRET=$clientsecret" + Write-Host "^^ Ensure you clear your console history to remove this secret" +} +Write-Host "> Setup complete. `n" \ No newline at end of file diff --git a/src/.env.example b/src/.env.example index 83999adfe..df5e2b125 100644 --- a/src/.env.example +++ b/src/.env.example @@ -1,24 +1,30 @@ # NOTES: +# - You will find most of these variables in the `/.azure//.env` file. # - Do not use double-quotes and do not delete any of the variables. # - Make sure that NEXTAUTH_URL=http://localhost:3000 has no comments in the same line. +# Enable the use of Managed Identity (passwordless authentication) for Azure services +# This will build a DefaultAzureCredential from your local logged in user (az login) +# Use this if you deployed using "disableLocalAuth = true" in main.bicep +USE_MANAGED_IDENTITIES=false + # Update your Azure OpenAI details # AZURE_OPENAI_API_INSTANCE_NAME should be just the name of azure openai resource and not the full url; # AZURE_OPENAI_API_DEPLOYMENT_NAME should be deployment name from your azure openai studio and not the model name. # AZURE_OPENAI_API_VERSION should be Supported versions checkout docs https://learn.microsoft.com/en-us/azure/ai-services/openai/reference AZURE_OPENAI_API_KEY=111111 -AZURE_OPENAI_API_INSTANCE_NAME=azurechat -AZURE_OPENAI_API_DEPLOYMENT_NAME=gpt-4 -AZURE_OPENAI_API_VERSION=2023-12-01-preview +AZURE_OPENAI_API_INSTANCE_NAME=ABC-aillm-XYZ +AZURE_OPENAI_API_DEPLOYMENT_NAME=gpt-4o +AZURE_OPENAI_API_VERSION=2024-10-21 AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME=embedding # DALL-E image creation endpoint config AZURE_OPENAI_DALLE_API_KEY=222222 -AZURE_OPENAI_DALLE_API_INSTANCE_NAME=azurechat-dall-e -AZURE_OPENAI_DALLE_API_DEPLOYMENT_NAME=dall-e +AZURE_OPENAI_DALLE_API_INSTANCE_NAME=ABC-aidalle-XYZ +AZURE_OPENAI_DALLE_API_DEPLOYMENT_NAME=dall-e-3 AZURE_OPENAI_DALLE_API_VERSION=2023-12-01-preview -# Update your admin email addresses - comma separated +# Update your admin email addresses - comma separated (add dev@localhost for local admin) ADMIN_EMAIL_ADDRESS=you@email.com,you2@email.com # Identity provider is optional if you are running in development mode locally (npm run dev) @@ -45,8 +51,8 @@ AZURE_COSMOSDB_CONFIG_CONTAINER_NAME=config # Azure AI Search is used for chat over your data AZURE_SEARCH_API_KEY= -AZURE_SEARCH_NAME= -AZURE_SEARCH_INDEX_NAME= +AZURE_SEARCH_NAME=ABCsearchXYZ +AZURE_SEARCH_INDEX_NAME=azure-chat # Azure AI Document Intelligence to extract content from your data AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT=https://NAME.cognitiveservices.azure.com/ @@ -56,18 +62,22 @@ AZURE_DOCUMENT_INTELLIGENCE_KEY= MAX_UPLOAD_DOCUMENT_SIZE=20000000 # Azure Speech to Text to convert audio to text +# NOTE: Speech does not support managed identity yet, so you need to populate these for speech to work locally AZURE_SPEECH_REGION= AZURE_SPEECH_KEY= # Azure Storage account to store files -AZURE_STORAGE_ACCOUNT_NAME=azurechat +AZURE_STORAGE_ACCOUNT_NAME=ABCstoXYZ AZURE_STORAGE_ACCOUNT_KEY=123456 # Azure Key Vault to store secrets -AZURE_KEY_VAULT_NAME= +AZURE_KEY_VAULT_NAME=ABC-kv-XYZ # optional - endpoint suffix overrides - typically used for Azure Government Clouds, China Clouds, etc. Only use if required. # AZURE_OPENAI_API_ENDPOINT_SUFFIX= # AZURE_SEARCH_ENDPOINT_SUFFIX= # AZURE_STORAGE_ENDPOINT_SUFFIX= -# AZURE_KEY_VAULT_ENDPOINT_SUFFIX= \ No newline at end of file +# AZURE_KEY_VAULT_ENDPOINT_SUFFIX= + +# to enable debug logging, set DEBUG=true +DEBUG=false \ No newline at end of file diff --git a/src/features/chat-page/chat-services/azure-ai-search/azure-ai-search.ts b/src/features/chat-page/chat-services/azure-ai-search/azure-ai-search.ts index b6d26679f..e0fad56ce 100644 --- a/src/features/chat-page/chat-services/azure-ai-search/azure-ai-search.ts +++ b/src/features/chat-page/chat-services/azure-ai-search/azure-ai-search.ts @@ -15,6 +15,8 @@ import { SearchIndex, } from "@azure/search-documents"; +const debug = process.env.DEBUG === "true"; + export interface AzureSearchDocumentIndex { id: string; pageContent: string; @@ -34,6 +36,7 @@ export const SimpleSearch = async ( filter?: string ): Promise>> => { try { + if (debug) console.log("Executing SimpleSearch with searchText:", searchText, "filter:", filter); const instance = AzureAISearchInstance(); const searchResults = await instance.search(searchText, { filter: filter }); @@ -45,11 +48,13 @@ export const SimpleSearch = async ( }); } + if (debug) console.log("SimpleSearch results:", results); return { status: "OK", response: results, }; } catch (e) { + console.error("SimpleSearch error:", e); return { status: "ERROR", errors: [ @@ -67,14 +72,16 @@ export const SimilaritySearch = async ( filter?: string ): Promise>> => { try { + if (debug) console.log("Executing SimilaritySearch with searchText:", searchText, "k:", k, "filter:", filter); const openai = OpenAIEmbeddingInstance(); const embeddings = await openai.embeddings.create({ input: searchText, model: "", }); - const searchClient = AzureAISearchInstance(); + if (debug) console.log("Embeddings obtained:", embeddings); + const searchClient = AzureAISearchInstance(); const searchResults = await searchClient.search(searchText, { top: k, filter: filter, @@ -98,11 +105,13 @@ export const SimilaritySearch = async ( }); } + if (debug) console.log("SimilaritySearch results:", results); return { status: "OK", response: results, }; } catch (e) { + console.error("SimilaritySearch error:", e); return { status: "ERROR", errors: [ @@ -122,6 +131,7 @@ export const ExtensionSimilaritySearch = async (props: { indexName: string; }): Promise>> => { try { + if (debug) console.log("Executing ExtensionSimilaritySearch with props:", props); const openai = OpenAIEmbeddingInstance(); const { searchText, vectors, apiKey, searchName, indexName } = props; @@ -129,10 +139,11 @@ export const ExtensionSimilaritySearch = async (props: { input: searchText, model: "", }); - const endpointSuffix = process.env.AZURE_SEARCH_ENDPOINT_SUFFIX || "search.windows.net"; - const endpoint = `https://${searchName}.${endpointSuffix}`; + if (debug) console.log("Embeddings obtained:", embeddings); + const endpointSuffix = process.env.AZURE_SEARCH_ENDPOINT_SUFFIX || "search.windows.net"; + const endpoint = `https://${searchName}.${endpointSuffix}`; const searchClient = new SearchClient( endpoint, indexName, @@ -141,8 +152,6 @@ export const ExtensionSimilaritySearch = async (props: { const searchResults = await searchClient.search(searchText, { top: 3, - - // filter: filter, vectorSearchOptions: { queries: [ { @@ -162,13 +171,9 @@ export const ExtensionSimilaritySearch = async (props: { document: result.document, }; - // exclude the all the fields that are not in the fields array const document = item.document as any; const newDocument: any = {}; - // iterate over the object entries in document - // and only include the fields that are in the fields array - for (const key in document) { const hasKey = vectors.includes(key); if (!hasKey) { @@ -178,15 +183,17 @@ export const ExtensionSimilaritySearch = async (props: { results.push({ score: result.score, - document: newDocument, // Use the newDocument object instead of the original document + document: newDocument, }); } + if (debug) console.log("ExtensionSimilaritySearch results:", results); return { status: "OK", response: results, }; } catch (e) { + console.error("ExtensionSimilaritySearch error:", e); return { status: "ERROR", errors: [ @@ -204,6 +211,7 @@ export const IndexDocuments = async ( chatThreadId: string ): Promise>> => { try { + if (debug) console.log("Indexing documents with fileName:", fileName, "chatThreadId:", chatThreadId); const documentsToIndex: AzureSearchDocumentIndex[] = []; for (const doc of docs) { @@ -219,6 +227,8 @@ export const IndexDocuments = async ( documentsToIndex.push(docToAdd); } + if (debug) console.log("Documents to index:", documentsToIndex); + const instance = AzureAISearchInstance(); const embeddingsResponse = await EmbedDocuments(documentsToIndex); @@ -246,11 +256,13 @@ export const IndexDocuments = async ( } }); + if (debug) console.log("IndexDocuments response:", response); return response; } return [embeddingsResponse]; } catch (e) { + console.error("IndexDocuments error:", e); return [ { status: "ERROR", @@ -268,7 +280,7 @@ export const DeleteDocuments = async ( chatThreadId: string ): Promise>> => { try { - // find all documents for chat thread + if (debug) console.log("Deleting documents for chatThreadId:", chatThreadId); const documentsInChatResponse = await SimpleSearch( undefined, `chatThreadId eq '${chatThreadId}'` @@ -279,6 +291,7 @@ export const DeleteDocuments = async ( const deletedResponse = await instance.deleteDocuments( documentsInChatResponse.response.map((r) => r.document) ); + const response: Array> = []; deletedResponse.results.forEach((r) => { if (r.succeeded) { @@ -298,11 +311,13 @@ export const DeleteDocuments = async ( } }); + if (debug) console.log("DeleteDocuments response:", response); return response; } return [documentsInChatResponse]; } catch (e) { + console.error("DeleteDocuments error:", e); return [ { status: "ERROR", @@ -320,8 +335,8 @@ export const EmbedDocuments = async ( documents: Array ): Promise>> => { try { + if (debug) console.log("Embedding documents:", documents.map((d) => d.id)); const openai = OpenAIEmbeddingInstance(); - const contentsToEmbed = documents.map((d) => d.pageContent); const embeddings = await openai.embeddings.create({ @@ -329,15 +344,19 @@ export const EmbedDocuments = async ( model: process.env.AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME, }); + if (debug) console.log("Embeddings received:", embeddings); + embeddings.data.forEach((embedding, index) => { documents[index].embedding = embedding.embedding; }); + if (debug) console.log("Documents after embedding:", documents); return { status: "OK", response: documents, }; } catch (e) { + console.error("EmbedDocuments error:", e); return { status: "ERROR", errors: [ @@ -353,13 +372,16 @@ export const EnsureIndexIsCreated = async (): Promise< ServerActionResponse > => { try { + console.log("Ensuring index is created: ", process.env.AZURE_SEARCH_INDEX_NAME); const client = AzureAISearchIndexClientInstance(); const result = await client.getIndex(process.env.AZURE_SEARCH_INDEX_NAME); + console.log("Index exists: ", result); return { status: "OK", response: result, }; } catch (e) { + console.log(`Error Creating index:${e}`); return await CreateSearchIndex(); } }; @@ -368,6 +390,7 @@ const CreateSearchIndex = async (): Promise< ServerActionResponse > => { try { + console.log("Creating search index"); const client = AzureAISearchIndexClientInstance(); const result = await client.createIndex({ name: process.env.AZURE_SEARCH_INDEX_NAME, @@ -433,11 +456,13 @@ const CreateSearchIndex = async (): Promise< ], }); + console.log("Search index created:", result); return { status: "OK", response: result, }; } catch (e) { + console.error("CreateSearchIndex error:", e); return { status: "ERROR", errors: [ diff --git a/src/features/chat-page/chat-services/chat-document-service.ts b/src/features/chat-page/chat-services/chat-document-service.ts index 58b9defec..232b97fdc 100644 --- a/src/features/chat-page/chat-services/chat-document-service.ts +++ b/src/features/chat-page/chat-services/chat-document-service.ts @@ -14,32 +14,40 @@ import { CHAT_DOCUMENT_ATTRIBUTE, ChatDocumentModel } from "./models"; const MAX_UPLOAD_DOCUMENT_SIZE: number = 20000000; const CHUNK_SIZE = 2300; -// 25% overlap const CHUNK_OVERLAP = CHUNK_SIZE * 0.25; +const debug = process.env.DEBUG === "true"; + export const CrackDocument = async ( formData: FormData ): Promise> => { try { + if (debug) console.log("CrackDocument: Ensuring index is created."); const response = await EnsureIndexIsCreated(); if (response.status === "OK") { + if (debug) console.log("CrackDocument: Index is created, loading file."); const fileResponse = await LoadFile(formData); if (fileResponse.status === "OK") { + if (debug) console.log("CrackDocument: File loaded successfully, splitting documents."); const splitDocuments = await ChunkDocumentWithOverlap( fileResponse.response.join("\n") ); + if (debug) console.log("CrackDocument: Documents split successfully."); return { status: "OK", response: splitDocuments, }; } + console.error("CrackDocument: File loading failed.", fileResponse.errors); return fileResponse; } + console.error("CrackDocument: Index creation failed.", response.errors); return response; } catch (e) { + console.error("CrackDocument error:", e); return { status: "ERROR", errors: [ @@ -55,6 +63,7 @@ const LoadFile = async ( formData: FormData ): Promise> => { try { + if (debug) console.log("LoadFile: Loading file from form data."); const file: File | null = formData.get("file") as unknown as File; const fileSize = process.env.MAX_UPLOAD_DOCUMENT_SIZE @@ -62,10 +71,12 @@ const LoadFile = async ( : MAX_UPLOAD_DOCUMENT_SIZE; if (file && file.size < fileSize) { + if (debug) console.log("LoadFile: File size is within the acceptable limit."); const client = DocumentIntelligenceInstance(); const blob = new Blob([file], { type: file.type }); + if (debug) console.log("LoadFile: Beginning document analysis."); const poller = await client.beginAnalyzeDocument( "prebuilt-read", await blob.arrayBuffer() @@ -78,6 +89,7 @@ const LoadFile = async ( for (const paragraph of paragraphs) { docs.push(paragraph.content); } + if (debug) console.log("LoadFile: Document analysis completed successfully."); } return { @@ -85,6 +97,7 @@ const LoadFile = async ( response: docs, }; } else { + console.error("LoadFile: File size is too large."); return { status: "ERROR", errors: [ @@ -95,6 +108,7 @@ const LoadFile = async ( }; } } catch (e) { + console.error("LoadFile error:", e); return { status: "ERROR", errors: [ @@ -110,6 +124,7 @@ export const FindAllChatDocuments = async ( chatThreadID: string ): Promise> => { try { + if (debug) console.log("FindAllChatDocuments: Searching documents for chatThreadID:", chatThreadID); const querySpec: SqlQuerySpec = { query: "SELECT * FROM root r WHERE r.type=@type AND r.chatThreadId = @threadId AND r.isDeleted=@isDeleted", @@ -134,11 +149,13 @@ export const FindAllChatDocuments = async ( .fetchAll(); if (resources) { + if (debug) console.log(`FindAllChatDocuments: ${resources.length} Documents found.`); return { status: "OK", response: resources, }; } else { + console.error("FindAllChatDocuments: No documents found."); return { status: "ERROR", errors: [ @@ -149,6 +166,7 @@ export const FindAllChatDocuments = async ( }; } } catch (e) { + console.error("FindAllChatDocuments error:", e); return { status: "ERROR", errors: [ @@ -165,6 +183,7 @@ export const CreateChatDocument = async ( chatThreadID: string ): Promise> => { try { + if (debug) console.log("CreateChatDocument: Creating document with fileName:", fileName, "chatThreadID:", chatThreadID); const modelToSave: ChatDocumentModel = { chatThreadId: chatThreadID, id: uniqueId(), @@ -175,20 +194,21 @@ export const CreateChatDocument = async ( name: fileName, }; - const { resource } = - await HistoryContainer().items.upsert(modelToSave); + const { resource } = await HistoryContainer().items.upsert(modelToSave); RevalidateCache({ page: "chat", params: chatThreadID, }); if (resource) { + if (debug) console.log("CreateChatDocument: Document created successfully."); return { status: "OK", response: resource, }; } + console.error("CreateChatDocument: Unable to save chat document."); return { status: "ERROR", errors: [ @@ -198,6 +218,7 @@ export const CreateChatDocument = async ( ], }; } catch (e) { + console.error("CreateChatDocument error:", e); return { status: "ERROR", errors: [ @@ -212,17 +233,17 @@ export const CreateChatDocument = async ( export async function ChunkDocumentWithOverlap( document: string ): Promise { + if (debug) console.log("ChunkDocumentWithOverlap: Starting chunking process."); const chunks: string[] = []; if (document.length <= CHUNK_SIZE) { - // If the document is smaller than the desired chunk size, return it as a single chunk. + if (debug) console.log("ChunkDocumentWithOverlap: Document length is within single chunk size."); chunks.push(document); return chunks; } let startIndex = 0; - // Split the document into chunks of the desired size, with overlap. while (startIndex < document.length) { const endIndex = startIndex + CHUNK_SIZE; const chunk = document.substring(startIndex, endIndex); @@ -230,5 +251,6 @@ export async function ChunkDocumentWithOverlap( startIndex = endIndex - CHUNK_OVERLAP; } + if (debug) console.log("ChunkDocumentWithOverlap: Chunking completed.", chunks); return chunks; } diff --git a/src/features/common/services/ai-search.ts b/src/features/common/services/ai-search.ts index 86b669e04..f966bf571 100644 --- a/src/features/common/services/ai-search.ts +++ b/src/features/common/services/ai-search.ts @@ -4,57 +4,70 @@ import { SearchIndexClient, SearchIndexerClient, } from "@azure/search-documents"; +import { DefaultAzureCredential } from "@azure/identity"; -export const AzureAISearchCredentials = () => { - const apiKey = process.env.AZURE_SEARCH_API_KEY; - const searchName = process.env.AZURE_SEARCH_NAME; - const indexName = process.env.AZURE_SEARCH_INDEX_NAME; - - if (!apiKey || !searchName || !indexName) { - throw new Error( - "One or more Azure AI Search environment variables are not set" - ); - } - const endpointSuffix = process.env.AZURE_SEARCH_ENDPOINT_SUFFIX || "search.windows.net"; - - const endpoint = `https://${searchName}.${endpointSuffix}`; - return { - apiKey, - endpoint, - indexName, - }; -}; +const USE_MANAGED_IDENTITIES = process.env.USE_MANAGED_IDENTITIES === "true"; +const endpointSuffix = process.env.AZURE_SEARCH_ENDPOINT_SUFFIX || "search.windows.net"; +const apiKey = process.env.AZURE_SEARCH_API_KEY; +const searchName = process.env.AZURE_SEARCH_NAME; +const indexName = process.env.AZURE_SEARCH_INDEX_NAME; +const endpoint = `https://${searchName}.${endpointSuffix}`; +const debug = process.env.DEBUG === "true"; + +console.log("Configuration parameters:", { + USE_MANAGED_IDENTITIES, + endpointSuffix, + searchName, + indexName, + endpoint, +}); + +export const GetCredential = () => { + console.log("Getting credential using", USE_MANAGED_IDENTITIES ? "Managed Identities" : "API Key"); + const credential = USE_MANAGED_IDENTITIES + ? new DefaultAzureCredential() + : new AzureKeyCredential(apiKey); + + if (debug) console.log("Credential obtained:", credential); + return credential; +} export const AzureAISearchInstance = () => { - const { apiKey, endpoint, indexName } = AzureAISearchCredentials(); + console.log("Creating Azure AI Search Client Instance"); + const credential = GetCredential(); const searchClient = new SearchClient( endpoint, indexName, - new AzureKeyCredential(apiKey) + credential ); + console.log("Search Client created:", searchClient); return searchClient; }; export const AzureAISearchIndexClientInstance = () => { - const { apiKey, endpoint } = AzureAISearchCredentials(); + console.log("Creating Azure AI Search Index Client Instance"); + const credential = GetCredential(); const searchClient = new SearchIndexClient( endpoint, - new AzureKeyCredential(apiKey) + credential ); + console.log("Search Index Client created:", searchClient); return searchClient; }; export const AzureAISearchIndexerClientInstance = () => { - const { apiKey, endpoint } = AzureAISearchCredentials(); + console.log("Creating Azure AI Search Indexer Client Instance"); + const credential = GetCredential(); const client = new SearchIndexerClient( endpoint, - new AzureKeyCredential(apiKey) + credential ); + console.log("Search Indexer Client created:", client); return client; }; diff --git a/src/features/common/services/azure-storage.ts b/src/features/common/services/azure-storage.ts index 42e0eaffe..13f5dc962 100644 --- a/src/features/common/services/azure-storage.ts +++ b/src/features/common/services/azure-storage.ts @@ -1,22 +1,28 @@ import { BlobServiceClient, RestError } from "@azure/storage-blob"; import { ServerActionResponse } from "../server-action-response"; +import { DefaultAzureCredential } from "@azure/identity"; // initialize the blobServiceClient +const USE_MANAGED_IDENTITIES = process.env.USE_MANAGED_IDENTITIES === "true"; + const InitBlobServiceClient = () => { - const acc = process.env.AZURE_STORAGE_ACCOUNT_NAME; - const key = process.env.AZURE_STORAGE_ACCOUNT_KEY; + const accountName = process.env.AZURE_STORAGE_ACCOUNT_NAME; + const endpointSuffix = process.env.AZURE_STORAGE_ENDPOINT_SUFFIX || "core.windows.net"; + const endpoint = `https://${accountName}.blob.${endpointSuffix}`; + + if (USE_MANAGED_IDENTITIES) { + return new BlobServiceClient(endpoint, new DefaultAzureCredential()); + } - if (!acc || !key) + const accountKey = process.env.AZURE_STORAGE_ACCOUNT_KEY; + if (!accountName || !accountKey) { throw new Error( "Azure Storage Account not configured correctly, check environment variables." ); - const endpointSuffix = process.env.AZURE_STORAGE_ENDPOINT_SUFFIX || "core.windows.net"; - - const connectionString = `DefaultEndpointsProtocol=https;AccountName=${acc};AccountKey=${key};EndpointSuffix=${endpointSuffix}`; + } - const blobServiceClient = - BlobServiceClient.fromConnectionString(connectionString); - return blobServiceClient; + const connectionString = `DefaultEndpointsProtocol=https;AccountName=${accountName};AccountKey=${accountKey};EndpointSuffix=${endpointSuffix}`; + return BlobServiceClient.fromConnectionString(connectionString); }; export const UploadBlob = async ( @@ -79,7 +85,8 @@ export const GetBlob = async ( }; } catch (error) { if (error instanceof RestError) { - if (error.statusCode === 404) { + const restError = error as RestError; + if (restError.statusCode === 404) { return { status: "NOT_FOUND", errors: [ diff --git a/src/features/common/services/cosmos.ts b/src/features/common/services/cosmos.ts index 82f4933ea..2c6d59674 100644 --- a/src/features/common/services/cosmos.ts +++ b/src/features/common/services/cosmos.ts @@ -1,22 +1,38 @@ import { CosmosClient } from "@azure/cosmos"; +import { DefaultAzureCredential } from "@azure/identity"; -// Read Cosmos DB_NAME and CONTAINER_NAME from .env +// Configure Cosmos DB details const DB_NAME = process.env.AZURE_COSMOSDB_DB_NAME || "chat"; const CONTAINER_NAME = process.env.AZURE_COSMOSDB_CONTAINER_NAME || "history"; -const CONFIG_CONTAINER_NAME = - process.env.AZURE_COSMOSDB_CONFIG_CONTAINER_NAME || "config"; +const CONFIG_CONTAINER_NAME = process.env.AZURE_COSMOSDB_CONFIG_CONTAINER_NAME || "config"; +const USE_MANAGED_IDENTITIES = process.env.USE_MANAGED_IDENTITIES === "true"; + +const getCosmosCredential = () => { + if (USE_MANAGED_IDENTITIES) { + return new DefaultAzureCredential(); + } + const key = process.env.AZURE_COSMOSDB_KEY; + if (!key) { + throw new Error("Azure Cosmos DB key is not provided in environment variables."); + } + return key; +}; export const CosmosInstance = () => { const endpoint = process.env.AZURE_COSMOSDB_URI; - const key = process.env.AZURE_COSMOSDB_KEY; - if (!endpoint || !key) { + if (!endpoint) { throw new Error( - "Azure Cosmos DB is not configured. Please configure it in the .env file." + "Azure Cosmos DB endpoint is not configured. Please configure it in the .env file." ); } - return new CosmosClient({ endpoint, key }); + const credential = getCosmosCredential(); + if (credential instanceof DefaultAzureCredential) { + return new CosmosClient({ endpoint, aadCredentials: credential }); + } else { + return new CosmosClient({ endpoint, key: credential }); + } }; export const ConfigContainer = () => { diff --git a/src/features/common/services/document-intelligence.ts b/src/features/common/services/document-intelligence.ts index 163f3e1a4..25ff47c4b 100644 --- a/src/features/common/services/document-intelligence.ts +++ b/src/features/common/services/document-intelligence.ts @@ -2,21 +2,37 @@ import { AzureKeyCredential, DocumentAnalysisClient, } from "@azure/ai-form-recognizer"; +import { DefaultAzureCredential } from "@azure/identity"; + +const USE_MANAGED_IDENTITIES = process.env.USE_MANAGED_IDENTITIES === "true"; +console.log("Using Managed Identities:", USE_MANAGED_IDENTITIES); + +const debug = process.env.DEBUG === "true"; export const DocumentIntelligenceInstance = () => { const endpoint = process.env.AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT; - const key = process.env.AZURE_DOCUMENT_INTELLIGENCE_KEY; + console.log("Document Intelligence Endpoint:", endpoint); + + if (!endpoint) { + throw new Error( + "Document Intelligence environment variable for the endpoint is not set" + ); + } - if (!endpoint || !key) { + const credential = USE_MANAGED_IDENTITIES + ? new DefaultAzureCredential() + : new AzureKeyCredential(process.env.AZURE_DOCUMENT_INTELLIGENCE_KEY); + + if (!USE_MANAGED_IDENTITIES && !process.env.AZURE_DOCUMENT_INTELLIGENCE_KEY) { throw new Error( - "One or more Document Intelligence environment variables are not set" + "Document Intelligence environment variable for the key is not set" ); } - const client = new DocumentAnalysisClient( - endpoint, - new AzureKeyCredential(key) - ); + console.log("Credential obtained using", USE_MANAGED_IDENTITIES ? "Managed Identities" : "API Key"); + + const client = new DocumentAnalysisClient(endpoint, credential); + if (debug) console.log("Document Analysis Client created:", client); return client; }; diff --git a/src/features/common/services/openai.ts b/src/features/common/services/openai.ts index 0e44a4523..b9962858d 100644 --- a/src/features/common/services/openai.ts +++ b/src/features/common/services/openai.ts @@ -1,60 +1,87 @@ import { OpenAI } from "openai"; +import { DefaultAzureCredential, getBearerTokenProvider } from "@azure/identity"; +import { AzureOpenAI } from "openai"; -export const OpenAIInstance = () => { +const USE_MANAGED_IDENTITIES = process.env.USE_MANAGED_IDENTITIES === "true"; + +export const OpenAIInstance = () => { const endpointSuffix = process.env.AZURE_OPENAI_API_ENDPOINT_SUFFIX || "openai.azure.com"; - const openai = new OpenAI({ - apiKey: process.env.AZURE_OPENAI_API_KEY, - baseURL: `https://${process.env.AZURE_OPENAI_API_INSTANCE_NAME}.${endpointSuffix}/openai/deployments/${process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME}`, - defaultQuery: { "api-version": process.env.AZURE_OPENAI_API_VERSION }, - defaultHeaders: { "api-key": process.env.AZURE_OPENAI_API_KEY }, - }); - return openai; + let token = process.env.AZURE_OPENAI_API_KEY; + if (USE_MANAGED_IDENTITIES) { + const credential = new DefaultAzureCredential(); + const scope = "https://cognitiveservices.azure.com/.default"; + const azureADTokenProvider = getBearerTokenProvider(credential, scope); + const deployment = process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME; + const apiVersion = process.env.AZURE_OPENAI_API_VERSION; + const client = new AzureOpenAI({ + azureADTokenProvider, + deployment, + apiVersion, + baseURL: `https://${process.env.AZURE_OPENAI_API_INSTANCE_NAME}.${endpointSuffix}/openai/deployments/${process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME}` + }); + return client; + } else { + const openai = new OpenAI({ + apiKey: token, + baseURL: `https://${process.env.AZURE_OPENAI_API_INSTANCE_NAME}.${endpointSuffix}/openai/deployments/${process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME}`, + defaultQuery: { "api-version": process.env.AZURE_OPENAI_API_VERSION }, + defaultHeaders: { "api-key": process.env.AZURE_OPENAI_API_KEY }, + }); + return openai; + } }; -export const OpenAIEmbeddingInstance = () => { - if ( - !process.env.AZURE_OPENAI_API_KEY || - !process.env.AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME || - !process.env.AZURE_OPENAI_API_INSTANCE_NAME - ) { - throw new Error( - "Azure OpenAI Embeddings endpoint config is not set, check environment variables." - ); - } +export const OpenAIEmbeddingInstance = () => { const endpointSuffix = process.env.AZURE_OPENAI_API_ENDPOINT_SUFFIX || "openai.azure.com"; - - const openai = new OpenAI({ - apiKey: process.env.AZURE_OPENAI_API_KEY, - baseURL: `https://${process.env.AZURE_OPENAI_API_INSTANCE_NAME}.${endpointSuffix}/openai/deployments/${process.env.AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME}`, - defaultQuery: { "api-version": process.env.AZURE_OPENAI_API_VERSION }, - defaultHeaders: { "api-key": process.env.AZURE_OPENAI_API_KEY }, - }); - return openai; + let token = process.env.AZURE_OPENAI_API_KEY; + if (USE_MANAGED_IDENTITIES) { + const credential = new DefaultAzureCredential(); + const scope = "https://cognitiveservices.azure.com/.default"; + const azureADTokenProvider = getBearerTokenProvider(credential, scope); + const deployment = process.env.AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME; + const apiVersion = process.env.AZURE_OPENAI_API_VERSION; + const client = new AzureOpenAI({ + azureADTokenProvider, + deployment, + apiVersion, + baseURL: `https://${process.env.AZURE_OPENAI_API_INSTANCE_NAME}.${endpointSuffix}/openai/deployments/${process.env.AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME}` + }); + return client; + } else { + const openai = new OpenAI({ + apiKey: token, + baseURL: `https://${process.env.AZURE_OPENAI_API_INSTANCE_NAME}.${endpointSuffix}/openai/deployments/${process.env.AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME}`, + defaultQuery: { "api-version": process.env.AZURE_OPENAI_API_VERSION }, + defaultHeaders: { "api-key": token }, + }); + return openai; + } }; -// a new instance definition for DALL-E image generation -export const OpenAIDALLEInstance = () => { - if ( - !process.env.AZURE_OPENAI_DALLE_API_KEY || - !process.env.AZURE_OPENAI_DALLE_API_DEPLOYMENT_NAME || - !process.env.AZURE_OPENAI_DALLE_API_INSTANCE_NAME - ) { - throw new Error( - "Azure OpenAI DALLE endpoint config is not set, check environment variables." - ); - } +// A new instance definition for DALL-E image generation +export const OpenAIDALLEInstance = () => { const endpointSuffix = process.env.AZURE_OPENAI_API_ENDPOINT_SUFFIX || "openai.azure.com"; - - const openai = new OpenAI({ - apiKey: process.env.AZURE_OPENAI_DALLE_API_KEY, - baseURL: `https://${process.env.AZURE_OPENAI_DALLE_API_INSTANCE_NAME}.${endpointSuffix}/openai/deployments/${process.env.AZURE_OPENAI_DALLE_API_DEPLOYMENT_NAME}`, - defaultQuery: { - "api-version": - process.env.AZURE_OPENAI_DALLE_API_VERSION || "2023-12-01-preview", - }, - defaultHeaders: { - "api-key": process.env.AZURE_OPENAI_DALLE_API_KEY, - }, - }); - return openai; + let token = process.env.AZURE_OPENAI_DALLE_API_KEY; + if (USE_MANAGED_IDENTITIES) { + const credential = new DefaultAzureCredential(); + const scope = "https://cognitiveservices.azure.com/.default"; + const azureADTokenProvider = getBearerTokenProvider(credential, scope); + const deployment = process.env.AZURE_OPENAI_DALLE_API_DEPLOYMENT_NAME; + const apiVersion = process.env.AZURE_OPENAI_DALLE_API_VERSION || "2023-12-01-preview"; + const client = new AzureOpenAI({ + azureADTokenProvider, + deployment, + apiVersion, + baseURL: `https://${process.env.AZURE_OPENAI_DALLE_API_INSTANCE_NAME}.${endpointSuffix}/openai/deployments/${process.env.AZURE_OPENAI_DALLE_API_DEPLOYMENT_NAME}` + }); + return client; + } else { + const openai = new OpenAI({ + apiKey: token, + baseURL: `https://${process.env.AZURE_OPENAI_DALLE_API_INSTANCE_NAME}.${endpointSuffix}/openai/deployments/${process.env.AZURE_OPENAI_DALLE_API_DEPLOYMENT_NAME}`, + defaultQuery: { "api-version": process.env.AZURE_OPENAI_DALLE_API_VERSION || "2023-12-01-preview" }, + defaultHeaders: { "api-key": token }, + }); + return openai; + } }; diff --git a/src/package.json b/src/package.json index 59375f410..9267d5cc4 100644 --- a/src/package.json +++ b/src/package.json @@ -6,12 +6,13 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "debug": "set NODE_OPTIONS=--inspect && next dev" }, "dependencies": { "@azure/ai-form-recognizer": "^5.0.0", "@azure/cosmos": "^4.0.0", - "@azure/identity": "^4.0.0", + "@azure/identity": "^4.4.1", "@azure/keyvault-secrets": "^4.7.0", "@azure/search-documents": "^12.0.0", "@azure/storage-blob": "^12.17.0", @@ -42,7 +43,8 @@ "next": "14.0.4", "next-auth": "^4.24.5", "next-themes": "^0.2.1", - "openai": "^4.26.0", + "openai": "^4.67.1", + "@azure/openai":"^2.0.0-beta.2", "react": "^18", "react-dom": "^18", "react-syntax-highlighter": "^15.5.0",