diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 717e96e26..29992bcd1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,7 +1,7 @@ { "name": "Azure Chat Solution Accelerator powered by Azure Open AI Service", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/javascript-node:0-18-bullseye", + "image": "mcr.microsoft.com/devcontainers/javascript-node:22", // Features to add to the dev container. More info: https://containers.dev/features. "features": { @@ -21,15 +21,13 @@ "version": "latest", "dockerDashComposeVersion": "v2" }, - "ghcr.io/devcontainers-contrib/features/zsh-plugins:0": { + "ghcr.io/devcontainers-extra/features/zsh-plugins:0": { "plugins": "ssh-agent npm zsh-syntax-highlighting zsh-autosuggestions", - "omzPlugins": "https://github.com/zsh-users/zsh-autosuggestions https://github.com/zsh-users/zsh-syntax-highlighting", - "username": "node" + "omzPlugins": "https://github.com/zsh-users/zsh-autosuggestions https://github.com/zsh-users/zsh-syntax-highlighting" }, "ghcr.io/devcontainers/features/azure-cli:1": { "installBicep": true }, - "ghcr.io/stuartleeks/dev-container-features/shell-history:0": {}, "ghcr.io/azure/azure-dev/azd:0.1.0": {} }, @@ -42,10 +40,9 @@ }, "extensions": ["shardulm94.trailing-spaces"] } - } - + }, // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [3000], + "forwardPorts": [3000] // Use 'portsAttributes' to set default properties for specific forwarded ports. // More info: https://containers.dev/implementors/json_reference/#port-attributes @@ -57,7 +54,7 @@ // }, // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "yarn install" + // "postCreateCommand": "mkdir ~/.ssh", // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. // "remoteUser": "root" diff --git a/.github/workflows/open-ai-app.yml b/.github/workflows/open-ai-app.yml index 08fdd810a..b7dd667d6 100644 --- a/.github/workflows/open-ai-app.yml +++ b/.github/workflows/open-ai-app.yml @@ -68,18 +68,18 @@ jobs: node-version: "20.x" - name: ⬇️ Download artifact from build job - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v4.1.8 with: name: Nextjs-site - name: 🗝️ Azure Login - uses: azure/login@v1 + uses: azure/login@v2.2.0 with: creds: ${{ secrets.AZURE_CREDENTIALS }} # Set the build during deployment setting to false. This setting was added in the templates to all azd to work, but breaks deployment via webapps-deploy - name: Azure CLI script - uses: azure/CLI@v1 + uses: azure/CLI@v2.1.0 with: inlineScript: | rg=$(az webapp list --query "[?name=='${{ secrets.AZURE_APP_SERVICE_NAME }}'].resourceGroup" --output tsv) @@ -91,7 +91,7 @@ jobs: - name: 🚀 Deploy to Azure Web App id: deploy-to-webapp - uses: azure/webapps-deploy@v2 + uses: azure/webapps-deploy@v3.0.1 with: app-name: ${{ secrets.AZURE_APP_SERVICE_NAME }} package: ${{ github.workspace }}/Nextjs-site.zip 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..b0155176a 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,47 @@ +# What's new - 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` and `appreg_setup.sh` helper scripts 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 🥰 +- Added support for private endpoints and ESLZ compliant deployment + # 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 +77,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..f482a933f 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 "\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/10-private-endpoints.md b/docs/10-private-endpoints.md new file mode 100644 index 000000000..06fd8737a --- /dev/null +++ b/docs/10-private-endpoints.md @@ -0,0 +1,40 @@ +# Securing Azure Chat Resources with Private Endpoints + +## Overview + +You can enhance the security of the Azure Chat application by using Private Endpoints. Private Endpoints provide a secure and private connection to Azure services, ensuring that traffic between the Web App that hosts the application and the supporting Azure services remains within the Microsoft network. Implementing Private Endpoints also allows for the removal of any public network access to the these services. + +The included bicep template can optionally be configured to make a number of key changes to the deployed Azure resources to enable Private Endpoints: +1. Deploy a Virtual Network and 2 subnets - one for the Web App backend, and one for the Private Endpoints. +1. Deploy Private Endpoints for the following services: + - OpenAI Service + - Cosmos DB + - Storage Account + - AI Search Service + - AI Document Intelligence + - Key Vault +1. Configure the Web App to use the Virtual Network for outgoing requests +1. Remove public access to all of the above services - only clients and applications within the Virtual Network will be able to access these services. + +![Private Endpoints image](/docs/images/private-endpoints.png) + +Using Private Endpoints for these services is a recommended best practice for production deployments of Azure Chat, and it can also be useful in Azure environments where policies are in place to disable public access to some services. There are some additional considerations to be aware of when using Private Endpoints however: +- **Local Developemnt**: If you deploy the Azure resources with Private Endpoints it will be more difficult to use these services if you are running the application locally - you will need to use a development environment that is connected to the Virtual Network. +- **Resource Access in the Portal** - If you deploy the Azure resources with Private Endpoints you will need to use a development environment that is connected to the Virtual Network to access the data plane for any of these services (e.g. the Cosmos DB Azure Portal Data Explorer). + +## How to enable Private Endpoints + +The addition of Private Endpoints and it's supportted configuration is controlled by the `usePrivateEndpoints` parameter in the bicep template. To enable Private Endpoints, set this parameter to `true`. If you are using the Azure Developer CLI to deploy the application see the [Deploy to Azure](4-deploy-to-azure.md) page for more details on how to do this. + +## Additional Configuration + +By default the Virtual Network that is deployed when you set `usePrivateEndpoints` to `true` has the following properies: +- **Address Space**: `192.168.0.0/16` +- **Subnet for Private Endpoints**: `privateEndpoints` - `192.168.0.0/24` +- **Subnet for Web App**: `appServiceBackend` - `192.168.1.0/24` + +The address spaces for each of the subnets can be changed by setting the `privateEndpointVNetPrefix`, `privateEndpointSubnetAddressPrefix` and `appServiceBackendSubnetAddressPrefix` parameters in the bicep template. + +If you want to deploy these resources into an existing Virtual Network you will need to modify the `private_endpoints_core.bicep` template - a parameterised version of this is not currently available. If you do this note that the deployment includes Private DNS Zones for each of the services that are deployed with Private Endpoints - if your Virtual Network uses a custom DNS server you will need to ensure that the DNS server can resolve the Private DNS Zones. + + diff --git a/docs/2-run-locally.md b/docs/2-run-locally.md new file mode 100644 index 000000000..5a587f567 --- /dev/null +++ b/docs/2-run-locally.md @@ -0,0 +1,38 @@ +# 👨🏻‍💻 Run Locally + +Clone this repository locally or fork to your GitHub account. Follow the steps below to run the solution locally: + +## Prerequisites + +Azure Chat is heavily dependant on a large number of Azure services. The easiest way to deploy all of these required services into an Azure subscription is to use the the [Azure Developer CLI](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/overview) as follows: + +1. Install the [Azure Developer CLI](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/overview) +1. From the root of the repository: + + 1. Run `azd init` + 1. Run `azd provision` to provision the Azure resources + + +## Identity Provider + +For local development you can use the `Basic Auth (DEV ONLY)` provider to sign in - this local development identity provider will accept any user name and password, and the username you enter will create a new user id (hash of username@localhost) so you can simulate multiple users. If you prefer to use an Identity Provider (Entra ID or GitHub) for local development follow the [instructions](./3-add-identity.md) in the next chapter to add one. + +## Run the App + +With the prerequisites complete, follow the steps below to run the solution locally: + +1. Change directory to the `src` folder +2. Rename/copy the file `.env.example` to `.env` and populate the environment variables based on the deployed resources in Azure. + + > **NOTE** + > If you have used the Azure Developer CLI to deploy the Azure services required for the solution (as described above), you can find the values for most the required environment variables in the `.env` file the `.azure\` directory. This generated file will not contain any keys, however it is recommended to use managed identities as described in "Run Locally with Managed Identities" on [this page](./9-managed-identities.md). + +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 your Identity Provider configuration), and you can start chatting. + +## 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..f6e0de719 100644 --- a/docs/4-deploy-to-azure.md +++ b/docs/4-deploy-to-azure.md @@ -1,40 +1,77 @@ -# ☁️ Deploy to Azure - GitHub Actions +# ☁️ Deploy to Azure -The following steps describes how the application can be deployed to Azure App service using GitHub Actions. +You can provision the required Azure resources in two ways: -## 🧬 Fork the repository +## Option 1: Azure Developer CLI (azd) -Fork this repository to your own organisation so that you can execute GitHub Actions against your own Azure Subscription. +To deploy the application to Azure using the Azure Developer CLI, follow the steps below. You can do this without cloning the repository, but instructions are also provided for those who have cloned the repository. -## 🗝️ Configure secrets in your GitHub repository +1. Download the [Azure Developer CLI](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/overview) -### 1. AZURE_CREDENTIALS +2. **If you have not cloned this repo**: -The GitHub workflow requires a secret named `AZURE_CREDENTIALS` to authenticate with Azure. The secret contains the credentials for a service principal with the Contributor role on the resource group containing the container app and container registry. + 1. Run `azd init -t microsoft/azurechat` + 2. Run `azd auth login` to authenticate with Azure + 3. Run `azd up` to provision and deploy the application -1. Create a service principal with the Contributor role on the resource group that contains the Azure App Service. +3. **If you have cloned this repo**: + 1. Run `azd init` from the repo root directory + 2. Run `azd auth login` to authenticate with Azure + 3. Run `azd up` to provision and deploy the application - ```console - az ad sp create-for-rbac - --name --role contributor --scopes /subscriptions//resourceGroups/ --sdk-auth --output json - ``` +In both cases you will be prompted for some configuration values. These are described below: + +| Prompt - azd init | Description | +|--------|-------------| +| Enter a new environment name                                            | The name of the azd environment. The application will be deployed into a resource group called `rg-`, and this value is also used in the name of all the created Azure resources.| + +| Prompt - azd up | Description | +|--------|-------------| +| Select an Azure Subscription to use| Select the Azure subscription you want to deploy the application to | +|Select an Azure location to use| Select the Azure region you want to deploy the Azure services to. This location is used by all services except OpenAI deployments - these are set separately (below) | +| Enter a value for the 'dalleLocation' infrastructure parameter| Select the Azure region you want to deploy the DALL-E model to. The number of regions that support DALL-E is currently limited | +| Enter a value for the 'disableLocalAuth' infrastructure parameter | Set to `true` to use Managed Identities for authentication, or `false` to use keys. See [Managed Identities](9-managed-identities.md) for more information. if you are unsure we recommend you select `true` | +| Enter a value for the 'openAILocation' infrastructure parameter: | Select the Azure region you want to deploy the OpenAI service to. | +|Enter a value for the 'usePrivateEndpoints' infrastructure parameter: | Set to `false` to deploy the application without Private Endpoints, or `true` to use Private Endpoints. See [Private Endpoints](10-private-endpoints.md) for more information. If you are unsure we recommend you select `false` | + + +## Option 2: GitHub Actions -2. Copy the JSON output from the command. +The following steps describes how the application can be deployed to Azure App service using GitHub Actions. + +### 🧬 Fork the repository -3. In the GitHub repository, navigate to Settings > Secrets > Actions and select New repository secret. +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. -4. Enter `AZURE_CREDENTIALS` as the name and paste the contents of the JSON output as the value. +### 🗝️ Configure secrets in your GitHub repository -5. Select **Add secret**. +#### 1. AZURE_CREDENTIALS -### 2. AZURE_APP_SERVICE_NAME +The GitHub workflow requires a secret named `AZURE_CREDENTIALS` to authenticate with Azure. The secret contains the credentials for a Service Principal with the Contributor role on the resource group containing the Azure App Service. + +1. Create a Service Principal with the Contributor role on the resource group that contains the Azure App Service. + ```console + az ad sp create-for-rbac + --name --role contributor --scopes /subscriptions//resourceGroups/ --sdk-auth --output json + ``` + **⚠️ Deprecation:** You may be presented with a warning that `--sdk-auth` is deprecated and will be removed in future versions. For now, you can ignore this warning or check out [#359](https://github.com/microsoft/azurechat/issues/359#issuecomment-2650632190) for more details. + + **💡 Good to know:** The Service Principal secret generated in this step has a default lifespan of 1 year. This can be modified by adding the `--years n` flag to the command above, or generating a new secret for the Service Principal through the Azure Portal. In either case, be sure to update the GitHub secret accordingly. +3. Copy the JSON output from the command. +4. In the GitHub repository, navigate to Settings > Secrets > Actions and select **New repository secret**. +5. Enter `AZURE_CREDENTIALS` as the name and paste the contents of the JSON output as the value. +6. Select **Add secret**. + +#### 2. AZURE_APP_SERVICE_NAME Under the same repository secrets add a new variable `AZURE_APP_SERVICE_NAME` to deploy to your Azure Web app. The value of this secret is the name of your Azure Web app e.g. `my-web-app-name` from the domain https://my-web-app-name.azurewebsites.net/ -### 3. Run GitHub Actions +#### 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..e5d1ebd04 --- /dev/null +++ b/docs/9-managed-identities.md @@ -0,0 +1,85 @@ +# 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 as follows: + +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 resources using azd**: + - Refer to the [README](../README.md) + +## Run Locally with Managed Identities + +You can run Azure Chat locally with Managed Identities - in this case the identity of the currently logged in user (via `az login`) is used to authenticate with the required Azure services. Follow the steps below to run Azure Chat locally with Managed Identities: + +1. Refer to the documentation in [Run Locally](2-run-locally.md) to set up your local environment up for development. +1. Update your `.env` file with the following setting: + ``` + USE_MANAGED_IDENTITIES=true + ``` +1. Make sure that your `.env` either has the following settings removed, uncommented, or set to empty. Even though you have set `USE_MANAGED_IDENTITIES=true` the various SDKs that the application uses to interact with these services can still default to key based authentication if these are present: + ``` + AZURE_OPENAI_API_KEY= + AZURE_OPENAI_DALLE_API_KEY= + AZURE_COSMOSDB_KEY= + AZURE_SEARCH_API_KEY= + AZURE_DOCUMENT_INTELLIGENCE_KEY= + ``` +1. Run this script to grant yourself RBAC permissions on the various Azure resources used by Azure Chat. + + If you haven't already done so then you will need to login to Azure using the Azure CLI command `az login` + - In Powershell: + ```powershell + PS> .\scripts\add_localdev_roles.ps1 + ``` + - In Bash: + ```bash + > chmod +x .\scripts\add_localdev_roles.sh + > .\scripts\add_localdev_roles.sh + ``` + + +## 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/docs/images/private-endpoints.png b/docs/images/private-endpoints.png new file mode 100644 index 000000000..58d5e308e Binary files /dev/null and b/docs/images/private-endpoints.png differ diff --git a/infra/main.bicep b/infra/main.bicep index f15740ad5..e9a84bec7 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -1,5 +1,14 @@ targetScope = 'subscription' +// Activates/Deactivates Authentication using keys. If true it will enforce RBAC using managed identities +@allowed([true, false]) +@description('Enables/Disables Authentication using keys. If true it will enforce RBAC using managed identity and disable key auth on backend resouces') +param disableLocalAuth bool + +@allowed([false, true]) +@description('Enables/Disables Private Endpoints for backend Azure resources. If true, it will create a virtual network and subnets to host the private endpoints.') +param usePrivateEndpoints bool + @minLength(1) @maxLength(64) @description('Name of the the environment which is used to generate a short unique hash used in all resources.') @@ -11,7 +20,30 @@ param location string // azure open ai -- regions currently support gpt-4o global-standard @description('Location for the OpenAI resource group') -@allowed(['australiaeast', 'brazilsouth', 'canadaeast', 'eastus', 'eastus2', 'francecentral', 'germanywestcentral', 'japaneast', 'koreacentral', 'northcentralus', 'norwayeast', 'polandcentral', 'spaincentral', 'southafricanorth', 'southcentralus', 'southindia', 'swedencentral', 'switzerlandnorth', 'uksouth', 'westeurope', 'westus', 'westus3']) +@allowed([ + 'australiaeast' + 'brazilsouth' + 'canadaeast' + 'eastus' + 'eastus2' + 'francecentral' + 'germanywestcentral' + 'japaneast' + 'koreacentral' + 'northcentralus' + 'norwayeast' + 'polandcentral' + 'spaincentral' + 'southafricanorth' + 'southcentralus' + 'southindia' + 'swedencentral' + 'switzerlandnorth' + 'uksouth' + 'westeurope' + 'westus' + 'westus3' +]) @metadata({ azd: { type: 'location' @@ -19,8 +51,18 @@ param location string }) param openAILocation string +// DALL-E v3 only supported in limited regions for now +@description('Location for the OpenAI DALL-E 3 instance resource group') +@allowed(['swedencentral', 'eastus', 'australiaeast']) +@metadata({ + azd: { + type: 'location' + } +}) +param dalleLocation string + param openAISku string = 'S0' -param openAIApiVersion string ='2024-08-01-preview' +param openAIApiVersion string = '2024-08-01-preview' param chatGptDeploymentCapacity int = 30 param chatGptDeploymentName string = 'gpt-4o' @@ -30,11 +72,6 @@ param embeddingDeploymentName string = 'embedding' param embeddingDeploymentCapacity int = 120 param embeddingModelName string = 'text-embedding-ada-002' -// DALL-E v3 only supported in limited regions for now -@description('Location for the OpenAI DALL-E 3 instance resource group') -@allowed(['swedencentral', 'eastus', 'australiaeast']) -param dalleLocation string - param dalleDeploymentCapacity int = 1 param dalleDeploymentName string = 'dall-e-3' param dalleModelName string = 'dall-e-3' @@ -45,11 +82,15 @@ param searchServiceIndexName string = 'azure-chat' param searchServiceSkuName string = 'standard' // TODO: define good default Sku and settings for storage account -param storageServiceSku object = { name: 'Standard_LRS' } +param storageServiceSku object = { name: 'Standard_LRS' } param storageServiceImageContainerName string = 'images' param resourceGroupName string = '' +param privateEndpointVNetPrefix string = '192.168.0.0/16' +param privateEndpointSubnetAddressPrefix string = '192.168.0.0/24' +param appServiceBackendSubnetAddressPrefix string = '192.168.1.0/24' + var resourceToken = toLower(uniqueString(subscription().id, name, location)) var tags = { 'azd-env-name': name } @@ -88,9 +129,41 @@ module resources 'resources.bicep' = { storageServiceSku: storageServiceSku storageServiceImageContainerName: storageServiceImageContainerName location: location + disableLocalAuth: disableLocalAuth + usePrivateEndpoints: usePrivateEndpoints + privateEndpointVNetPrefix: privateEndpointVNetPrefix + privateEndpointSubnetAddressPrefix: privateEndpointSubnetAddressPrefix + appServiceBackendSubnetAddressPrefix: appServiceBackendSubnetAddressPrefix } } 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/private_endpoints_core.bicep b/infra/private_endpoints_core.bicep new file mode 100644 index 000000000..629c507cc --- /dev/null +++ b/infra/private_endpoints_core.bicep @@ -0,0 +1,158 @@ +@minLength(1) +@description('Primary location for all resources') +param location string + +param name string +param resourceToken string + +param cosmos_id string +param openai_id string +param openai_dalle_id string +param form_recognizer_id string +// param speech_service_id string +param search_service_id string +param storage_id string +param keyVault_id string + +param tags object + +param privateEndpointVNetPrefix string = '192.168.0.0/16' +param privateEndpointSubnetAddressPrefix string = '192.168.0.0/24' +param appServiceBackendSubnetAddressPrefix string = '192.168.1.0/24' + +var subnetNamePrivateEndpoints = 'privateEndpoints' +var subnetNameAppServiceBackend = 'appServiceBackend' + +var virtualNetworkName = toLower('${name}-vnet-${resourceToken}') + +var privateEndpointSpecs = [ + { + serviceId: cosmos_id + dnsZoneName: 'privatelink.documents.azure.com' + groupId: 'Sql' + } + { + serviceId: openai_id + dnsZoneName: 'privatelink.openai.azure.com' + groupId: 'account' + } + { + serviceId: storage_id + dnsZoneName: 'privatelink.blob.core.windows.net' + groupId: 'blob' + } + { + serviceId: search_service_id + dnsZoneName: 'privatelink.search.windows.net' + groupId: 'searchService' + } + { + serviceId: keyVault_id + dnsZoneName: 'privatelink.vaultcore.azure.net' + groupId: 'vault' + } + { + serviceId: form_recognizer_id + dnsZoneName: 'privatelink.cognitiveservices.azure.com' + groupId: 'account' + } + // speech service is called from the browser so no private endpoint + // { + // serviceId: speech_service_id + // dnsZoneName: 'privatelink.cognitiveservices.azure.com' + // groupId: 'account' + // } +] + +// specified separately so that we can ensure the private DNS zones are created before these private endpoints +var privateEndpointSpecs_noDNSZone = [ + { + serviceId: openai_dalle_id + dnsZoneName: 'privatelink.openai.azure.com' + groupId: 'account' + } +] + +resource networkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2021-08-01' = { + name: toLower('${name}-nsg-${resourceToken}') + location: location + tags: tags +} + +resource virtualNetwork 'Microsoft.Network/VirtualNetworks@2021-08-01' = { + name: virtualNetworkName + location: location + tags: tags + properties: { + addressSpace: { + addressPrefixes: [ + privateEndpointVNetPrefix + ] + } + } +} + +resource subnet_privateEndpoint 'Microsoft.Network/virtualNetworks/subnets@2024-05-01' = { + parent: virtualNetwork + name: subnetNamePrivateEndpoints + properties: { + addressPrefix: privateEndpointSubnetAddressPrefix + privateEndpointNetworkPolicies: 'Disabled' + networkSecurityGroup: { + id: networkSecurityGroup.id + } + } +} + +resource subnet_appServiceBackend 'Microsoft.Network/virtualNetworks/subnets@2024-05-01' = { + parent: virtualNetwork + name: subnetNameAppServiceBackend + properties: { + addressPrefix: appServiceBackendSubnetAddressPrefix + delegations: [ + { + name: 'delegation' + properties: { + serviceName: 'Microsoft.Web/serverFarms' + } + } + ] + networkSecurityGroup: { + id: networkSecurityGroup.id + } + } +} + +module privateEndpoints 'private_endpoints_services.bicep' = [ + for (privateEndpointSpec, i) in privateEndpointSpecs: { + name: 'private-endpoint-${i}' + params: { + serviceId: privateEndpointSpec.serviceId + dnsZoneName: privateEndpointSpec.dnsZoneName + createDnsZone: true + virtualNetworkId: virtualNetwork.id + privateEndpointSubnetId: subnet_privateEndpoint.id + groupId: privateEndpointSpec.groupId + } + } +] + +// created after the previous private endpoints to ensure the private DNS zones are created first +module privateEndpoints_noDNSZone 'private_endpoints_services.bicep' = [ + for (privateEndpointSpec, i) in privateEndpointSpecs_noDNSZone: { + name: 'private-endpoint-noDns-${i}' + dependsOn: [ + privateEndpoints + ] + params: { + serviceId: privateEndpointSpec.serviceId + dnsZoneName: privateEndpointSpec.dnsZoneName + createDnsZone: false + virtualNetworkId: virtualNetwork.id + privateEndpointSubnetId: subnet_privateEndpoint.id + groupId: privateEndpointSpec.groupId + } + } +] + +output appServiceSubnetId string = subnet_appServiceBackend.id diff --git a/infra/private_endpoints_services.bicep b/infra/private_endpoints_services.bicep new file mode 100644 index 000000000..6e2b734c2 --- /dev/null +++ b/infra/private_endpoints_services.bicep @@ -0,0 +1,65 @@ +param serviceId string + +param dnsZoneName string +param createDnsZone bool = true +param virtualNetworkId string +param privateEndpointSubnetId string +param groupId string + +var idElements = split(serviceId, '/') +var idElementsLength = length(idElements) + +var serviceName = idElementsLength == 1 ? serviceId : idElements[idElementsLength-1] + +resource privateEndpoint 'Microsoft.Network/privateEndpoints@2021-08-01' = { + name: toLower('${serviceName}-pe') + location: resourceGroup().location + properties: { + subnet: { + id: privateEndpointSubnetId + } + privateLinkServiceConnections: [ + { + name: toLower('${serviceName}-pe-connections') + properties: { + privateLinkServiceId: serviceId + groupIds: [ + groupId + ] + } + } + ] + } +} + +resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (createDnsZone) { + name: dnsZoneName + location: 'global' +} + +resource privateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = if (createDnsZone){ + parent: privateDnsZone + name: '${privateDnsZone.name}-link' + location: 'global' + properties: { + registrationEnabled: false + virtualNetwork: { + id: virtualNetworkId + } + } +} + +resource privateEndpointsDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-05-01' = { + name: '${serviceName}-dns-zone-group' + parent: privateEndpoint + properties: { + privateDnsZoneConfigs: [ + { + name: dnsZoneName + properties: { + privateDnsZoneId: resourceId('Microsoft.Network/privateDnsZones', dnsZoneName) + } + } + ] + } +} diff --git a/infra/resources.bicep b/infra/resources.bicep index 48be71c66..4b796a526 100644 --- a/infra/resources.bicep +++ b/infra/resources.bicep @@ -4,14 +4,14 @@ param resourceToken string param openai_api_version string param openAiLocation string -param openAiSkuName string -param chatGptDeploymentCapacity int +param openAiSkuName string +param chatGptDeploymentCapacity int param chatGptDeploymentName string -param chatGptModelName string +param chatGptModelName string param chatGptModelVersion string -param embeddingDeploymentName string +param embeddingDeploymentName string param embeddingDeploymentCapacity int -param embeddingModelName string +param embeddingModelName string param dalleLocation string param dalleDeploymentCapacity int @@ -31,11 +31,18 @@ param storageServiceImageContainerName string param location string = resourceGroup().location +param disableLocalAuth bool = true +param usePrivateEndpoints bool = true + @secure() param nextAuthHash string = uniqueString(newGuid()) param tags object = {} +param privateEndpointVNetPrefix string = '192.168.0.0/16' +param privateEndpointSubnetAddressPrefix string = '192.168.0.0/24' +param appServiceBackendSubnetAddressPrefix string = '192.168.1.0/24' + var openai_name = toLower('${name}-aillm-${resourceToken}') var openai_dalle_name = toLower('${name}-aidalle-${resourceToken}') @@ -49,13 +56,16 @@ var appservice_name = toLower('${name}-app-${resourceToken}') var clean_name = replace(replace(name, '-', ''), '_', '') var storage_prefix = take(clean_name, 8) var storage_name = toLower('${storage_prefix}sto${resourceToken}') -// keyvault name must be less than 24 chars - token is 13 -var kv_prefix = take(name, 7) -var keyVaultName = toLower('${kv_prefix}-kv-${resourceToken}') +// keyvault name must be less than 24 chars - token is 13, 'kv' is 2 +var kv_prefix = take(clean_name, 7) +var keyVaultName = toLower('${kv_prefix}kv${resourceToken}') var la_workspace_name = toLower('${name}-la-${resourceToken}') var diagnostic_setting_name = 'AppServiceConsoleLogs' -var keyVaultSecretsOfficerRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7') +var keyVaultSecretsOfficerRole = subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7' +) var validStorageServiceImageContainerName = toLower(replace(storageServiceImageContainerName, '-', '')) @@ -87,6 +97,26 @@ var llmDeployments = [ } ] +module privateEndpoints 'private_endpoints_core.bicep' = if (usePrivateEndpoints) { + name: 'private-endpoints' + params: { + location: location + name: name + resourceToken: resourceToken + tags: tags + cosmos_id: cosmosDbAccount.id + openai_id: azureopenai.id + openai_dalle_id: azureopenaidalle.id + form_recognizer_id: formRecognizer.id + storage_id: storage.id + keyVault_id: kv.id + search_service_id: searchService.id + privateEndpointVNetPrefix: privateEndpointVNetPrefix + privateEndpointSubnetAddressPrefix: privateEndpointSubnetAddressPrefix + appServiceBackendSubnetAddressPrefix: appServiceBackendSubnetAddressPrefix + } +} + resource appServicePlan 'Microsoft.Web/serverfarms@2020-06-01' = { name: appservice_name location: location @@ -104,120 +134,134 @@ resource appServicePlan 'Microsoft.Web/serverfarms@2020-06-01' = { kind: 'linux' } -resource webApp 'Microsoft.Web/sites@2020-06-01' = { +var appSettingsCommon = [ + { + name: 'USE_MANAGED_IDENTITIES' + value: disableLocalAuth + } + + { + name: 'AZURE_KEY_VAULT_NAME' + value: keyVaultName + } + { + name: 'SCM_DO_BUILD_DURING_DEPLOYMENT' + value: 'true' + } + { + name: 'AZURE_OPENAI_API_INSTANCE_NAME' + value: openai_name + } + { + name: 'AZURE_OPENAI_API_DEPLOYMENT_NAME' + value: chatGptDeploymentName + } + { + name: 'AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME' + value: embeddingDeploymentName + } + { + name: 'AZURE_OPENAI_API_VERSION' + value: openai_api_version + } + { + name: 'AZURE_OPENAI_DALLE_API_INSTANCE_NAME' + value: openai_dalle_name + } + { + name: 'AZURE_OPENAI_DALLE_API_DEPLOYMENT_NAME' + value: dalleDeploymentName + } + { + name: 'AZURE_OPENAI_DALLE_API_VERSION' + value: dalleApiVersion + } + { + name: 'NEXTAUTH_SECRET' + value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::NEXTAUTH_SECRET.name})' + } + { + name: 'NEXTAUTH_URL' + value: 'https://${webapp_name}.azurewebsites.net' + } + { + name: 'AZURE_COSMOSDB_URI' + value: cosmosDbAccount.properties.documentEndpoint + } + { + name: 'AZURE_SEARCH_NAME' + value: search_name + } + { + name: 'AZURE_SEARCH_INDEX_NAME' + value: searchServiceIndexName + } + { + name: 'AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT' + value: 'https://${form_recognizer_name}.cognitiveservices.azure.com/' + } + { + name: 'AZURE_SPEECH_REGION' + value: location + } + { + name: 'AZURE_SPEECH_KEY' + value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_SPEECH_KEY.name})' + } + { + name: 'AZURE_STORAGE_ACCOUNT_NAME' + value: storage_name + } +] + +var appSettingsWithLocalAuth = disableLocalAuth + ? [] + : [ + { + name: 'AZURE_OPENAI_API_KEY' + value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_OPENAI_API_KEY.name})' + } + { + name: 'AZURE_OPENAI_DALLE_API_KEY' + value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_OPENAI_DALLE_API_KEY.name})' + } + { + name: 'AZURE_COSMOSDB_KEY' + value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_COSMOSDB_KEY.name})' + } + { + name: 'AZURE_SEARCH_API_KEY' + value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_SEARCH_API_KEY.name})' + } + { + name: 'AZURE_DOCUMENT_INTELLIGENCE_KEY' + value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_DOCUMENT_INTELLIGENCE_KEY.name})' + } + { + name: 'AZURE_STORAGE_ACCOUNT_KEY' + value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_STORAGE_ACCOUNT_KEY.name})' + } + ] + +resource webApp 'Microsoft.Web/sites@2024-04-01' = { name: webapp_name location: location tags: union(tags, { 'azd-service-name': 'frontend' }) properties: { serverFarmId: appServicePlan.id httpsOnly: true + virtualNetworkSubnetId: usePrivateEndpoints ? privateEndpoints.outputs.appServiceSubnetId : null + vnetRouteAllEnabled: usePrivateEndpoints ? false : null siteConfig: { - linuxFxVersion: 'node|18-lts' + linuxFxVersion: 'NODE|22-lts' alwaysOn: true appCommandLine: 'next start' ftpsState: 'Disabled' minTlsVersion: '1.2' - appSettings: [ - { - name: 'AZURE_KEY_VAULT_NAME' - value: keyVaultName - } - { - name: 'SCM_DO_BUILD_DURING_DEPLOYMENT' - value: 'true' - } - { - name: 'AZURE_OPENAI_API_KEY' - value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_OPENAI_API_KEY.name})' - } - { - name: 'AZURE_OPENAI_API_INSTANCE_NAME' - value: openai_name - } - { - name: 'AZURE_OPENAI_API_DEPLOYMENT_NAME' - value: chatGptDeploymentName - } - { - name: 'AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME' - value: embeddingDeploymentName - } - { - name: 'AZURE_OPENAI_API_VERSION' - value: openai_api_version - } - { - name: 'AZURE_OPENAI_DALLE_API_KEY' - value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_OPENAI_DALLE_API_KEY.name})' - } - { - name: 'AZURE_OPENAI_DALLE_API_INSTANCE_NAME' - value: openai_dalle_name - } - { - name: 'AZURE_OPENAI_DALLE_API_DEPLOYMENT_NAME' - value: dalleDeploymentName - } - { - name: 'AZURE_OPENAI_DALLE_API_VERSION' - value: dalleApiVersion - } - { - name: 'NEXTAUTH_SECRET' - value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::NEXTAUTH_SECRET.name})' - } - { - name: 'NEXTAUTH_URL' - value: 'https://${webapp_name}.azurewebsites.net' - } - { - name: 'AZURE_COSMOSDB_URI' - value: cosmosDbAccount.properties.documentEndpoint - } - { - name: 'AZURE_COSMOSDB_KEY' - value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_COSMOSDB_KEY.name})' - } - { - name: 'AZURE_SEARCH_API_KEY' - value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_SEARCH_API_KEY.name})' - } - { - name: 'AZURE_SEARCH_NAME' - value: search_name - } - { - name: 'AZURE_SEARCH_INDEX_NAME' - value: searchServiceIndexName - } - { - name: 'AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT' - value: 'https://${form_recognizer_name}.cognitiveservices.azure.com/' - } - { - name: 'AZURE_DOCUMENT_INTELLIGENCE_KEY' - value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_DOCUMENT_INTELLIGENCE_KEY.name})' - } - { - name: 'AZURE_SPEECH_REGION' - value: location - } - { - name: 'AZURE_SPEECH_KEY' - value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_SPEECH_KEY.name})' - } - { - name: 'AZURE_STORAGE_ACCOUNT_NAME' - value: storage_name - } - { - name: 'AZURE_STORAGE_ACCOUNT_KEY' - value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_STORAGE_ACCOUNT_KEY.name})' - } - ] + appSettings: concat(appSettingsCommon, appSettingsWithLocalAuth) } } - identity: { type: 'SystemAssigned'} + identity: { type: 'SystemAssigned' } resource configLogs 'config' = { name: 'logs' @@ -254,13 +298,13 @@ 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 } } -resource kv 'Microsoft.KeyVault/vaults@2021-06-01-preview' = { +resource kv 'Microsoft.KeyVault/vaults@2024-12-01-preview' = { name: keyVaultName location: location properties: { @@ -273,9 +317,13 @@ resource kv 'Microsoft.KeyVault/vaults@2021-06-01-preview' = { enabledForDeployment: false enabledForDiskEncryption: true enabledForTemplateDeployment: false + enableSoftDelete: true + softDeleteRetentionInDays: 90 + enablePurgeProtection: true + publicNetworkAccess: usePrivateEndpoints ? 'Disabled' : 'Enabled' } - resource AZURE_OPENAI_API_KEY 'secrets' = { + resource AZURE_OPENAI_API_KEY 'secrets' = if (!disableLocalAuth) { name: 'AZURE-OPENAI-API-KEY' properties: { contentType: 'text/plain' @@ -283,7 +331,7 @@ resource kv 'Microsoft.KeyVault/vaults@2021-06-01-preview' = { } } - resource AZURE_OPENAI_DALLE_API_KEY 'secrets' = { + resource AZURE_OPENAI_DALLE_API_KEY 'secrets' = if (!disableLocalAuth) { name: 'AZURE-OPENAI-DALLE-API-KEY' properties: { contentType: 'text/plain' @@ -299,7 +347,7 @@ resource kv 'Microsoft.KeyVault/vaults@2021-06-01-preview' = { } } - resource AZURE_COSMOSDB_KEY 'secrets' = { + resource AZURE_COSMOSDB_KEY 'secrets' = if (!disableLocalAuth) { name: 'AZURE-COSMOSDB-KEY' properties: { contentType: 'text/plain' @@ -307,7 +355,7 @@ resource kv 'Microsoft.KeyVault/vaults@2021-06-01-preview' = { } } - resource AZURE_DOCUMENT_INTELLIGENCE_KEY 'secrets' = { + resource AZURE_DOCUMENT_INTELLIGENCE_KEY 'secrets' = if (!disableLocalAuth) { name: 'AZURE-DOCUMENT-INTELLIGENCE-KEY' properties: { contentType: 'text/plain' @@ -323,7 +371,7 @@ resource kv 'Microsoft.KeyVault/vaults@2021-06-01-preview' = { } } - resource AZURE_SEARCH_API_KEY 'secrets' = { + resource AZURE_SEARCH_API_KEY 'secrets' = if (!disableLocalAuth) { name: 'AZURE-SEARCH-API-KEY' properties: { contentType: 'text/plain' @@ -331,7 +379,7 @@ resource kv 'Microsoft.KeyVault/vaults@2021-06-01-preview' = { } } - resource AZURE_STORAGE_ACCOUNT_KEY 'secrets' = { + resource AZURE_STORAGE_ACCOUNT_KEY 'secrets' = if (!disableLocalAuth) { name: 'AZURE-STORAGE-ACCOUNT-KEY' properties: { contentType: 'text/plain' @@ -347,6 +395,8 @@ resource cosmosDbAccount 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' = { kind: 'GlobalDocumentDB' properties: { databaseAccountOfferType: 'Standard' + disableLocalAuth: disableLocalAuth + publicNetworkAccess: usePrivateEndpoints ? 'Disabled' : 'Enabled' locations: [ { locationName: location @@ -406,7 +456,8 @@ resource formRecognizer 'Microsoft.CognitiveServices/accounts@2023-05-01' = { kind: 'FormRecognizer' properties: { customSubDomainName: form_recognizer_name - publicNetworkAccess: 'Enabled' + publicNetworkAccess: usePrivateEndpoints ? 'Disabled' : 'Enabled' + disableLocalAuth: disableLocalAuth } sku: { name: formRecognizerSkuName @@ -419,8 +470,9 @@ resource searchService 'Microsoft.Search/searchServices@2022-09-01' = { tags: tags properties: { partitionCount: 1 - publicNetworkAccess: 'enabled' + publicNetworkAccess: usePrivateEndpoints ? 'disabled' : 'enabled' replicaCount: 1 + disableLocalAuth: disableLocalAuth } sku: { name: searchServiceSkuName @@ -434,7 +486,8 @@ resource azureopenai 'Microsoft.CognitiveServices/accounts@2023-05-01' = { kind: 'OpenAI' properties: { customSubDomainName: openai_name - publicNetworkAccess: 'Enabled' + publicNetworkAccess: usePrivateEndpoints ? 'Disabled' : 'Enabled' + disableLocalAuth: disableLocalAuth } sku: { name: openAiSkuName @@ -442,18 +495,22 @@ resource azureopenai 'Microsoft.CognitiveServices/accounts@2023-05-01' = { } @batchSize(1) -resource llmdeployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = [for deployment in llmDeployments: { - parent: azureopenai - name: deployment.name - properties: { - model: deployment.model - raiPolicyName: contains(deployment, 'raiPolicyName') ? deployment.raiPolicyName : null - } - sku: contains(deployment, 'sku') ? deployment.sku : { - name: 'Standard' - capacity: deployment.capacity +resource llmdeployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = [ + for deployment in llmDeployments: { + parent: azureopenai + name: deployment.name + properties: { + model: deployment.model + /*raiPolicyName: contains(deployment, 'raiPolicyName') ? deployment.raiPolicyName : null*/ + } + sku: contains(deployment, 'sku') + ? deployment.sku + : { + name: 'Standard' + capacity: deployment.capacity + } } -}] +] resource azureopenaidalle 'Microsoft.CognitiveServices/accounts@2023-05-01' = { name: openai_dalle_name @@ -462,7 +519,8 @@ resource azureopenaidalle 'Microsoft.CognitiveServices/accounts@2023-05-01' = { kind: 'OpenAI' properties: { customSubDomainName: openai_dalle_name - publicNetworkAccess: 'Enabled' + publicNetworkAccess: usePrivateEndpoints ? 'Disabled' : 'Enabled' + disableLocalAuth: disableLocalAuth } sku: { name: openAiSkuName @@ -483,8 +541,6 @@ resource azureopenaidalle 'Microsoft.CognitiveServices/accounts@2023-05-01' = { } } - - resource speechService 'Microsoft.CognitiveServices/accounts@2023-05-01' = { name: speech_service_name location: location @@ -492,7 +548,9 @@ resource speechService 'Microsoft.CognitiveServices/accounts@2023-05-01' = { kind: 'SpeechServices' properties: { customSubDomainName: speech_service_name + // called from the browser so public endpoint is required publicNetworkAccess: 'Enabled' + /* TODO: disableLocalAuth: disableLocalAuth*/ } sku: { name: speechServiceSkuName @@ -500,12 +558,17 @@ resource speechService 'Microsoft.CognitiveServices/accounts@2023-05-01' = { } // TODO: define good default Sku and settings for storage account -resource storage 'Microsoft.Storage/storageAccounts@2022-05-01' = { +resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = { name: storage_name location: location tags: tags kind: 'StorageV2' sku: storageServiceSku + properties: { + allowSharedKeyAccess: !disableLocalAuth + publicNetworkAccess: usePrivateEndpoints ? 'Disabled' : 'Enabled' + minimumTlsVersion: 'TLS1_2' + } resource blobServices 'blobServices' = { name: 'default' @@ -518,4 +581,148 @@ 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/add_localdev_roles.sh b/scripts/add_localdev_roles.sh new file mode 100755 index 000000000..dbfda5030 --- /dev/null +++ b/scripts/add_localdev_roles.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +echo -e "\nThis script will add the required IAM roles to allow the logged in user to run AzureChat locally." +echo "This will only work if you have used AZD to deploy the app, and have the required permissions to modify IAM." + +echo -e "\nLoading azd .env file from current environment..." +# Read the environment variables from azd and export them +while IFS= read -r line; do + # Only process lines with an equal sign + if [[ "$line" != *"="* ]]; then + continue + fi + + key="${line%%=*}" + value="${line#*=}" + # Remove leading and trailing quotes if they exist + value="${value%\"}" + value="${value#\"}" + export "$key"="$value" +done < <(azd env get-values) + +# Retrieve required environment variables +sub="${AZURE_SUBSCRIPTION_ID}" +rg="${AZURE_RESOURCE_GROUP}" +appName="${AZURE_WEBAPP_NAME}" +cosmosAccName="${AZURE_COSMOSDB_ACCOUNT_NAME}" +aillmName="${AZURE_OPENAI_API_INSTANCE_NAME}" +searchName="${AZURE_SEARCH_NAME}" +storageName="${AZURE_STORAGE_ACCOUNT_NAME}" +dalleName="${AZURE_OPENAI_DALLE_API_INSTANCE_NAME}" +docIntelName="${AZURE_DOCUMENT_INTELLIGENCE_NAME}" + +echo "Resource Group: $rg" +echo "App Host Name: $appName" +echo "CosmosDB Account: $cosmosAccName" +echo "OpenAI LLM Instance: $aillmName" +echo "OpenAI DALL-E Instance: $dalleName" +echo "Storage Account: $storageName" +echo "Document Intelligence: $docIntelName" +echo "Search Service: $searchName" + +# Get currently logged in user details using az and jq +userId=$(az ad signed-in-user show --query "id" -o tsv) +userPrincipalName=$(az ad signed-in-user show --query "userPrincipalName" -o tsv) + +echo -e "\nLogged-in user: $userPrincipalName (ID: $userId)" + +read -p $'\nDoes this look ok? \nEnter "y" to continue, anything else to exit: ' response +if [[ "$response" != "y" ]]; then + exit +fi + +echo -e "\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" + +echo -e "\nAdding 'Cognitive Services OpenAI User' role on $aillmName" +az role assignment create --assignee "$userId" --role "Cognitive Services OpenAI User" --scope "$aillmNameScope" + +echo -e "\nAdding 'Cognitive Services OpenAI User' role on $dalleName" +az role assignment create --assignee "$userId" --role "Cognitive Services OpenAI User" --scope "$dalleScope" + +echo -e "\nAdding 'Storage Blob Data Contributor' role on $storageName" +az role assignment create --assignee "$userId" --role "Storage Blob Data Contributor" --scope "$storageScope" + +echo -e "\nAdding 'Cognitive Services User' role on $docIntelName" +az role assignment create --assignee "$userId" --role "Cognitive Services User" --scope "$docIntelScope" + +echo -e "\nAdding 'Search Service Contributor' role on $searchName" +az role assignment create --assignee "$userId" --role "Search Service Contributor" --scope "$searchScope" + +echo -e "\nAdding 'Search Index Data Contributor' role on $searchName" +az role assignment create --assignee "$userId" --role "Search Index Data Contributor" --scope "$searchScope" + +echo "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..80f8700bf --- /dev/null +++ b/scripts/appreg_setup.ps1 @@ -0,0 +1,59 @@ +# 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) { + $webappname=(azd env get-value AZURE_WEBAPP_NAME).Trim() +} + +if (-not $webappname -or $webappname -like "*ERROR*") { + 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/scripts/appreg_setup.sh b/scripts/appreg_setup.sh new file mode 100755 index 000000000..c0eb05598 --- /dev/null +++ b/scripts/appreg_setup.sh @@ -0,0 +1,88 @@ +#!/bin/bash + +# Parse parameters +showsecret_flag="false" +webappname="" + +while [[ "$#" -gt 0 ]]; do + case $1 in + -w|--webappname) + webappname="$2" + shift + ;; + -showsecret) + showsecret_flag="true" + ;; + -localredirect) + local_redirect_flag="true" + ;; + *) + echo "Unknown parameter passed: $1" + exit 1 + ;; + esac + shift +done + +if [[ -z "$webappname" ]]; then + webappname=$(azd env get-value AZURE_WEBAPP_NAME) +fi + +if [[ -z "$webappname" ]] || [[ $webappname == *"ERROR"* ]]; then + + echo "" + echo "Usage: $0 -w [-showsecret] [-localredirect]" + echo "No arguments provided. Please provide the web app name from the Azure portal (e.g. azurechat-ulg3yy5ybjhdq)." + exit 1 +fi + +tenantid=$(az account show --query tenantId --output tsv | tr -d '[:space:]') + +echo "About to create a new App Registration called ${webappname}-app in Microsoft Entra tenant $tenantid" +echo "NOTE: This will only work if you have the necessary permissions in the tenant." + +read -p "Do you wish to proceed (y/n)? " choice +if [[ "$choice" != "y" && "$choice" != "Y" ]]; then + echo "Exiting." + exit 1 +fi + +clientid=$(az ad app create --display-name "${webappname}-app" --sign-in-audience AzureADMyOrg --query appId --output tsv | tr -d '[:space:]') +echo "> Creating app registration with client id $clientid ..." + +objectid=$(az ad app show --id $clientid --query id --output tsv | tr -d '[:space:]') +echo "Done. Object id is $objectid" + +echo "> 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 | tr -d '[:space:]') +echo "Done." + +redirecturl="https://${webappname}.azurewebsites.net/api/auth/callback/azure-ad" +graphurl="https://graph.microsoft.com/v1.0/applications/${objectid}" + +if [[ "$local_redirect_flag" == "true" ]]; then + echo "> Updating redirect url to $redirecturl and http://localhost:3000/api/auth/callback/azure-ad..." + redirectBody="{'web':{'redirectUris':['${redirecturl}','http://localhost:3000/api/auth/callback/azure-ad']}}" +else + echo "> Updating redirect url to $redirecturl..." + redirectBody="{'web':{'redirectUris':['${redirecturl}']}}" +fi + +az rest --method PATCH --uri $graphurl --body $redirectBody +echo "Done." + +rg=$(az webapp list --query "[?name=='${webappname}'].resourceGroup" --output tsv | tr -d '[:space:]') +echo "> Found the app resource group: $rg" + +echo "> 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 +echo "Done." + +echo "AZURE_AD_CLIENT_ID=$clientid" +echo "AZURE_AD_TENANT_ID=$tenantid" +if [[ "$showsecret_flag" == "true" ]]; then + echo "AZURE_AD_CLIENT_SECRET=$clientsecret" + echo "^^ Ensure you clear your console history to remove this secret" +fi + +echo "> Setup complete." \ 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/auth-page/auth-api.ts b/src/features/auth-page/auth-api.ts index a70aaca47..751bc65dd 100644 --- a/src/features/auth-page/auth-api.ts +++ b/src/features/auth-page/auth-api.ts @@ -4,6 +4,8 @@ import CredentialsProvider from "next-auth/providers/credentials"; import GitHubProvider from "next-auth/providers/github"; import { Provider } from "next-auth/providers/index"; import { hashValue } from "./helpers"; +import { image } from "@markdoc/markdoc/dist/src/schema"; +import { access } from "fs"; const configureIdentityProvider = () => { const providers: Array = []; @@ -18,10 +20,13 @@ const configureIdentityProvider = () => { clientId: process.env.AUTH_GITHUB_ID!, clientSecret: process.env.AUTH_GITHUB_SECRET!, async profile(profile) { + const image = await fetchProfilePicture(profile.avatar_url, null); const newProfile = { ...profile, isAdmin: adminEmails?.includes(profile.email.toLowerCase()), + image: image, }; + console.log("GitHub profile:", newProfile); return newProfile; }, }) @@ -38,15 +43,24 @@ const configureIdentityProvider = () => { clientId: process.env.AZURE_AD_CLIENT_ID!, clientSecret: process.env.AZURE_AD_CLIENT_SECRET!, tenantId: process.env.AZURE_AD_TENANT_ID!, - async profile(profile) { + authorization: { + params: { + scope: "openid profile User.Read", + }, + }, + async profile(profile, tokens) { + const email = profile.email || profile.preferred_username || ""; + const image = await fetchProfilePicture(`https://graph.microsoft.com/v1.0/me/photos/48x48/$value`, tokens.access_token); const newProfile = { ...profile, - // throws error without this - unsure of the root cause (https://stackoverflow.com/questions/76244244/profile-id-is-missing-in-google-oauth-profile-response-nextauth) + email, id: profile.sub, isAdmin: - adminEmails?.includes(profile.email.toLowerCase()) || - adminEmails?.includes(profile.preferred_username.toLowerCase()), + adminEmails?.includes(profile.email?.toLowerCase()) || + adminEmails?.includes(profile.preferred_username?.toLowerCase()), + image: image, }; + console.log("Azure AD profile:", newProfile); return newProfile; }, }) @@ -75,12 +89,13 @@ const configureIdentityProvider = () => { id: hashValue(email), name: username, email: email, - isAdmin: false, + isAdmin: adminEmails?.includes(email), image: "", }; console.log( "=== DEV USER LOGGED IN:\n", - JSON.stringify(user, null, 2) + JSON.stringify(user, null, 2, + ) ); return user; }, @@ -91,6 +106,30 @@ const configureIdentityProvider = () => { return providers; }; +export const fetchProfilePicture = async (profilePictureUrl: string, accessToken: any): Promise => { + console.log("Fetching profile picture..."); + var image = null + const profilePicture = await fetch( + profilePictureUrl, + accessToken && { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + if (profilePicture.ok) { + console.log("Profile picture fetched successfully."); + const pictureBuffer = await profilePicture.arrayBuffer(); + const pictureBase64 = Buffer.from(pictureBuffer).toString("base64"); + image = `data:image/jpeg;base64,${pictureBase64}`; + } + else { + console.error("Failed to fetch profile picture:", profilePictureUrl, profilePicture.statusText); + } + return image; +}; + + export const options: NextAuthOptions = { secret: process.env.NEXTAUTH_SECRET, providers: [...configureIdentityProvider()], 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-lock.json b/src/package-lock.json index 2fdc29900..1817e43ae 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -10,8 +10,9 @@ "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/openai": "^2.0.0-beta.2", "@azure/search-documents": "^12.0.0", "@azure/storage-blob": "^12.17.0", "@codemirror/lang-javascript": "^6.2.1", @@ -41,7 +42,7 @@ "next": "14.0.4", "next-auth": "^4.24.5", "next-themes": "^0.2.1", - "openai": "^4.26.0", + "openai": "^4.67.1", "react": "^18", "react-dom": "^18", "react-syntax-highlighter": "^15.5.0", @@ -84,6 +85,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@azure-rest/core-client": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.3.3.tgz", + "integrity": "sha512-fTj4eanz7+ph0reoS4VaqFXP9PUkiTWOq+RVrgaNiUpHn0p6RgHRM+eSo7EnB89KGmIRg6gUpFjxModmjXVUPg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-rest-pipeline": "^1.5.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure-rest/core-client/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@azure/abort-controller": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", @@ -115,33 +145,59 @@ } }, "node_modules/@azure/core-auth": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.5.0.tgz", - "integrity": "sha512-udzoBuYG1VBoHVohDTrvKjyzel34zt77Bhp7dQntVGGD0ehVq48owENbBG8fIgkHRNUBQH5k1r0hpoMu5L8+kw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz", + "integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==", + "license": "MIT", "dependencies": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-util": "^1.1.0", - "tslib": "^2.2.0" + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.11.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@azure/core-client": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.7.3.tgz", - "integrity": "sha512-kleJ1iUTxcO32Y06dH9Pfi9K4U+Tlb111WXEnbt7R/ne+NLRwppZiTGJuTD5VVoxTMK5NTbEtm5t2vcdNCFe2g==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz", + "integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==", + "license": "MIT", "dependencies": { - "@azure/abort-controller": "^1.0.0", + "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.4.0", "@azure/core-rest-pipeline": "^1.9.1", "@azure/core-tracing": "^1.0.0", - "@azure/core-util": "^1.0.0", + "@azure/core-util": "^1.6.1", "@azure/logger": "^1.0.0", - "tslib": "^2.2.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-client/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@azure/core-http": { @@ -227,33 +283,56 @@ } }, "node_modules/@azure/core-rest-pipeline": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.13.0.tgz", - "integrity": "sha512-a62aP/wppgmnfIkJLfcB4ssPBcH94WzrzPVJ3tlJt050zX4lfmtnvy95D3igDo3f31StO+9BgPrzvkj4aOxnoA==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.19.0.tgz", + "integrity": "sha512-bM3308LRyg5g7r3Twprtqww0R/r7+GyVxj4BafcmVPo4WQoGt5JXuaqxHEFjw2o3rvFZcUPiqJMg6WuvEEeVUA==", + "license": "MIT", "dependencies": { - "@azure/abort-controller": "^1.1.0", - "@azure/core-auth": "^1.4.0", + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.8.0", "@azure/core-tracing": "^1.0.1", - "@azure/core-util": "^1.3.0", + "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "tslib": "^2.2.0" + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, + "node_modules/@azure/core-rest-pipeline/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/@azure/core-rest-pipeline/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", "dependencies": { - "agent-base": "6", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/@azure/core-tracing": { @@ -268,15 +347,28 @@ } }, "node_modules/@azure/core-util": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.6.1.tgz", - "integrity": "sha512-h5taHeySlsV9qxuK64KZxy4iln1BtMYlNt5jbuEFN3UFSAd1EwKg/Gjl5a6tZ/W8t6li3xPnutOx7zbDyXnPmQ==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.11.0.tgz", + "integrity": "sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g==", + "license": "MIT", "dependencies": { - "@azure/abort-controller": "^1.0.0", - "tslib": "^2.2.0" + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-util/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, "node_modules/@azure/cosmos": { @@ -311,19 +403,20 @@ } }, "node_modules/@azure/identity": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.0.0.tgz", - "integrity": "sha512-gtPYxIL0kI39Dw4t3HvlbfhOdXqKD2MqDgynlklF0j728j51dcKgRo6FLX0QzpBw/1gGfLxjMXqq3nKOSQ2lmA==", - "dependencies": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.5.0", - "@azure/core-client": "^1.4.0", - "@azure/core-rest-pipeline": "^1.1.0", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.6.0.tgz", + "integrity": "sha512-ANpO1iAvcZmpD4QY7/kaE/P2n66pRXsDp3nMUC6Ow3c9KfXOZF7qMU9VgqPw8m7adP7TVIbVyrCEmD9cth3KQQ==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", - "@azure/core-util": "^1.0.0", + "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", - "@azure/msal-browser": "^3.5.0", - "@azure/msal-node": "^2.5.1", + "@azure/msal-browser": "^4.0.1", + "@azure/msal-node": "^2.15.0", "events": "^3.0.0", "jws": "^4.0.0", "open": "^8.0.0", @@ -334,6 +427,18 @@ "node": ">=18.0.0" } }, + "node_modules/@azure/identity/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@azure/keyvault-secrets": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@azure/keyvault-secrets/-/keyvault-secrets-4.7.0.tgz", @@ -380,30 +485,33 @@ } }, "node_modules/@azure/msal-browser": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.7.0.tgz", - "integrity": "sha512-ktDB/Gf7UDgYBJOnoIlh70lxIo4e1/D2UgHuayB4RntN1IlusfTtIVH3k8NpJMdl+38tfTXIaUoR+qlr5voZEg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.2.0.tgz", + "integrity": "sha512-MXQjgAgjg/2VRKV+UPWHESoZPcue2ZvWKfpBLCyTUyixP+mhCl0q5D1+xDiwBGV3lru2poKZVZDQAOE40wKmWg==", + "license": "MIT", "dependencies": { - "@azure/msal-common": "14.6.0" + "@azure/msal-common": "15.1.1" }, "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-common": { - "version": "14.6.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.6.0.tgz", - "integrity": "sha512-AGusT/JvxdzJIYi5u0n97cmhd3pUT6UuI6rEkT5iDeT2FGcV0/EB8pk+dy6GLPpYg9vhDCuyoYrEZGd+2UeCCQ==", + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.1.1.tgz", + "integrity": "sha512-bvLWYq9fleAcTJ6H+hfkG91On6vI/UhGyOB7Z6r0Bsa+KTL3zPtigmGCOJgdxrEklOYD88X9SehexLDH/5NRKQ==", + "license": "MIT", "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-node": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.6.1.tgz", - "integrity": "sha512-wYwz83pWatTNWUCkTi3cAOXbchad5FnZz/pbZz7b8Z6FuEqohXcTtg6BLip9SmcjN6FlbwUdJIZYOof2v1Gnrg==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.16.2.tgz", + "integrity": "sha512-An7l1hEr0w1HMMh1LU+rtDtqL7/jw74ORlc9Wnh06v7TU/xpG39/Zdr1ZJu3QpjUfKJ+E0/OXMW8DRSWTlh7qQ==", + "license": "MIT", "dependencies": { - "@azure/msal-common": "14.6.0", + "@azure/msal-common": "14.16.0", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" }, @@ -411,14 +519,37 @@ "node": ">=16" } }, + "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { + "version": "14.16.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.0.tgz", + "integrity": "sha512-1KOZj9IpcDSwpNiQNjt0jDYZpQvNZay7QAEi/5DLubay40iGYtLzya/jbjRPLyOTZhEKyL1MzPuw2HqBCjceYA==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/@azure/msal-node/node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } }, + "node_modules/@azure/openai": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@azure/openai/-/openai-2.0.0.tgz", + "integrity": "sha512-zSNhwarYbqg3P048uKMjEjbge41OnAgmiiE1elCHVsuCCXRyz2BXnHMJkW6WR6ZKQy5NHswJNUNSWsuqancqFA==", + "license": "MIT", + "dependencies": { + "@azure-rest/core-client": "^2.2.0", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@azure/search-documents": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/@azure/search-documents/-/search-documents-12.0.0.tgz", @@ -2428,14 +2559,6 @@ "node": ">=4" } }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "engines": { - "node": ">= 10" - } - }, "node_modules/@types/hast": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.8.tgz", @@ -3067,11 +3190,6 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, - "node_modules/base-64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", - "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" - }, "node_modules/bent": { "version": "7.3.12", "resolved": "https://registry.npmjs.org/bent/-/bent-7.3.12.tgz", @@ -3261,14 +3379,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "engines": { - "node": "*" - } - }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -3441,14 +3551,6 @@ "node": ">= 8" } }, - "node_modules/crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", - "engines": { - "node": "*" - } - }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -3575,15 +3677,6 @@ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" }, - "node_modules/digest-fetch": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz", - "integrity": "sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==", - "dependencies": { - "base-64": "^0.1.0", - "md5": "^2.3.0" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -4733,16 +4826,25 @@ } }, "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">= 6" + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" } }, "node_modules/https-proxy-agent": { @@ -4938,11 +5040,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -5347,6 +5444,7 @@ "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", @@ -5368,6 +5466,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", "dependencies": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", @@ -5378,6 +5477,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" @@ -5493,22 +5593,26 @@ "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" }, "node_modules/lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" }, "node_modules/lodash.isnumber": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", @@ -5518,7 +5622,8 @@ "node_modules/lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -5528,7 +5633,8 @@ "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" }, "node_modules/loose-envify": { "version": "1.4.0", @@ -5573,16 +5679,6 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, - "node_modules/md5": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", - "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "dependencies": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5616,6 +5712,27 @@ "ws": "^7.5.6" } }, + "node_modules/microsoft-cognitiveservices-speech-sdk/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -6056,22 +6173,33 @@ } }, "node_modules/openai": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.26.0.tgz", - "integrity": "sha512-HPC7tgYdeP38F3uHA5WgnoXZyGbAp9jgcIo23p6It+q/07u4C+NZ8xHKlMShsPbDDmFRpPsa3vdbXYpbhJH3eg==", + "version": "4.83.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.83.0.tgz", + "integrity": "sha512-fmTsqud0uTtRKsPC7L8Lu55dkaTwYucqncDHzVvO64DKOpNTuiYwjbR/nVgpapXuYy8xSnhQQPUm+3jQaxICgw==", + "license": "Apache-2.0", "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", - "digest-fetch": "^1.3.0", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7", - "web-streams-polyfill": "^3.2.1" + "node-fetch": "^2.6.7" }, "bin": { "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } } }, "node_modules/openai/node_modules/@types/node": { @@ -7361,9 +7489,10 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/tunnel": { "version": "0.0.6", @@ -7647,14 +7776,6 @@ "node": ">=10.13.0" } }, - "node_modules/web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", - "engines": { - "node": ">= 8" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -7766,15 +7887,18 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "optional": true, + "peer": true, "engines": { - "node": ">=8.3.0" + "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -7839,9 +7963,10 @@ } }, "node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } 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",