├── .azdo └── pipelines │ └── azure-dev.yml ├── .devcontainer ├── devcontainer.json └── postCreate.sh ├── .gitattributes ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ └── azure-dev.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── azure.yaml ├── debug.txt ├── docs ├── images │ ├── arch.png │ ├── chatapp.png │ ├── config_chatapp.png │ ├── dashboard.png │ └── deploy_chatapp.png └── raw │ └── arch.drawio ├── infra ├── abbreviations.json ├── azurechat.bicep ├── main.bicep ├── main.parameters.json ├── modules │ ├── ai │ │ ├── cognitiveservices.bicep │ │ └── rai_policies │ │ │ ├── default-with-jailbreak-detection.json │ │ │ └── low-filtering-policy.json │ ├── apim │ │ ├── .DS_Store │ │ ├── apim-backend.bicep │ │ ├── apim-redis-cache.bicep │ │ ├── apim.bicep │ │ └── policies │ │ │ ├── api_policy_chargeback.xml │ │ │ └── api_policy_openai.xml │ ├── appconfig │ │ ├── appconfig-chatapp.bicep │ │ ├── appconfig-proxy.bicep │ │ ├── configurationStore.bicep │ │ └── configurationStoreKeyValues.bicep │ ├── appservice │ │ └── azurechat.bicep │ ├── cache │ │ └── redis.bicep │ ├── cosmosdb │ │ └── account.bicep │ ├── host │ │ ├── container-app-environment.bicep │ │ ├── container-app.bicep │ │ └── container-registry.bicep │ ├── keyvault │ │ └── keyvault.bicep │ ├── monitor │ │ ├── applicationinsights.bicep │ │ ├── dashboard.bicep │ │ ├── datacollectionendpoint.bicep │ │ ├── datacollectionrule.bicep │ │ ├── eventhub.bicep │ │ ├── loganalytics.bicep │ │ ├── loganatylicscustomtable.bicep │ │ └── monitoring.bicep │ ├── networking │ │ ├── dns.bicep │ │ ├── dnsentry.bicep │ │ ├── dnsvirtualnetworklink.bicep │ │ ├── private-endpoint.bicep │ │ ├── publicip.bicep │ │ └── vnet.bicep │ ├── roleassignments │ │ ├── roleassignment.bicep │ │ ├── roleassignmentARM.json │ │ └── roles.json │ └── security │ │ ├── assignment.bicep │ │ └── managed-identity.bicep ├── proxy.bicep └── proxy.parameters.json ├── package-lock.json ├── scripts ├── appreg.ps1 ├── appreg.sh ├── cleanup.ps1 ├── cleanup.sh ├── deploy-azurechat.ps1 ├── deploy-azurechat.sh ├── set-az-currentsubscription.ps1 ├── set-az-currentsubscription.sh ├── set-env.ps1 └── set-env.sh ├── src ├── .dockerignore ├── azurechat │ ├── .env.example │ ├── .eslintrc.json │ ├── app-global.ts │ ├── app │ │ ├── api │ │ │ ├── auth │ │ │ │ └── [...nextauth] │ │ │ │ │ └── route.ts │ │ │ └── chat │ │ │ │ └── route.ts │ │ ├── change-log │ │ │ ├── layout.tsx │ │ │ ├── loading.tsx │ │ │ ├── page.tsx │ │ │ └── update.md │ │ ├── chat │ │ │ ├── [id] │ │ │ │ ├── loading.tsx │ │ │ │ ├── not-found.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── loading.tsx │ │ ├── page.tsx │ │ ├── reporting │ │ │ ├── [chatid] │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ └── unauthorized │ │ │ ├── layout.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ ├── components.json │ ├── components │ │ ├── chat │ │ │ ├── chat-loading.tsx │ │ │ └── chat-row.tsx │ │ ├── hooks │ │ │ └── use-chat-scroll-anchor.tsx │ │ ├── login │ │ │ └── login.tsx │ │ ├── markdown │ │ │ ├── code-block.tsx │ │ │ ├── config.tsx │ │ │ ├── markdown.tsx │ │ │ └── paragraph.tsx │ │ ├── menu.tsx │ │ ├── theme-provider.tsx │ │ ├── typography.tsx │ │ └── ui │ │ │ ├── avatar.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── select.tsx │ │ │ ├── sheet.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ └── use-toast.ts │ ├── dockerfile │ ├── features │ │ ├── auth │ │ │ ├── auth-api.ts │ │ │ └── helpers.ts │ │ ├── change-log │ │ │ ├── app-version.ts │ │ │ ├── update-indicator.tsx │ │ │ ├── version-action.tsx │ │ │ └── version-display.tsx │ │ ├── chat │ │ │ ├── chat-menu │ │ │ │ ├── chat-menu-container.tsx │ │ │ │ ├── chat-menu.tsx │ │ │ │ ├── menu-items.tsx │ │ │ │ └── new-chat.tsx │ │ │ ├── chat-services │ │ │ │ ├── azure-cog-search │ │ │ │ │ └── azure-cog-vector-store.ts │ │ │ │ ├── chat-api-data.ts │ │ │ │ ├── chat-api-entry.ts │ │ │ │ ├── chat-api-simple.ts │ │ │ │ ├── chat-document-service.ts │ │ │ │ ├── chat-service.ts │ │ │ │ ├── chat-thread-service.ts │ │ │ │ ├── cosmosdb │ │ │ │ │ └── cosmosdb.ts │ │ │ │ ├── models.ts │ │ │ │ ├── text-chunk.ts │ │ │ │ └── utils.ts │ │ │ └── chat-ui │ │ │ │ ├── chat-context.tsx │ │ │ │ ├── chat-empty-state │ │ │ │ ├── chat-department-selector.tsx │ │ │ │ ├── chat-deployment-selector.tsx │ │ │ │ ├── chat-message-empty-state.tsx │ │ │ │ ├── chat-style-selector.tsx │ │ │ │ ├── chat-type-selector.tsx │ │ │ │ └── start-new-chat.tsx │ │ │ │ ├── chat-file │ │ │ │ ├── chat-file-slider.tsx │ │ │ │ ├── chat-file-ui.tsx │ │ │ │ ├── use-file-selection.ts │ │ │ │ └── use-file-state.ts │ │ │ │ ├── chat-header.tsx │ │ │ │ ├── chat-input │ │ │ │ ├── chat-input.tsx │ │ │ │ └── use-chat-input-dynamic-height.tsx │ │ │ │ ├── chat-message-container.tsx │ │ │ │ ├── chat-speech │ │ │ │ ├── microphone.tsx │ │ │ │ ├── record-speech.tsx │ │ │ │ ├── speech-service.ts │ │ │ │ ├── stop-speech.tsx │ │ │ │ ├── use-speech-to-text.ts │ │ │ │ └── use-text-to-speech.ts │ │ │ │ ├── chat-ui.tsx │ │ │ │ └── markdown │ │ │ │ ├── citation-action.tsx │ │ │ │ ├── citation-slider.tsx │ │ │ │ └── citation.tsx │ │ ├── common │ │ │ ├── appconfig.ts │ │ │ ├── cosmos.ts │ │ │ ├── keyvault.ts │ │ │ ├── managedIdentity.ts │ │ │ ├── openai.ts │ │ │ └── util.ts │ │ ├── global-config │ │ │ └── global-client-config-context.tsx │ │ ├── global-message │ │ │ └── global-message-context.tsx │ │ ├── loading-skeleton.tsx │ │ ├── main-menu │ │ │ ├── menu-context.tsx │ │ │ └── menu.tsx │ │ ├── providers.tsx │ │ ├── reporting │ │ │ ├── chat-reporting-ui.tsx │ │ │ ├── reporting-service.ts │ │ │ └── reporting.tsx │ │ ├── theme │ │ │ ├── customise.ts │ │ │ └── theme-toggle.tsx │ │ └── user-profile.tsx │ ├── lib │ │ └── utils.ts │ ├── middleware.ts │ ├── next.config.js │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ └── ai-icon.png │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── type.ts │ └── types │ │ └── next-auth.d.ts └── dotnet │ ├── AzureAI.Proxy.Client │ ├── AzureAI.Proxy.Client.csproj │ ├── Program.cs │ └── appsettings.json │ ├── AzureAI.Proxy │ ├── AzureAI.Proxy.csproj │ ├── Dockerfile │ ├── Models │ │ ├── LogAnalyticsRecord.cs │ │ └── ProxyConfig.cs │ ├── OpenAIHandlers │ │ ├── ChatCompletionChunck.cs │ │ ├── Error.cs │ │ ├── OpenAIAccessToken.cs │ │ ├── Tokens.cs │ │ └── Usage.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── ReverseProxy │ │ ├── OpenAIChargebackTransformProvider.cs │ │ ├── ProxyConfiguration.cs │ │ ├── RetryMiddleware.cs │ │ └── ThrottlingHealthPolicy.cs │ ├── Services │ │ ├── ILogIngestionService.cs │ │ ├── IManagedIdentityService.cs │ │ ├── LogIngestionService.cs │ │ └── ManagedIdentityService.cs │ ├── appsettings.json │ └── proxyconfig.json │ └── AzureAI.sln └── tests.http /.azdo/pipelines/azure-dev.yml: -------------------------------------------------------------------------------- 1 | # Run when commits are pushed to mainline branch (main or master) 2 | # Set this to the mainline branch you are using 3 | trigger: 4 | - main 5 | - master 6 | 7 | # Azure Pipelines workflow to deploy to Azure using azd 8 | # To configure required secrets for connecting to Azure, simply run `azd pipeline config --provider azdo` 9 | 10 | pool: 11 | vmImage: ubuntu-latest 12 | 13 | # Use azd provided container image that has azd, infra, multi-language build tools pre-installed. 14 | container: mcr.microsoft.com/azure-dev-cli-apps:latest 15 | 16 | steps: 17 | - pwsh: | 18 | azd config set auth.useAzCliAuth "true" 19 | displayName: Configure AZD to Use AZ CLI Authentication. 20 | 21 | - task: AzureCLI@2 22 | displayName: Provision Infrastructure 23 | inputs: 24 | azureSubscription: azconnection 25 | scriptType: bash 26 | scriptLocation: inlineScript 27 | inlineScript: | 28 | azd provision --no-prompt 29 | env: 30 | AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) 31 | AZURE_ENV_NAME: $(AZURE_ENV_NAME) 32 | AZURE_LOCATION: $(AZURE_LOCATION) 33 | 34 | - task: AzureCLI@2 35 | displayName: Deploy Application 36 | inputs: 37 | azureSubscription: azconnection 38 | scriptType: bash 39 | scriptLocation: inlineScript 40 | inlineScript: | 41 | azd deploy --no-prompt 42 | env: 43 | AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) 44 | AZURE_ENV_NAME: $(AZURE_ENV_NAME) 45 | AZURE_LOCATION: $(AZURE_LOCATION) 46 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Azure Developer CLI", 3 | // See https://github.com/devcontainers/images/tree/main/src/dotnet for list of supported versions. 4 | "image": "mcr.microsoft.com/devcontainers/dotnet:8.0", 5 | "features": { 6 | // See https://containers.dev/features for list of features 7 | "ghcr.io/devcontainers/features/azure-cli:1": { 8 | "installBicep": true 9 | }, 10 | "ghcr.io/devcontainers/features/docker-in-docker:2": {}, 11 | "ghcr.io/devcontainers/features/dotnet:2": {}, 12 | "ghcr.io/devcontainers/features/github-cli:1": {}, 13 | "ghcr.io/azure/azure-dev/azd:latest": {}, 14 | "ghcr.io/eitsupi/devcontainer-features/jq-likes:2" : { 15 | "yqVersion": "none", 16 | "gojqVersion": "none", 17 | "xqVersion": "none", 18 | "jqVersion": "latest" 19 | }, 20 | "ghcr.io/devcontainers/features/node:1": {} 21 | }, 22 | "customizations": { 23 | "vscode": { 24 | "extensions": [ 25 | "ms-vscode.azurecli", 26 | "ms-azuretools.azure-dev", 27 | "ms-azuretools.vscode-bicep", 28 | "ms-azuretools.vscode-docker", 29 | "ms-dotnettools.csharp", 30 | "humao.rest-client", 31 | "ms-azuretools.vscode-apimanagement", 32 | "ms-azuretools.vscode-azurecontainerapps", 33 | "GitHub.copilot", 34 | "dbaeumer.vscode-eslint" 35 | ] 36 | } 37 | }, 38 | "forwardPorts": [8080], 39 | "postCreateCommand": "sh .devcontainer/postCreate.sh", 40 | "remoteUser": "root", 41 | "hostRequirements": { 42 | "memory": "8gb" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.devcontainer/postCreate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | chmod u+x ./scripts/set-env.sh 3 | chmod u+x ./scripts/appreg.sh 4 | chmod u+x ./scripts/cleanup.sh 5 | chmod u+x ./scripts/deploy-azurechat.sh 6 | chmod u+x ./scripts/set-az-currentsubscription.sh 7 | curl -fsSL https://aka.ms/install-azd.sh | bash -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto eol=lf 3 | *.{cmd,[cC][mM][dD]} text eol=crlf 4 | *.{bat,[bB][aA][tT]} text eol=crlf 5 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @pascalvanderheiden @azureholic @iMicknl -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "nuget" 9 | directory: "/src" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | -------------------------------------------------------------------------------- /.github/workflows/azure-dev.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | push: 4 | # Run when commits are pushed to mainline branch (main or master) 5 | # Set this to the mainline branch you are using 6 | branches: 7 | - main 8 | - master 9 | 10 | # GitHub Actions workflow to deploy to Azure using azd 11 | # To configure required secrets for connecting to Azure, simply run `azd pipeline config` 12 | 13 | # Set up permissions for deploying with secretless Azure federated credentials 14 | # https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux#set-up-azure-login-with-openid-connect-authentication 15 | permissions: 16 | id-token: write 17 | contents: read 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | container: 23 | image: mcr.microsoft.com/azure-dev-cli-apps:latest 24 | env: 25 | AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} 26 | AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} 27 | AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} 28 | AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v4 32 | 33 | - name: Log in with Azure (Federated Credentials) 34 | if: ${{ env.AZURE_CLIENT_ID != '' }} 35 | run: | 36 | azd auth login ` 37 | --client-id "$Env:AZURE_CLIENT_ID" ` 38 | --federated-credential-provider "github" ` 39 | --tenant-id "$Env:AZURE_TENANT_ID" 40 | shell: pwsh 41 | 42 | - name: Log in with Azure (Client Credentials) 43 | if: ${{ env.AZURE_CREDENTIALS != '' }} 44 | run: | 45 | $info = $Env:AZURE_CREDENTIALS | ConvertFrom-Json -AsHashtable; 46 | Write-Host "::add-mask::$($info.clientSecret)" 47 | 48 | azd auth login ` 49 | --client-id "$($info.clientId)" ` 50 | --client-secret "$($info.clientSecret)" ` 51 | --tenant-id "$($info.tenantId)" 52 | shell: pwsh 53 | env: 54 | AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} 55 | 56 | - name: Provision Infrastructure 57 | run: azd provision --no-prompt 58 | env: 59 | AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} 60 | AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} 61 | AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} 62 | 63 | - name: Deploy Application 64 | run: azd deploy --no-prompt 65 | env: 66 | AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} 67 | AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} 68 | AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} 69 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Microsoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | -------------------------------------------------------------------------------- /azure.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json 2 | 3 | # This is an example starter azure.yaml file containing several example services in comments below. 4 | # Make changes as needed to describe your application setup. 5 | # To learn more about the azure.yaml file, visit https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/azd-schema 6 | 7 | # Name of the application. 8 | name: enterprise-azureai 9 | metadata: 10 | template: enterprise-azureai@1.2.0 11 | services: 12 | 13 | proxy: 14 | project: ./src/dotnet/AzureAI.Proxy 15 | host: containerapp 16 | language: dotnet 17 | docker: 18 | path: ./Dockerfile 19 | context: ../ 20 | 21 | azurechat: 22 | project: ./src/azurechat 23 | host: appservice 24 | language: ts 25 | 26 | hooks: 27 | preprovision: #determine "MY_IP" and write to .env file | whitelist IP during deployment 28 | posix: 29 | shell: sh 30 | run: ./scripts/set-env.sh 31 | interactive: true 32 | windows: 33 | shell: pwsh 34 | run: ./scripts/set-env.ps1 35 | interactive: true 36 | postprovision: # Deploy the azurechat app if DEPLOY_AZURE_CHATAPP is set to true 37 | posix: 38 | shell: sh 39 | run: ./scripts/deploy-azurechat.sh 40 | interactive: true 41 | windows: 42 | shell: pwsh 43 | run: ./scripts/deploy-azurechat.ps1 44 | interactive: true 45 | postdown: 46 | posix: 47 | shell: sh 48 | run: ./scripts/cleanup.sh 49 | interactive: true 50 | windows: 51 | shell: pwsh 52 | run: ./scripts/cleanup.ps1 53 | interactive: true 54 | 55 | 56 | workflows: 57 | up: # Deploy the application to Azure / Proxy only 58 | # post-provision hook will deploy azurechat if parameter 59 | # DEPLOY_AZURE_CHATAPP is set to true 60 | - azd: provision 61 | - azd: package proxy 62 | - azd: deploy proxy 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /debug.txt: -------------------------------------------------------------------------------- 1 | 2 | Deploying services (azd deploy) 3 | 4 | (-) Skipped: Deploying service azurechat 5 | Deploying service proxy 6 | Deploying service proxy (Tagging container image) 7 | Deploying service proxy (Tagging container image) 8 | Deploying service proxy (Logging into container registry) 9 | Deploying service proxy (Pushing container image) 10 | Deploying service proxy (Updating container app revision) 11 | Deploying service proxy (Fetching endpoints for container app service) 12 | (✓) Done: Deploying service proxy 13 | - Endpoint: https://ca-d2zff763zvpfy-proxy.nicepond-5e0ead5c.francecentral.azurecontainerapps.io/ 14 | 15 | 16 | SUCCESS: Your application was deployed to Azure in 23 seconds. 17 | You can view the resources created under the resource group rg-rbr-test-entai in Azure Portal: 18 | https://portal.azure.com/#@/resource/subscriptions/0865ebb3-1889-4871-8119-562cc313d111/resourceGroups/rg-rbr-test-entai/overview 19 | -------------------------------------------------------------------------------- /docs/images/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/enterprise-azureai/3e6c97fa4e0a04d26a378280a20b7e749fa5c013/docs/images/arch.png -------------------------------------------------------------------------------- /docs/images/chatapp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/enterprise-azureai/3e6c97fa4e0a04d26a378280a20b7e749fa5c013/docs/images/chatapp.png -------------------------------------------------------------------------------- /docs/images/config_chatapp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/enterprise-azureai/3e6c97fa4e0a04d26a378280a20b7e749fa5c013/docs/images/config_chatapp.png -------------------------------------------------------------------------------- /docs/images/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/enterprise-azureai/3e6c97fa4e0a04d26a378280a20b7e749fa5c013/docs/images/dashboard.png -------------------------------------------------------------------------------- /docs/images/deploy_chatapp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/enterprise-azureai/3e6c97fa4e0a04d26a378280a20b7e749fa5c013/docs/images/deploy_chatapp.png -------------------------------------------------------------------------------- /infra/main.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "environmentName": { 6 | "value": "${AZURE_ENV_NAME}" 7 | }, 8 | "location": { 9 | "value": "${AZURE_LOCATION}" 10 | }, 11 | "myIpAddress": { 12 | "value": "${MY_IP_ADDRESS}" 13 | }, 14 | "myPrincipalId": { 15 | "value": "${MY_USER_ID}" 16 | }, 17 | "useRedisCacheForAPIM" : { 18 | "value": "${USE_REDIS_CACHE_APIM}" 19 | }, 20 | "secondaryOpenAILocation" : { 21 | "value": "${SECONDARY_OPENAI_LOCATION}" 22 | }, 23 | "apimServiceName": { 24 | "value": "${APIM_NAME}" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /infra/modules/apim/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/enterprise-azureai/3e6c97fa4e0a04d26a378280a20b7e749fa5c013/infra/modules/apim/.DS_Store -------------------------------------------------------------------------------- /infra/modules/apim/apim-backend.bicep: -------------------------------------------------------------------------------- 1 | param apimServiceName string 2 | param proxyApiBackendId string 3 | param proxyAppUri string 4 | param logBytes int = 8192 5 | 6 | var logSettings = { 7 | headers: [ 'Content-type', 'User-agent' ] 8 | body: { bytes: logBytes } 9 | } 10 | 11 | resource apimService 'Microsoft.ApiManagement/service@2023-03-01-preview' existing = { 12 | name: apimServiceName 13 | } 14 | 15 | resource apimLogger 'Microsoft.ApiManagement/service/loggers@2021-12-01-preview' existing = { 16 | name: 'appinsights-logger' 17 | parent: apimService 18 | } 19 | 20 | resource backend 'Microsoft.ApiManagement/service/backends@2023-03-01-preview' = { 21 | name: proxyApiBackendId 22 | parent: apimService 23 | properties: { 24 | description: proxyApiBackendId 25 | url: proxyAppUri 26 | protocol: 'http' 27 | tls: { 28 | validateCertificateChain: true 29 | validateCertificateName: true 30 | } 31 | 32 | } 33 | } 34 | 35 | resource apimProxyApi 'Microsoft.ApiManagement/service/apis@2023-03-01-preview' = { 36 | name: 'ai-proxy' 37 | parent: apimService 38 | properties: { 39 | path: 'openai' 40 | apiRevision: '1' 41 | displayName: 'AI Proxy' 42 | format: 'openapi-link' 43 | protocols: [ 'https' ] 44 | value: 'https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cognitiveservices/data-plane/AzureOpenAI/inference/stable/2023-05-15/inference.json' 45 | subscriptionRequired: true 46 | subscriptionKeyParameterNames: { 47 | header: 'api-key' 48 | } 49 | } 50 | } 51 | 52 | resource proxyApiPolicy 'Microsoft.ApiManagement/service/apis/policies@2023-03-01-preview' = { 53 | name: 'policy' 54 | parent: apimProxyApi 55 | properties: { 56 | value: loadTextContent('./policies/api_policy_chargeback.xml') 57 | format: 'rawxml' 58 | } 59 | dependsOn: [ 60 | backend 61 | ] 62 | } 63 | 64 | resource diagnosticsPolicy 'Microsoft.ApiManagement/service/apis/diagnostics@2022-08-01' = if (!empty(apimLogger.name)) { 65 | name: 'applicationinsights' 66 | parent: apimProxyApi 67 | properties: { 68 | alwaysLog: 'allErrors' 69 | httpCorrelationProtocol: 'W3C' 70 | logClientIp: true 71 | loggerId: apimLogger.id 72 | metrics: true 73 | verbosity: 'verbose' 74 | sampling: { 75 | samplingType: 'fixed' 76 | percentage: 100 77 | } 78 | frontend: { 79 | request: logSettings 80 | response: logSettings 81 | } 82 | backend: { 83 | request: logSettings 84 | response: logSettings 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /infra/modules/apim/apim-redis-cache.bicep: -------------------------------------------------------------------------------- 1 | param apimServiceName string 2 | param redisCacheName string 3 | 4 | resource apimService 'Microsoft.ApiManagement/service@2023-03-01-preview' existing = { 5 | name: apimServiceName 6 | } 7 | 8 | resource redisCache 'Microsoft.Cache/redis@2022-06-01' existing = { 9 | name: redisCacheName 10 | } 11 | 12 | resource apimCache 'Microsoft.ApiManagement/service/caches@2023-03-01-preview' = { 13 | name: 'redis-cache' 14 | parent: apimService 15 | properties: { 16 | connectionString: '${redisCache.properties.hostName},password=${redisCache.listKeys().primaryKey},ssl=True,abortConnect=False' 17 | useFromLocation: 'default' 18 | description: redisCache.properties.hostName 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /infra/modules/apim/policies/api_policy_chargeback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | @( context.Subscription.Name ) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | @( context.Operation.Name ) 17 | 18 | 19 | @( context.Operation.Method ) 20 | 21 | 22 | @( context.Operation.UrlTemplate ) 23 | 24 | 25 | @( context.Api.Name ) 26 | 27 | 28 | @( context.Api.Path ) 29 | 30 | 31 | 32 | 33 | 34 | 35 | @( context.Operation.Name ) 36 | 37 | 38 | @( context.Operation.Method ) 39 | 40 | 41 | @( context.Operation.UrlTemplate ) 42 | 43 | 44 | @( context.Api.Name ) 45 | 46 | 47 | @( context.Api.Path ) 48 | 49 | 50 | @( context.LastError.Message ) 51 | 52 | 53 | -------------------------------------------------------------------------------- /infra/modules/appconfig/appconfig-proxy.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param azureMonitorDataCollectionEndPointUrl string 3 | param azureMonitorDataCollectionRuleImmutableId string 4 | param azureMonitorDataCollectionRuleStream string 5 | param proxyManagedIdentityName string 6 | param proxyConfig object 7 | param myPrincipalId string 8 | 9 | 10 | resource proxyIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' existing = { 11 | name: proxyManagedIdentityName 12 | } 13 | 14 | resource appconfig 'Microsoft.AppConfiguration/configurationStores@2023-03-01' existing = { 15 | name: name 16 | } 17 | 18 | module proxyRoleAssignment '../roleassignments/roleassignment.bicep' = { 19 | name: 'proxy-roleAssignment' 20 | params: { 21 | principalId: proxyIdentity.properties.principalId 22 | roleName: 'App Configuration Data Reader' 23 | targetResourceId: appconfig.id 24 | deploymentName: 'proxy-roleassignment-AppConfigurationDataReader' 25 | } 26 | } 27 | 28 | module currentUserRoleAssignment '../roleassignments/roleassignment.bicep' = { 29 | name: 'currentuser-roleAssignment' 30 | params: { 31 | principalId: myPrincipalId 32 | roleName: 'App Configuration Data Reader' 33 | targetResourceId: appconfig.id 34 | deploymentName: 'currentuser-roleassignment-AppConfigurationDataReader' 35 | principalType: 'User' 36 | } 37 | } 38 | 39 | 40 | 41 | module dataCollectionEndpoint 'configurationStoreKeyValues.bicep' = { 42 | name: 'AzureMonitor-DataCollectionEndPoint' 43 | params: { 44 | appconfigName : appconfig.name 45 | key : 'AzureMonitor:DataCollectionEndPoint' 46 | value: azureMonitorDataCollectionEndPointUrl 47 | } 48 | } 49 | 50 | module dataCollectionRuleImmutableId 'configurationStoreKeyValues.bicep' = { 51 | name: 'AzureMonitor-DataCollectionRuleImmutableId' 52 | params:{ 53 | appconfigName : appconfig.name 54 | key: 'AzureMonitor:DataCollectionRuleImmutableId' 55 | value: azureMonitorDataCollectionRuleImmutableId 56 | } 57 | } 58 | 59 | module dataCollectionRuleStream 'configurationStoreKeyValues.bicep' = { 60 | name: 'AzureMonitor-DataCollectionRuleStream' 61 | params: { 62 | appconfigName : appconfig.name 63 | key : 'AzureMonitor:DataCollectionRuleStream' 64 | value: azureMonitorDataCollectionRuleStream 65 | } 66 | } 67 | 68 | module EntraIdTenantId 'configurationStoreKeyValues.bicep' = { 69 | name: 'EntraId-TenantId' 70 | params: { 71 | appconfigName : appconfig.name 72 | key : 'EntraId:TenantId' 73 | value: subscription().tenantId 74 | } 75 | } 76 | 77 | module proxyConfiguration 'configurationStoreKeyValues.bicep' = { 78 | name: 'AzureAIProxy-ProxyConfig' 79 | params: { 80 | appconfigName : appconfig.name 81 | key : 'AzureAIProxy:ProxyConfig' 82 | value: replace(string(proxyConfig),',{}', '') 83 | } 84 | } 85 | 86 | -------------------------------------------------------------------------------- /infra/modules/appconfig/configurationStore.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param appconfigPrivateDnsZoneName string 4 | param appconfigPrivateEndpointName string 5 | param privateEndpointSubnetName string 6 | param vnetName string 7 | param vnetResourceGroupName string = resourceGroup().name 8 | param dnsResourceGroupName string = resourceGroup().name 9 | 10 | resource appconfig 'Microsoft.AppConfiguration/configurationStores@2023-03-01' = { 11 | name: name 12 | location: location 13 | sku: { 14 | name: 'Standard' 15 | } 16 | properties: { 17 | publicNetworkAccess: 'Enabled' 18 | } 19 | } 20 | 21 | 22 | module privateEndpoint '../networking/private-endpoint.bicep' = { 23 | name: '${appconfig.name}-privateEndpoint-deployment' 24 | params: { 25 | groupIds: [ 26 | 'configurationStores' 27 | ] 28 | dnsZoneName: appconfigPrivateDnsZoneName 29 | dnsResourceGroupName: dnsResourceGroupName 30 | name: appconfigPrivateEndpointName 31 | subnetName: privateEndpointSubnetName 32 | privateLinkServiceId: appconfig.id 33 | vNetName: vnetName 34 | vnetResourceGroupName : vnetResourceGroupName 35 | location: location 36 | } 37 | } 38 | 39 | 40 | output appConfigEndPoint string = appconfig.properties.endpoint 41 | output appConfigName string = appconfig.name 42 | -------------------------------------------------------------------------------- /infra/modules/appconfig/configurationStoreKeyValues.bicep: -------------------------------------------------------------------------------- 1 | param appconfigName string 2 | param key string 3 | param value string 4 | 5 | resource appconfig 'Microsoft.AppConfiguration/configurationStores@2023-03-01' existing = { 6 | name: appconfigName 7 | } 8 | 9 | resource keyvalue 'Microsoft.AppConfiguration/configurationStores/keyValues@2023-03-01' = { 10 | name: key 11 | parent: appconfig 12 | properties:{ 13 | value: value 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /infra/modules/appservice/azurechat.bicep: -------------------------------------------------------------------------------- 1 | param appservice_name string 2 | param location string 3 | param tags object 4 | param webapp_name string 5 | param appConfigEndpoint string 6 | param azureChatIdentityName string 7 | param subnetId string 8 | param keyvaultName string 9 | 10 | 11 | var nextAuthHash = uniqueString(azureChatIdentityName) 12 | 13 | 14 | resource userIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { 15 | name: azureChatIdentityName 16 | } 17 | 18 | resource appServicePlan 'Microsoft.Web/serverfarms@2020-06-01' = { 19 | name: appservice_name 20 | location: location 21 | tags: tags 22 | properties: { 23 | reserved: true 24 | } 25 | sku: { 26 | name: 'S1' 27 | tier: 'Standard' 28 | size: 'S1' 29 | family: 'S' 30 | capacity: 1 31 | } 32 | 33 | kind: 'linux' 34 | 35 | } 36 | 37 | resource webApp 'Microsoft.Web/sites@2023-01-01' = { 38 | name: webapp_name 39 | location: location 40 | tags: union(tags, { 'azd-service-name': 'azurechat' }) 41 | properties: { 42 | serverFarmId: appServicePlan.id 43 | virtualNetworkSubnetId: subnetId 44 | httpsOnly: true 45 | keyVaultReferenceIdentity: userIdentity.id 46 | siteConfig: { 47 | linuxFxVersion: 'node|18-lts' 48 | alwaysOn: true 49 | appCommandLine: 'next start' 50 | ftpsState: 'Disabled' 51 | minTlsVersion: '1.2' 52 | appSettings: [ 53 | { 54 | name: 'APPCONFIG_ENDPOINT' 55 | value: appConfigEndpoint 56 | } 57 | { 58 | name: 'NEXTAUTH_SECRET' 59 | value: nextAuthHash 60 | } 61 | { 62 | name: 'NEXTAUTH_URL' 63 | value: 'https://${webapp_name}.azurewebsites.net' 64 | } 65 | { 66 | name: 'SCM_DO_BUILD_DURING_DEPLOYMENT' 67 | value: 'true' 68 | } 69 | { 70 | name: 'AZURE_CLIENT_ID' 71 | value: userIdentity.properties.clientId 72 | } 73 | { 74 | name: 'AZURE_TENANT_ID' 75 | value: userIdentity.properties.tenantId 76 | } 77 | { 78 | name: 'AUTH_ENTRA_CLIENT_ID' 79 | value: '@Microsoft.KeyVault(VaultName=${keyvaultName};SecretName=AzureChatClientId)' 80 | } 81 | { 82 | name: 'AUTH_ENTRA_CLIENT_SECRET' 83 | value: '@Microsoft.KeyVault(VaultName=${keyvaultName};SecretName=AzureChatClientSecret)' 84 | } 85 | 86 | 87 | ] 88 | } 89 | 90 | } 91 | identity: { 92 | type: 'UserAssigned' 93 | userAssignedIdentities: { 94 | '${userIdentity.id}': {} 95 | } 96 | } 97 | } 98 | 99 | output webAppUrl string = 'https://${webApp.properties.defaultHostName}' 100 | output name string = webApp.name 101 | -------------------------------------------------------------------------------- /infra/modules/cache/redis.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param apimServiceName string 4 | param tags object = {} 5 | 6 | @description('The pricing tier of the new Azure Cache for Redis instance') 7 | @allowed([ 'Basic', 'Standard', 'Premium' ]) 8 | param sku string = 'Basic' 9 | 10 | @description('Specify the size of the new Azure Redis Cache instance. Valid values: for C (Basic/Standard) family (0, 1, 2, 3, 4, 5, 6), for P (Premium) family (1, 2, 3, 4)') 11 | @minValue(0) 12 | @maxValue(6) 13 | param capacity int = 1 14 | param publicNetworkAccess string = 'Disabled' 15 | 16 | //Private Endpoint settings 17 | param redisCachePrivateEndpointName string 18 | param vNetName string 19 | param privateEndpointSubnetName string 20 | param redisCacheDnsZoneName string 21 | 22 | var skuFamily = (sku == 'Premium') ? 'P' : 'C' 23 | 24 | resource redisCache 'Microsoft.Cache/redis@2022-06-01' = { 25 | name: name 26 | location: location 27 | tags: union(tags, { 'azd-service-name': name }) 28 | properties: { 29 | enableNonSslPort: false 30 | minimumTlsVersion: '1.2' 31 | publicNetworkAccess: publicNetworkAccess 32 | sku: { 33 | capacity: capacity 34 | family: skuFamily 35 | name: sku 36 | } 37 | } 38 | } 39 | 40 | module privateEndpoint '../networking/private-endpoint.bicep' = { 41 | name: '${redisCache.name}-privateEndpoint-deployment' 42 | params: { 43 | groupIds: [ 44 | 'redisCache' 45 | ] 46 | dnsZoneName: redisCacheDnsZoneName 47 | name: redisCachePrivateEndpointName 48 | subnetName: privateEndpointSubnetName 49 | privateLinkServiceId: redisCache.id 50 | vNetName: vNetName 51 | location: location 52 | } 53 | } 54 | 55 | module apimRedisCache '../apim/apim-redis-cache.bicep' = { 56 | name: 'apim-redis-cache-deployment' 57 | params: { 58 | apimServiceName: apimServiceName 59 | redisCacheName: redisCache.name 60 | } 61 | } 62 | 63 | output cacheName string = redisCache.name 64 | output hostName string = redisCache.properties.hostName 65 | -------------------------------------------------------------------------------- /infra/modules/host/container-app-environment.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | @description('Name of the Application Insights resource') 6 | param applicationInsightsName string = '' 7 | 8 | @description('Specifies if Dapr is enabled') 9 | param daprEnabled bool = false 10 | 11 | @description('Name of the Log Analytics workspace') 12 | param logAnalyticsWorkspaceName string 13 | 14 | param vnetName string 15 | param subnetName string 16 | 17 | resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-04-01-preview' = { 18 | name: name 19 | location: location 20 | tags: union(tags, { 'azd-service-name': name }) 21 | properties: { 22 | appLogsConfiguration: { 23 | destination: 'log-analytics' 24 | logAnalyticsConfiguration: { 25 | customerId: logAnalyticsWorkspace.properties.customerId 26 | sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey 27 | } 28 | } 29 | daprAIInstrumentationKey: daprEnabled && !empty(applicationInsightsName) ? applicationInsights.properties.InstrumentationKey : '' 30 | vnetConfiguration: { 31 | internal: true 32 | infrastructureSubnetId: resourceId('Microsoft.Network/VirtualNetworks/subnets', vnetName, subnetName) 33 | } 34 | workloadProfiles: [ 35 | { 36 | name: 'Consumption' 37 | workloadProfileType: 'Consumption' 38 | } 39 | ] 40 | } 41 | } 42 | 43 | module privateDnsZone '../networking/dns.bicep' = { 44 | name: 'dns-deployment-app' 45 | params: { 46 | name: containerAppsEnvironment.properties.defaultDomain 47 | } 48 | } 49 | 50 | module dnsEntry '../networking/dnsentry.bicep' = { 51 | name: 'dns-entry-env' 52 | params: { 53 | dnsZoneName: privateDnsZone.outputs.privateDnsZoneName 54 | hostname: '*' 55 | ipAddress: containerAppsEnvironment.properties.staticIp 56 | } 57 | } 58 | 59 | module privateDnsZoneLink '../networking/dnsvirtualnetworklink.bicep' = { 60 | name: 'dns-vnetlink' 61 | dependsOn: [ 62 | dnsEntry 63 | ] 64 | params: { 65 | dnsZoneName: privateDnsZone.outputs.privateDnsZoneName 66 | vnetName: vnetName 67 | } 68 | } 69 | 70 | resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = { 71 | name: logAnalyticsWorkspaceName 72 | } 73 | 74 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (daprEnabled && !empty(applicationInsightsName)) { 75 | name: applicationInsightsName 76 | } 77 | 78 | output defaultDomain string = containerAppsEnvironment.properties.defaultDomain 79 | output id string = containerAppsEnvironment.id 80 | output name string = containerAppsEnvironment.name 81 | -------------------------------------------------------------------------------- /infra/modules/monitor/applicationinsights.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | param logAnalyticsWorkspaceId string 5 | //Private Endpoint settings 6 | param privateLinkScopeName string 7 | param vNetName string 8 | param privateEndpointSubnetName string 9 | param dnsZoneName string 10 | param privateEndpointName string 11 | 12 | resource privateLinkScope 'microsoft.insights/privateLinkScopes@2021-07-01-preview' existing = { 13 | name: privateLinkScopeName 14 | } 15 | 16 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { 17 | name: name 18 | location: location 19 | tags: union(tags, { 'azd-service-name': name }) 20 | kind: 'web' 21 | properties: { 22 | Application_Type: 'web' 23 | WorkspaceResourceId: logAnalyticsWorkspaceId 24 | publicNetworkAccessForIngestion: 'Disabled' 25 | publicNetworkAccessForQuery: 'Enabled' 26 | } 27 | } 28 | 29 | module privateEndpoint '../networking/private-endpoint.bicep' = { 30 | name: '${applicationInsights.name}-privateEndpoint-deployment' 31 | params: { 32 | groupIds: [ 33 | 'azuremonitor' 34 | ] 35 | dnsZoneName: dnsZoneName 36 | name: privateEndpointName 37 | subnetName: privateEndpointSubnetName 38 | privateLinkServiceId: privateLinkScope.id 39 | vNetName: vNetName 40 | location: location 41 | } 42 | } 43 | 44 | resource appInsightsScopedResource 'Microsoft.Insights/privateLinkScopes/scopedResources@2021-07-01-preview' = { 45 | parent: privateLinkScope 46 | name: '${applicationInsights.name}-connection' 47 | properties: { 48 | linkedResourceId: applicationInsights.id 49 | } 50 | } 51 | 52 | output connectionString string = applicationInsights.properties.ConnectionString 53 | output instrumentationKey string = applicationInsights.properties.InstrumentationKey 54 | output name string = applicationInsights.name 55 | -------------------------------------------------------------------------------- /infra/modules/monitor/datacollectionendpoint.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string 3 | 4 | 5 | resource dataCollectionEndpoint 'Microsoft.Insights/dataCollectionEndpoints@2022-06-01' = { 6 | name: name 7 | location: location 8 | properties:{ 9 | description: 'Data Collection Endpoint for Azure OpenAI Chargeback' 10 | } 11 | 12 | } 13 | 14 | output dataCollectionEndpointResourceId string = dataCollectionEndpoint.id 15 | output dataCollectionEndPointLogIngestionUrl string = dataCollectionEndpoint.properties.logsIngestion.endpoint 16 | 17 | -------------------------------------------------------------------------------- /infra/modules/monitor/datacollectionrule.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | param logAnalyticsWorkspaceResourceId string 5 | param dataCollectionEndpointResourceId string 6 | param proxyManagedIdentityName string 7 | 8 | 9 | resource proxyIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' existing = { 10 | name: proxyManagedIdentityName 11 | } 12 | 13 | var uniqueDestinationName = uniqueString('Custom-OpenAIChargeback_CL') 14 | 15 | resource dataCollectionRule 'Microsoft.Insights/dataCollectionRules@2022-06-01' = { 16 | location: location 17 | name: name 18 | tags: union(tags, { 'azd-service-name': name }) 19 | properties: { 20 | dataCollectionEndpointId: dataCollectionEndpointResourceId 21 | streamDeclarations: { 22 | 'Custom-OpenAIChargeback_CL': { 23 | columns: [ 24 | { 25 | name: 'TimeGenerated' 26 | type: 'datetime' 27 | } 28 | { 29 | name: 'Consumer' 30 | type: 'string' 31 | } 32 | { 33 | name: 'Model' 34 | type: 'string' 35 | } 36 | { 37 | name: 'ObjectType' 38 | type: 'string' 39 | } 40 | { 41 | name: 'InputTokens' 42 | type: 'int' 43 | } 44 | { 45 | name: 'OutputTokens' 46 | type: 'int' 47 | } 48 | { 49 | name: 'TotalTokens' 50 | type: 'int' 51 | } 52 | ] 53 | } 54 | } 55 | destinations: { 56 | logAnalytics: [ 57 | { 58 | workspaceResourceId: logAnalyticsWorkspaceResourceId 59 | name: uniqueDestinationName 60 | } 61 | ] 62 | } 63 | dataFlows: [ 64 | { 65 | streams:[ 66 | 'Custom-OpenAIChargeback_CL' 67 | ] 68 | destinations: [ 69 | uniqueDestinationName 70 | ] 71 | transformKql: 'source' 72 | outputStream: 'Custom-OpenAIChargeback_CL' 73 | } 74 | ] 75 | } 76 | } 77 | 78 | module roleAssignment '../roleassignments/roleassignment.bicep' = { 79 | name: 'roleAssignment' 80 | params: { 81 | principalId: proxyIdentity.properties.principalId 82 | roleName: 'Monitoring Metrics Publisher' 83 | targetResourceId: dataCollectionRule.id 84 | deploymentName: 'proxy-roleassignment-MonitoringMetricsPublisher' 85 | } 86 | } 87 | 88 | output dataCollectionRuleId string = dataCollectionRule.id 89 | output dataCollectionRuleImmutableId string = dataCollectionRule.properties.immutableId 90 | output dataCollectionRuleStreamName string = 'Custom-OpenAIChargeback_CL' 91 | -------------------------------------------------------------------------------- /infra/modules/monitor/loganalytics.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | //Private Endpoint 5 | param privateLinkScopeName string 6 | 7 | resource privateLinkScope 'microsoft.insights/privateLinkScopes@2021-07-01-preview' existing = { 8 | name: privateLinkScopeName 9 | } 10 | 11 | resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { 12 | name: name 13 | location: location 14 | tags: union(tags, { 'azd-service-name': name }) 15 | properties: any({ 16 | retentionInDays: 30 17 | features: { 18 | searchVersion: 1 19 | } 20 | sku: { 21 | name: 'PerGB2018' 22 | } 23 | publicNetworkAccessForIngestion: 'Disabled' 24 | publicNetworkAccessForQuery: 'Enabled' 25 | }) 26 | } 27 | 28 | //Private Endpoint 29 | resource logAnalyticsScopedResource 'Microsoft.Insights/privateLinkScopes/scopedResources@2021-07-01-preview' = { 30 | parent: privateLinkScope 31 | name: '${logAnalytics.name}-connection' 32 | properties: { 33 | linkedResourceId: logAnalytics.id 34 | } 35 | } 36 | 37 | // resource roleAssignmentChargeBack 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 38 | // scope: logAnalytics 39 | // name: guid(managedIdentityChargeBack.id, roleDefinitionResourceId) 40 | // properties: { 41 | // roleDefinitionId: roleDefinitionResourceId 42 | // principalId: managedIdentityChargeBack.properties.principalId 43 | // principalType: 'ServicePrincipal' 44 | // } 45 | // } 46 | 47 | output resourceId string = logAnalytics.id 48 | output name string = logAnalytics.name 49 | -------------------------------------------------------------------------------- /infra/modules/monitor/loganatylicscustomtable.bicep: -------------------------------------------------------------------------------- 1 | param logAnalyticsWorkspaceName string 2 | 3 | 4 | resource azureOpenAIChargebackTable 'Microsoft.OperationalInsights/workspaces/tables@2022-10-01' = { 5 | name: '${logAnalyticsWorkspaceName}/OpenAIChargeback_CL' 6 | properties: { 7 | totalRetentionInDays: 90 8 | retentionInDays: 90 9 | plan: 'Analytics' 10 | schema: { 11 | name: 'OpenAIChargeback_CL' 12 | description:'Custom Log Analytics table for OpenAI Chargeback data' 13 | columns: [ 14 | { 15 | name: 'TimeGenerated' 16 | type: 'datetime' 17 | } 18 | { 19 | name: 'Consumer' 20 | type: 'string' 21 | } 22 | { 23 | name: 'Model' 24 | type: 'string' 25 | } 26 | { 27 | name: 'ObjectType' 28 | type: 'string' 29 | } 30 | { 31 | name: 'InputTokens' 32 | type: 'int' 33 | } 34 | { 35 | name: 'OutputTokens' 36 | type: 'int' 37 | } 38 | { 39 | name: 'TotalTokens' 40 | type: 'int' 41 | } 42 | ] 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /infra/modules/networking/dns.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param tags object = {} 3 | 4 | resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { 5 | name: name 6 | location: 'global' 7 | tags: union(tags, { 'azd-service-name': name }) 8 | 9 | } 10 | 11 | output privateDnsZoneName string = privateDnsZone.name 12 | -------------------------------------------------------------------------------- /infra/modules/networking/dnsentry.bicep: -------------------------------------------------------------------------------- 1 | param dnsZoneName string 2 | param ipAddress string 3 | param hostname string 4 | 5 | resource dnsEntry 'Microsoft.Network/privateDnsZones/A@2020-06-01' = { 6 | name : '${dnsZoneName}/${hostname}' 7 | properties: { 8 | ttl: 3600 9 | aRecords: [ 10 | { 11 | ipv4Address: ipAddress 12 | } 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /infra/modules/networking/dnsvirtualnetworklink.bicep: -------------------------------------------------------------------------------- 1 | param vnetName string 2 | param dnsZoneName string 3 | 4 | 5 | resource vnet 'Microsoft.Network/virtualNetworks@2019-11-01' existing = { 6 | name: vnetName 7 | } 8 | 9 | resource dnsVnetLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { 10 | name : '${dnsZoneName}/${vnetName}' 11 | location: 'global' 12 | properties: { 13 | virtualNetwork: { 14 | id: vnet.id 15 | } 16 | registrationEnabled: false 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /infra/modules/networking/private-endpoint.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param subnetName string 3 | param vNetName string 4 | param privateLinkServiceId string 5 | param groupIds array 6 | param dnsZoneName string 7 | param location string 8 | param vnetResourceGroupName string = resourceGroup().name 9 | param dnsResourceGroupName string = resourceGroup().name 10 | 11 | resource rgNetwork 'Microsoft.Resources/resourceGroups@2023-07-01' existing = { 12 | name: vnetResourceGroupName 13 | scope: subscription() 14 | } 15 | 16 | resource rgDns 'Microsoft.Resources/resourceGroups@2023-07-01' existing = { 17 | name: dnsResourceGroupName 18 | scope: subscription() 19 | } 20 | 21 | resource privateEndpointSubnet 'Microsoft.Network/virtualNetworks/subnets@2022-09-01' existing = { 22 | name: '${vNetName}/${subnetName}' 23 | scope: rgNetwork 24 | } 25 | 26 | resource privateEndpointDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' existing = { 27 | name: dnsZoneName 28 | scope: rgDns 29 | } 30 | 31 | resource privateEndpoint 'Microsoft.Network/privateEndpoints@2022-09-01' = { 32 | name: name 33 | location: location 34 | properties: { 35 | subnet: { 36 | id: privateEndpointSubnet.id 37 | } 38 | privateLinkServiceConnections: [ 39 | { 40 | name: name 41 | properties: { 42 | privateLinkServiceId: privateLinkServiceId 43 | groupIds: groupIds 44 | } 45 | } 46 | ] 47 | } 48 | } 49 | 50 | resource privateEndpointDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2022-09-01' = { 51 | parent: privateEndpoint 52 | name: 'privateDnsZoneGroup' 53 | properties: { 54 | privateDnsZoneConfigs: [ 55 | { 56 | name: 'default' 57 | properties: { 58 | privateDnsZoneId: privateEndpointDnsZone.id 59 | } 60 | } 61 | ] 62 | } 63 | } 64 | 65 | output privateEndpointName string = privateEndpoint.name 66 | -------------------------------------------------------------------------------- /infra/modules/networking/publicip.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string 3 | param tags object = {} 4 | param fqdn string 5 | 6 | 7 | resource publicIp 'Microsoft.Network/publicIPAddresses@2023-04-01' = { 8 | name: name 9 | location: location 10 | tags: tags 11 | sku: { 12 | name: 'Standard' 13 | tier: 'Regional' 14 | } 15 | zones: [ 16 | '1' 17 | '2' 18 | '3' 19 | ] 20 | properties: { 21 | publicIPAllocationMethod: 'Static' 22 | publicIPAddressVersion: 'IPv4' 23 | dnsSettings: { 24 | domainNameLabel: name 25 | fqdn: fqdn 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /infra/modules/roleassignments/roleassignment.bicep: -------------------------------------------------------------------------------- 1 | param targetResourceId string 2 | param deploymentName string 3 | param roleName string = '' 4 | param roleDefinitionId string = '' 5 | param principalId string 6 | param principalType string = 'ServicePrincipal' 7 | 8 | 9 | var rolesIds = loadJsonContent('./roles.json') 10 | 11 | resource ResourceRoleAssignment 'Microsoft.Resources/deployments@2023-07-01' = { 12 | name: deploymentName 13 | properties: { 14 | mode: 'Incremental' 15 | template: json(loadTextContent('./roleassignmentARM.json')) 16 | parameters: { 17 | scope: { 18 | value: targetResourceId 19 | } 20 | roleDefinitionId: { 21 | value: roleName == '' ? roleDefinitionId : rolesIds[roleName] 22 | } 23 | principalId: { 24 | value: principalId 25 | } 26 | principalType: { 27 | value: principalType 28 | } 29 | name: { 30 | value: guid(targetResourceId, principalId) 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /infra/modules/roleassignments/roleassignmentARM.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "scope": { 6 | "type": "string" 7 | }, 8 | "name": { 9 | "type": "string" 10 | }, 11 | "roleDefinitionId": { 12 | "type": "string" 13 | }, 14 | "principalId": { 15 | "type": "string" 16 | }, 17 | "principalType": { 18 | "type": "string" 19 | } 20 | }, 21 | "resources": [ 22 | { 23 | "type": "Microsoft.Authorization/roleAssignments", 24 | "apiVersion": "2020-08-01-preview", 25 | "scope": "[parameters('scope')]", 26 | "name": "[parameters('name')]", 27 | "properties": { 28 | "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('roleDefinitionId'))]", 29 | "principalId": "[parameters('principalId')]", 30 | "principalType": "[parameters('principalType')]" 31 | } 32 | } 33 | ], 34 | "outputs": { 35 | "roleAssignmentId": { 36 | "type": "string", 37 | "value": "[extensionResourceId(parameters('scope'), 'Microsoft.Authorization/roleAssignments', parameters('name'))]" 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /infra/modules/roleassignments/roles.json: -------------------------------------------------------------------------------- 1 | { 2 | "App Configuration Data Reader": "516239f1-63e1-4d78-a4de-a74fb236a071", 3 | "Monitoring Metrics Publisher" : "3913510d-42f4-4e42-8a64-420c390055eb", 4 | "AcrPull":"7f951dda-4ed3-4680-a7ca-43fe172d538d", 5 | "Cognitive Services OpenAI Contributor": "a001fd3d-188f-4b5d-821b-7da978bf7442", 6 | "Cognitive Services OpenAI User" : "5e0bd9bd-7b93-4f28-af87-19fc36ad61bd", 7 | "Key Vault Secrets User": "4633458b-17de-408a-b874-0445c86b69e6", 8 | "Key Vault Secrets Officer": "b86a8fe4-44ce-4948-aee5-eccb2c155cd7" 9 | 10 | 11 | } -------------------------------------------------------------------------------- /infra/modules/security/assignment.bicep: -------------------------------------------------------------------------------- 1 | param name string = 'audit-cogs-disable-public-access' 2 | param definitionId string = '/providers/Microsoft.Authorization/policyDefinitions/0725b4dd-7e76-479c-a735-68e7ee23d5ca' 3 | param tags object = {} 4 | 5 | resource assignment 'Microsoft.Authorization/policyAssignments@2021-09-01' = { 6 | name: name 7 | tags: union(tags, { 'azd-service-name': name }) 8 | properties: { 9 | policyDefinitionId: definitionId 10 | } 11 | } 12 | 13 | output assignmentId string = assignment.id 14 | -------------------------------------------------------------------------------- /infra/modules/security/managed-identity.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = { 6 | name: name 7 | location: location 8 | tags: union(tags, { 'azd-service-name': name }) 9 | } 10 | 11 | output managedIdentityName string = managedIdentity.name 12 | output managedIdentityClientId string = managedIdentity.properties.clientId 13 | -------------------------------------------------------------------------------- /infra/proxy.bicep: -------------------------------------------------------------------------------- 1 | param name string = '' 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | param identityName string 5 | param imageName string 6 | param containerAppsEnvironmentName string 7 | param containerRegistryName string 8 | param appConfigEndpoint string 9 | param appInsightsConnectionString string 10 | param managedIdentityName string 11 | 12 | resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview' existing = { 13 | name: managedIdentityName 14 | } 15 | 16 | module app 'modules/host/container-app.bicep' = { 17 | name: 'container-app' 18 | params: { 19 | name: name 20 | location: location 21 | tags: tags 22 | identityName: identityName 23 | imageName: imageName 24 | containerAppsEnvironmentName: containerAppsEnvironmentName 25 | containerRegistryName: containerRegistryName 26 | containerName: imageName 27 | azdServiceName: 'proxy' 28 | pullFromPrivateRegistry: true 29 | targetPort: 8080 30 | external: true 31 | env: [ 32 | { 33 | name: 'APPCONFIG_ENDPOINT' 34 | value: appConfigEndpoint 35 | } 36 | { 37 | name: 'CLIENT_ID' 38 | value: managedIdentity.properties.clientId 39 | } 40 | { 41 | name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' 42 | value: appInsightsConnectionString 43 | } 44 | ] 45 | 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /infra/proxy.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "location": { 6 | "value": "${AZURE_LOCATION}" 7 | }, 8 | "name": { 9 | "value": "${SERVICE_PROXY_NAME}" 10 | }, 11 | "imageName": { 12 | "value": "${SERVICE_PROXY_IMAGE_NAME}" 13 | }, 14 | "containerAppsEnvironmentName": { 15 | "value": "${AZURE_CONTAINER_ENVIRONMENT_NAME}" 16 | }, 17 | "containerRegistryName": { 18 | "value": "${AZURE_CONTAINER_REGISTRY_NAME}" 19 | }, 20 | "managedIdentityName": { 21 | "value": "${AZURE_PROXY_MANAGED_IDENTITY_NAME}" 22 | }, 23 | "appInsightsConnectionString": { 24 | "value": "${APPLICATIONINSIGHTS_CONNECTION_STRING}" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "azh-enterprise-azureai", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /scripts/appreg.ps1: -------------------------------------------------------------------------------- 1 | #run az login and set correct subscription if needed 2 | ./scripts/set-az-currentsubscription.ps1 3 | 4 | if ($? -eq $true) { 5 | 6 | $azdenv = azd env get-values --output json | ConvertFrom-Json 7 | 8 | #check if registration exists 9 | $displayName = "Enterprise-AzureAI-ChatApp-" + $azdenv.RESOURCE_TOKEN 10 | $app = az ad app list --display-name $displayName --output json | ConvertFrom-Json 11 | 12 | if (!$app) { 13 | Write-Host "Creating new app registration $displayName..." 14 | 15 | $localReplyUrl = "http://localhost:3000/api/auth/callback/azure-ad" 16 | $azureReplyUrl = $azdenv.AZURE_CHATAPP_URL + "/api/auth/callback/azure-ad" 17 | $redirectUris = @($localReplyUrl, $azureReplyUrl) 18 | 19 | $app = az ad app create --display-name $displayName ` 20 | --web-redirect-uris $redirectUris ` 21 | --sign-in-audience AzureADMyOrg ` 22 | --output json | ConvertFrom-Json 23 | 24 | Write-Host "New App registration $displayName created successfully..." 25 | 26 | Write-Host "Create Secret Credentials" 27 | $cred = az ad app credential reset --id $app.appId ` 28 | --display-name "azurechat-secret" ` 29 | --output json | ConvertFrom-Json 30 | 31 | Write-Host "Secret Credentials created successfully..." 32 | Write-Host "Create Key Vault Secrets" 33 | 34 | $s1 = az keyvault secret set --name AzureChatClientSecret ` 35 | --vault-name $azdenv.AZURE_CHATAPP_KEYVAULT_NAME ` 36 | --value $cred.password ` 37 | --output json | ConvertFrom-Json 38 | 39 | $s2 = az keyvault secret set --name AzureChatClientId ` 40 | --vault-name $azdenv.AZURE_CHATAPP_KEYVAULT_NAME ` 41 | --value $app.appId ` 42 | --output json | ConvertFrom-Json 43 | 44 | azd env set AZURE_CHATAPP_CLIENT_ID $app.appId 45 | } 46 | else { 47 | Write-Host "Application registration $displayName already exists" 48 | } 49 | } -------------------------------------------------------------------------------- /scripts/appreg.sh: -------------------------------------------------------------------------------- 1 | # to run the script outside of the azd context, we need to set the env vars 2 | while IFS='=' read -r key value; do 3 | value=$(echo "$value" | sed 's/^"//' | sed 's/"$//') 4 | export "$key=$value" 5 | done <.azconfig.io 4 | 5 | #managed identity info, but running locally it will use your developer credentials 6 | AZURE_CLIENT_ID= 7 | AZURE_TENANT_ID= 8 | 9 | #login - when not using developer credentials 10 | AUTH_ENTRA_CLIENT_ID= 11 | AUTH_ENTRA_CLIENT_SECRET= should be available in keyvault> 12 | 13 | # no need to change these values 14 | NEXTAUTH_SECRET=AZURE-OPENAI-NEXTAUTH-OWNKEY@1 15 | NEXTAUTH_URL=http://localhost:3000 16 | -------------------------------------------------------------------------------- /src/azurechat/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/azurechat/app-global.ts: -------------------------------------------------------------------------------- 1 | export const APP_VERSION = "1.2.0"; 2 | -------------------------------------------------------------------------------- /src/azurechat/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from "@/features/auth/auth-api"; 2 | 3 | export { handlers as GET, handlers as POST }; 4 | -------------------------------------------------------------------------------- /src/azurechat/app/api/chat/route.ts: -------------------------------------------------------------------------------- 1 | import { chatAPIEntry } from "@/features/chat/chat-services/chat-api-entry"; 2 | 3 | export async function POST(req: Request) { 4 | const body = await req.json(); 5 | return await chatAPIEntry(body); 6 | } 7 | -------------------------------------------------------------------------------- /src/azurechat/app/change-log/layout.tsx: -------------------------------------------------------------------------------- 1 | import { MainMenu } from "@/features/main-menu/menu"; 2 | import { AI_NAME } from "@/features/theme/customise"; 3 | 4 | export const dynamic = "force-dynamic"; 5 | 6 | export const metadata = { 7 | title: AI_NAME, 8 | description: AI_NAME, 9 | }; 10 | 11 | export default async function RootLayout({ 12 | children, 13 | }: { 14 | children: React.ReactNode; 15 | }) { 16 | return ( 17 | <> 18 | 19 |
20 | {children} 21 |
22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/azurechat/app/change-log/loading.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingSkeleton } from "@/features/loading-skeleton"; 2 | 3 | export default function Loading() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/azurechat/app/change-log/page.tsx: -------------------------------------------------------------------------------- 1 | import { Markdown } from "@/components/markdown/markdown"; 2 | import { Card } from "@/components/ui/card"; 3 | import { VersionDisplay } from "@/features/change-log/version-display"; 4 | import { promises as fs } from "fs"; 5 | import { Suspense } from "react"; 6 | 7 | export const dynamic = "force-dynamic"; 8 | 9 | export default async function Home() { 10 | const content = await loadContent(); 11 | return ( 12 | 13 |
14 | 15 | 16 | 17 |
18 | 19 |
20 |
21 |
22 | ); 23 | } 24 | 25 | const loadContent = async () => { 26 | if (process.env.NODE_ENV === "production") { 27 | const response = await fetch( 28 | "https://raw.githubusercontent.com/microsoft/azurechat/main/src/app/change-log/update.md", 29 | { 30 | cache: "no-cache", 31 | } 32 | ); 33 | return await response.text(); 34 | } else { 35 | return await fs.readFile( 36 | process.cwd() + "/app/change-log/update.md", 37 | "utf8" 38 | ); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/azurechat/app/change-log/update.md: -------------------------------------------------------------------------------- 1 | # Azure Chat Updates 2 | 3 | Below are the updates for the Azure Chat Solution accelerator 4 | 5 | ## 📂 Chat with file 6 | 7 | - In the chat with file feature, you can now see citations within the responses. Simply click on the citation to access the related context. 8 | 9 | - You can now upload files to existing chats, allowing you to chat with multiple files simultaneously. 10 | 11 | ## 🎙️ Speech 12 | 13 | Ability to use Azure Speech in conversations. This feature is not enabled by default. To enable this feature, you must set the environment variable `PUBLIC_SPEECH_ENABLED=true` along with the Azure Speech subscription key and region. 14 | 15 | ``` 16 | PUBLIC_SPEECH_ENABLED=true 17 | AZURE_SPEECH_REGION="REGION" 18 | AZURE_SPEECH_KEY="1234...." 19 | ``` 20 | 21 | ## 🔑 Environment variable change 22 | 23 | Please note that the solution has been upgraded to utilise the most recent version of the OpenAI JavaScript SDK, necessitating the use of the `OPENAI_API_KEY` environment variable. 24 | 25 | Ensure that you update the variable name in both your '.env' file and the configuration within Azure App Service or Key Vault, changing it from `AZURE_OPENAI_API_KEY` to `OPENAI_API_KEY`. 26 | -------------------------------------------------------------------------------- /src/azurechat/app/chat/[id]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingSkeleton } from "@/features/loading-skeleton"; 2 | 3 | export default function Loading() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/azurechat/app/chat/[id]/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from "@/components/ui/card"; 2 | import { NewChat } from "@/features/chat/chat-menu/new-chat"; 3 | 4 | export default async function NotFound() { 5 | return ( 6 | 7 |
8 |
9 |

Uh-oh! 404

10 |

11 | How about we start a new chat? 12 |

13 | 14 |
15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/azurechat/app/chat/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { FindAllChats } from "@/features/chat/chat-services/chat-service"; 2 | import { FindChatThreadByID } from "@/features/chat/chat-services/chat-thread-service"; 3 | import { ChatProvider } from "@/features/chat/chat-ui/chat-context"; 4 | import { ChatUI } from "@/features/chat/chat-ui/chat-ui"; 5 | import { GetDepartments, GetDeployments } from "@/features/common/appconfig"; 6 | import { notFound } from "next/navigation"; 7 | 8 | export const dynamic = "force-dynamic"; 9 | 10 | export default async function Home({ params }: { params: { id: string } }) { 11 | const [items, thread] = await Promise.all([ 12 | FindAllChats(params.id), 13 | FindChatThreadByID(params.id), 14 | ]); 15 | 16 | const deployments = await GetDeployments(); 17 | const departments = await GetDepartments(); 18 | 19 | if (thread.length === 0) { 20 | notFound(); 21 | } 22 | 23 | return ( 24 | 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/azurechat/app/chat/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ChatMenu } from "@/features/chat/chat-menu/chat-menu"; 2 | import { ChatMenuContainer } from "@/features/chat/chat-menu/chat-menu-container"; 3 | import { MainMenu } from "@/features/main-menu/menu"; 4 | import { AI_NAME } from "@/features/theme/customise"; 5 | 6 | export const dynamic = "force-dynamic"; 7 | 8 | export const metadata = { 9 | title: AI_NAME, 10 | description: AI_NAME, 11 | }; 12 | 13 | export default async function RootLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }) { 18 | return ( 19 | <> 20 | 21 |
22 | 23 | 24 | 25 | {children} 26 |
27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/azurechat/app/chat/loading.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingSkeleton } from "@/features/loading-skeleton"; 2 | 3 | export default function Loading() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/azurechat/app/chat/page.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from "@/components/ui/card"; 2 | import { StartNewChat } from "@/features/chat/chat-ui/chat-empty-state/start-new-chat"; 3 | 4 | export const dynamic = "force-dynamic"; 5 | 6 | export default async function Home() { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/azurechat/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/enterprise-azureai/3e6c97fa4e0a04d26a378280a20b7e749fa5c013/src/azurechat/app/favicon.ico -------------------------------------------------------------------------------- /src/azurechat/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 240 10% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 240 10% 3.9%; 13 | --primary: 170 90% 29%; 14 | --primary-foreground: 0 0% 98%; 15 | --secondary: 240 4.8% 95.9%; 16 | --secondary-foreground: 240 5.9% 10%; 17 | --muted: 240 4.8% 95.9%; 18 | --muted-foreground: 240 3.8% 46.1%; 19 | --accent: 240 4.8% 95.9%; 20 | --accent-foreground: 240 5.9% 10%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 240 5.9% 90%; 24 | --input: 240 5.9% 90%; 25 | --ring: 170 90% 29%; 26 | --radius: 0.5rem; 27 | } 28 | 29 | .dark { 30 | --background: 240 10% 3.9%; 31 | --foreground: 0 0% 98%; 32 | --card: 240 10% 3.9%; 33 | --card-foreground: 0 0% 98%; 34 | --popover: 240 10% 3.9%; 35 | --popover-foreground: 0 0% 98%; 36 | --primary: 169 74% 22%; 37 | --primary-foreground: 240 5.9% 10%; 38 | --secondary: 240 3.7% 15.9%; 39 | --secondary-foreground: 0 0% 98%; 40 | --muted: 240 3.7% 15.9%; 41 | --muted-foreground: 240 5% 64.9%; 42 | --accent: 240 3.7% 15.9%; 43 | --accent-foreground: 0 0% 98%; 44 | --destructive: 0 62.8% 30.6%; 45 | --destructive-foreground: 0 0% 98%; 46 | --border: 240 3.7% 15.9%; 47 | --input: 240 3.7% 15.9%; 48 | --ring: 169 74% 22%; 49 | } 50 | } 51 | 52 | @layer base { 53 | * { 54 | @apply border-border; 55 | } 56 | body { 57 | @apply bg-background text-foreground; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/azurechat/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProvider } from "@/components/theme-provider"; 2 | import { Toaster } from "@/components/ui/toaster"; 3 | import { GlobalConfigProvider } from "@/features/global-config/global-client-config-context"; 4 | import { Providers } from "@/features/providers"; 5 | import { AI_NAME } from "@/features/theme/customise"; 6 | import { cn } from "@/lib/utils"; 7 | import { Inter } from "next/font/google"; 8 | import "./globals.css"; 9 | 10 | export const dynamic = "force-dynamic"; 11 | 12 | const inter = Inter({ subsets: ["latin"] }); 13 | 14 | export const metadata = { 15 | title: AI_NAME, 16 | description: AI_NAME, 17 | }; 18 | 19 | export default function RootLayout({ 20 | children, 21 | }: { 22 | children: React.ReactNode; 23 | }) { 24 | return ( 25 | 26 | 27 | 30 | 31 | 32 |
38 | {children} 39 |
40 | 41 | 42 |
43 |
44 |
45 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/azurechat/app/loading.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingSkeleton } from "@/features/loading-skeleton"; 2 | 3 | export default function Loading() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/azurechat/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { LogIn } from "@/components/login/login"; 2 | import { Card } from "@/components/ui/card"; 3 | import { userSession } from "@/features/auth/helpers"; 4 | import { redirect } from "next/navigation"; 5 | 6 | export const dynamic = "force-dynamic"; 7 | 8 | export default async function Home() { 9 | const user = await userSession(); 10 | if (user) { 11 | redirect("/chat"); 12 | } 13 | return ( 14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/azurechat/app/reporting/[chatid]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingSkeleton } from "@/features/loading-skeleton"; 2 | 3 | export default function Loading() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/azurechat/app/reporting/[chatid]/page.tsx: -------------------------------------------------------------------------------- 1 | import { ChatReportingUI } from "@/features/reporting/chat-reporting-ui"; 2 | 3 | export default async function Home({ params }: { params: { chatid: string } }) { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/azurechat/app/reporting/layout.tsx: -------------------------------------------------------------------------------- 1 | import { MainMenu } from "@/features/main-menu/menu"; 2 | import { AI_NAME } from "@/features/theme/customise"; 3 | 4 | export const metadata = { 5 | title: AI_NAME, 6 | description: AI_NAME, 7 | }; 8 | 9 | export default async function RootLayout({ 10 | children, 11 | }: { 12 | children: React.ReactNode; 13 | }) { 14 | return ( 15 | <> 16 | 17 |
{children}
18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/azurechat/app/reporting/loading.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingSkeleton } from "@/features/loading-skeleton"; 2 | 3 | export default function Loading() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/azurechat/app/reporting/page.tsx: -------------------------------------------------------------------------------- 1 | import { Reporting, ReportingProp } from "@/features/reporting/reporting"; 2 | 3 | export default async function Home(props: ReportingProp) { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/azurechat/app/unauthorized/layout.tsx: -------------------------------------------------------------------------------- 1 | import { MainMenu } from "@/features/main-menu/menu"; 2 | import { AI_NAME } from "@/features/theme/customise"; 3 | 4 | export const metadata = { 5 | title: AI_NAME, 6 | description: AI_NAME, 7 | }; 8 | 9 | export default async function RootLayout({ 10 | children, 11 | }: { 12 | children: React.ReactNode; 13 | }) { 14 | return ( 15 | <> 16 | 17 |
{children}
18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/azurechat/app/unauthorized/loading.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingSkeleton } from "@/features/loading-skeleton"; 2 | 3 | export default function Loading() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/azurechat/app/unauthorized/page.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from "@/components/ui/card"; 2 | 3 | export default async function Home() { 4 | return ( 5 | 6 |
7 |

You not authorized to view this page

8 |

This page can only be viewed by admin users.

9 |
10 |
11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /src/azurechat/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tailwind": { 6 | "config": "tailwind.config.js", 7 | "css": "app/globals.css", 8 | "baseColor": "slate", 9 | "cssVariables": true 10 | }, 11 | "aliases": { 12 | "components": "@/components", 13 | "utils": "@/lib/utils" 14 | } 15 | } -------------------------------------------------------------------------------- /src/azurechat/components/chat/chat-loading.tsx: -------------------------------------------------------------------------------- 1 | import { Loader } from "lucide-react"; 2 | import { FC } from "react"; 3 | 4 | interface Props {} 5 | 6 | const ChatLoading: FC = (props) => { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | }; 13 | 14 | export default ChatLoading; 15 | -------------------------------------------------------------------------------- /src/azurechat/components/hooks/use-chat-scroll-anchor.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Message } from "ai"; 4 | import { RefObject, useEffect } from "react"; 5 | 6 | export const useChatScrollAnchor = ( 7 | chats: Message[], 8 | ref: RefObject 9 | ) => { 10 | useEffect(() => { 11 | if (ref && ref.current) { 12 | ref.current.scrollTop = ref.current.scrollHeight; 13 | } 14 | }, [chats, ref]); 15 | }; 16 | -------------------------------------------------------------------------------- /src/azurechat/components/login/login.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { AI_NAME } from "@/features/theme/customise"; 3 | import { signIn } from "next-auth/react"; 4 | import { Avatar, AvatarImage } from "../ui/avatar"; 5 | import { Button } from "../ui/button"; 6 | import { 7 | Card, 8 | CardContent, 9 | CardDescription, 10 | CardHeader, 11 | CardTitle, 12 | } from "../ui/card"; 13 | 14 | export const LogIn = () => { 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | {AI_NAME} 23 | 24 | 25 | Login in with your Microsoft 365 account 26 | 27 | 28 | 29 | 30 | 31 | {process.env.NODE_ENV === "development" && ( 32 | 33 | )} 34 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/azurechat/components/markdown/code-block.tsx: -------------------------------------------------------------------------------- 1 | import { FC, memo } from "react"; 2 | import { Prism } from "react-syntax-highlighter"; 3 | import { atomDark } from "react-syntax-highlighter/dist/esm/styles/prism"; 4 | 5 | export const fence = { 6 | render: "CodeBlock", 7 | attributes: { 8 | language: { 9 | type: String, 10 | }, 11 | value: { 12 | type: String, 13 | }, 14 | }, 15 | }; 16 | 17 | interface Props { 18 | language: string; 19 | children: string; 20 | } 21 | 22 | export const CodeBlock: FC = memo(({ language, children }) => { 23 | console.log(language); 24 | return ( 25 | 26 | {children} 27 | 28 | ); 29 | }); 30 | 31 | CodeBlock.displayName = "CodeBlock"; 32 | -------------------------------------------------------------------------------- /src/azurechat/components/markdown/config.tsx: -------------------------------------------------------------------------------- 1 | import { citation } from "@/features/chat/chat-ui/markdown/citation"; 2 | import { Config } from "@markdoc/markdoc"; 3 | import { fence } from "./code-block"; 4 | import { paragraph } from "./paragraph"; 5 | 6 | export const citationConfig: Config = { 7 | nodes: { 8 | paragraph, 9 | fence, 10 | }, 11 | tags: { 12 | citation, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/azurechat/components/markdown/markdown.tsx: -------------------------------------------------------------------------------- 1 | import Markdoc from "@markdoc/markdoc"; 2 | import React, { FC } from "react"; 3 | import { Citation } from "../../features/chat/chat-ui/markdown/citation"; 4 | import { CodeBlock } from "./code-block"; 5 | import { citationConfig } from "./config"; 6 | import { Paragraph } from "./paragraph"; 7 | 8 | interface Props { 9 | content: string; 10 | } 11 | 12 | export const Markdown: FC = (props) => { 13 | const ast = Markdoc.parse(props.content); 14 | 15 | const content = Markdoc.transform(ast, { 16 | ...citationConfig, 17 | }); 18 | 19 | return Markdoc.renderers.react(content, React, { 20 | components: { Citation, Paragraph, CodeBlock }, 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /src/azurechat/components/markdown/paragraph.tsx: -------------------------------------------------------------------------------- 1 | export const Paragraph = ({ children, className }: any) => { 2 | return
{children}
; 3 | }; 4 | 5 | export const paragraph = { 6 | render: "Paragraph", 7 | }; 8 | -------------------------------------------------------------------------------- /src/azurechat/components/menu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import Link from "next/link"; 5 | 6 | const Menu = React.forwardRef< 7 | HTMLDivElement, 8 | React.HTMLAttributes 9 | >(({ className, ...props }, ref) => ( 10 |
11 | )); 12 | 13 | Menu.displayName = "Menu"; 14 | 15 | const MenuHeader = React.forwardRef< 16 | HTMLDivElement, 17 | React.HTMLAttributes 18 | >(({ className, ...props }, ref) => ( 19 |
24 | )); 25 | MenuHeader.displayName = "MenuHeader"; 26 | 27 | const MenuContent = React.forwardRef< 28 | HTMLDivElement, 29 | React.HTMLAttributes 30 | >(({ className, ...props }, ref) => ( 31 |
39 | )); 40 | MenuContent.displayName = "MenuContent"; 41 | 42 | interface MenuItemProps extends React.HTMLAttributes { 43 | href: string; 44 | isSelected?: boolean; 45 | } 46 | 47 | const MenuItem: React.FC = (props) => { 48 | return ( 49 | 57 | {props.children} 58 | 59 | ); 60 | }; 61 | 62 | const MenuFooter = React.forwardRef< 63 | HTMLDivElement, 64 | React.HTMLAttributes 65 | >(({ className, ...props }, ref) => ( 66 |
67 | )); 68 | MenuFooter.displayName = "MenuFooter"; 69 | 70 | export { Menu, MenuContent, MenuFooter, MenuHeader, MenuItem }; 71 | -------------------------------------------------------------------------------- /src/azurechat/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 4 | import { type ThemeProviderProps } from "next-themes/dist/types"; 5 | 6 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 7 | return {children}; 8 | } 9 | -------------------------------------------------------------------------------- /src/azurechat/components/typography.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import * as React from "react"; 3 | 4 | type TypographyProps = { 5 | variant: "h1" | "h2" | "h3" | "h4" | "h5" | "p"; 6 | } & React.HTMLAttributes; 7 | 8 | const Typography = React.forwardRef( 9 | function Typography({ variant, className, ...props }, ref) { 10 | const Component = variant; 11 | return ( 12 | 31 | ); 32 | } 33 | ); 34 | 35 | export default Typography; 36 | -------------------------------------------------------------------------------- /src/azurechat/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /src/azurechat/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /src/azurechat/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /src/azurechat/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /src/azurechat/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as LabelPrimitive from "@radix-ui/react-label"; 4 | import { cva, type VariantProps } from "class-variance-authority"; 5 | import * as React from "react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ); 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )); 24 | Label.displayName = LabelPrimitive.Root.displayName; 25 | 26 | export { Label }; 27 | -------------------------------------------------------------------------------- /src/azurechat/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TabsPrimitive from "@radix-ui/react-tabs" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Tabs = TabsPrimitive.Root 9 | 10 | const TabsList = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )) 23 | TabsList.displayName = TabsPrimitive.List.displayName 24 | 25 | const TabsTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 37 | )) 38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName 39 | 40 | const TabsContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | )) 53 | TabsContent.displayName = TabsPrimitive.Content.displayName 54 | 55 | export { Tabs, TabsList, TabsTrigger, TabsContent } 56 | -------------------------------------------------------------------------------- /src/azurechat/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 | 54 |
55 | {speechEnabled && } 56 | 69 |
70 |
71 | 72 | ); 73 | }; 74 | 75 | export default ChatInput; 76 | -------------------------------------------------------------------------------- /src/azurechat/features/chat/chat-ui/chat-input/use-chat-input-dynamic-height.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | interface Props { 4 | buttonRef: React.RefObject; 5 | } 6 | 7 | export const useChatInputDynamicHeight = (props: Props) => { 8 | const maxRows = 6; 9 | const [rows, setRows] = useState(1); 10 | 11 | const [keysPressed, setKeysPressed] = useState(new Set()); 12 | 13 | const onKeyUp = (event: React.KeyboardEvent) => { 14 | keysPressed.delete(event.key); 15 | setKeysPressed(keysPressed); 16 | }; 17 | 18 | const setRowsToMax = (rows: number) => { 19 | if (rows < maxRows) { 20 | setRows(rows + 1); 21 | } 22 | }; 23 | 24 | const resetRows = () => { 25 | setRows(1); 26 | }; 27 | 28 | const onChange = (event: React.ChangeEvent) => { 29 | setRowsToMax(event.target.value.split("\n").length - 1); 30 | }; 31 | 32 | const onKeyDown = (event: React.KeyboardEvent) => { 33 | setKeysPressed(keysPressed.add(event.key)); 34 | 35 | if (keysPressed.has("Enter") && keysPressed.has("Shift")) { 36 | setRowsToMax(rows + 1); 37 | } 38 | 39 | if ( 40 | !event.nativeEvent.isComposing && 41 | keysPressed.has("Enter") && 42 | !keysPressed.has("Shift") && 43 | props.buttonRef.current 44 | ) { 45 | props.buttonRef.current.click(); 46 | event.preventDefault(); 47 | } 48 | }; 49 | 50 | return { 51 | rows, 52 | resetRows, 53 | onChange, 54 | onKeyDown, 55 | onKeyUp, 56 | }; 57 | }; 58 | -------------------------------------------------------------------------------- /src/azurechat/features/chat/chat-ui/chat-message-container.tsx: -------------------------------------------------------------------------------- 1 | import ChatLoading from "@/components/chat/chat-loading"; 2 | import ChatRow from "@/components/chat/chat-row"; 3 | import { useChatScrollAnchor } from "@/components/hooks/use-chat-scroll-anchor"; 4 | import { AI_NAME } from "@/features/theme/customise"; 5 | import { useSession } from "next-auth/react"; 6 | import { useRef } from "react"; 7 | import { useChatContext } from "./chat-context"; 8 | import { ChatHeader } from "./chat-header"; 9 | 10 | export const ChatMessageContainer = () => { 11 | const { data: session } = useSession(); 12 | const scrollRef = useRef(null); 13 | 14 | const { messages, isLoading } = useChatContext(); 15 | 16 | useChatScrollAnchor(messages, scrollRef); 17 | 18 | return ( 19 |
20 |
21 | 22 |
23 |
24 | {messages.map((message, index) => ( 25 | 34 | ))} 35 | {isLoading && } 36 |
37 |
38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/azurechat/features/chat/chat-ui/chat-speech/microphone.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { useChatContext } from "../chat-context"; 3 | import { RecordSpeech } from "./record-speech"; 4 | import { StopSpeech } from "./stop-speech"; 5 | 6 | interface MicrophoneProps { 7 | disabled: boolean; 8 | } 9 | 10 | export const Microphone: FC = (props) => { 11 | const { speech } = useChatContext(); 12 | return ( 13 | <> 14 | {speech.isPlaying ? ( 15 | 16 | ) : ( 17 | 18 | )} 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/azurechat/features/chat/chat-ui/chat-speech/record-speech.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Mic } from "lucide-react"; 3 | import { FC } from "react"; 4 | import { useChatContext } from "../chat-context"; 5 | 6 | interface Prop { 7 | disabled: boolean; 8 | } 9 | 10 | export const RecordSpeech: FC = (props) => { 11 | const { speech } = useChatContext(); 12 | const { startRecognition, stopRecognition, isMicrophonePressed } = speech; 13 | 14 | const handleMouseDown = async () => { 15 | await startRecognition(); 16 | }; 17 | 18 | const handleMouseUp = () => { 19 | stopRecognition(); 20 | }; 21 | 22 | return ( 23 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/azurechat/features/chat/chat-ui/chat-speech/speech-service.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | export const GetSpeechToken = async () => { 4 | if ( 5 | process.env.AZURE_SPEECH_REGION === undefined || 6 | process.env.AZURE_SPEECH_KEY === undefined 7 | ) { 8 | return { 9 | error: true, 10 | errorMessage: "Missing Azure Speech credentials", 11 | token: "", 12 | region: "", 13 | }; 14 | } 15 | 16 | const response = await fetch( 17 | `https://${process.env.AZURE_SPEECH_REGION}.api.cognitive.microsoft.com/sts/v1.0/issueToken`, 18 | { 19 | method: "POST", 20 | headers: { 21 | "Ocp-Apim-Subscription-Key": process.env.AZURE_SPEECH_KEY!, 22 | }, 23 | cache: "no-store", 24 | } 25 | ); 26 | 27 | return { 28 | error: response.status !== 200, 29 | errorMessage: response.statusText, 30 | token: await response.text(), 31 | region: process.env.AZURE_SPEECH_REGION, 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /src/azurechat/features/chat/chat-ui/chat-speech/stop-speech.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Square } from "lucide-react"; 3 | import { FC } from "react"; 4 | import { useChatContext } from "../chat-context"; 5 | 6 | interface StopButtonProps { 7 | disabled: boolean; 8 | } 9 | 10 | export const StopSpeech: FC = (props) => { 11 | const { speech } = useChatContext(); 12 | return ( 13 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/azurechat/features/chat/chat-ui/chat-speech/use-speech-to-text.ts: -------------------------------------------------------------------------------- 1 | import { useGlobalMessageContext } from "@/features/global-message/global-message-context"; 2 | import { 3 | AudioConfig, 4 | AutoDetectSourceLanguageConfig, 5 | SpeechConfig, 6 | SpeechRecognizer, 7 | } from "microsoft-cognitiveservices-speech-sdk"; 8 | import { useRef, useState } from "react"; 9 | import { GetSpeechToken } from "./speech-service"; 10 | 11 | export interface SpeechToTextProps { 12 | startRecognition: () => void; 13 | stopRecognition: () => void; 14 | isMicrophoneUsed: boolean; 15 | resetMicrophoneUsed: () => void; 16 | isMicrophonePressed: boolean; 17 | } 18 | 19 | interface Props { 20 | onSpeech: (value: string) => void; 21 | } 22 | 23 | export const useSpeechToText = (props: Props): SpeechToTextProps => { 24 | const recognizerRef = useRef(); 25 | 26 | const [isMicrophoneUsed, setIsMicrophoneUsed] = useState(false); 27 | const [isMicrophonePressed, setIsMicrophonePressed] = useState(false); 28 | 29 | const { showError } = useGlobalMessageContext(); 30 | 31 | const startRecognition = async () => { 32 | const token = await GetSpeechToken(); 33 | 34 | if (token.error) { 35 | showError(token.errorMessage); 36 | return; 37 | } 38 | 39 | setIsMicrophoneUsed(true); 40 | setIsMicrophonePressed(true); 41 | const speechConfig = SpeechConfig.fromAuthorizationToken( 42 | token.token, 43 | token.region 44 | ); 45 | 46 | const audioConfig = AudioConfig.fromDefaultMicrophoneInput(); 47 | 48 | const autoDetectSourceLanguageConfig = 49 | AutoDetectSourceLanguageConfig.fromLanguages([ 50 | "en-US", 51 | "zh-CN", 52 | "it-IT", 53 | "pt-BR", 54 | ]); 55 | 56 | const recognizer = SpeechRecognizer.FromConfig( 57 | speechConfig, 58 | autoDetectSourceLanguageConfig, 59 | audioConfig 60 | ); 61 | 62 | recognizerRef.current = recognizer; 63 | 64 | recognizer.recognizing = (s, e) => { 65 | props.onSpeech(e.result.text); 66 | }; 67 | 68 | recognizer.canceled = (s, e) => { 69 | showError(e.errorDetails); 70 | }; 71 | 72 | recognizer.startContinuousRecognitionAsync(); 73 | }; 74 | 75 | const stopRecognition = () => { 76 | recognizerRef.current?.stopContinuousRecognitionAsync(); 77 | setIsMicrophonePressed(false); 78 | }; 79 | 80 | const resetMicrophoneUsed = () => { 81 | setIsMicrophoneUsed(false); 82 | }; 83 | 84 | return { 85 | startRecognition, 86 | stopRecognition, 87 | isMicrophoneUsed, 88 | resetMicrophoneUsed, 89 | isMicrophonePressed, 90 | }; 91 | }; 92 | -------------------------------------------------------------------------------- /src/azurechat/features/chat/chat-ui/chat-speech/use-text-to-speech.ts: -------------------------------------------------------------------------------- 1 | import { useGlobalMessageContext } from "@/features/global-message/global-message-context"; 2 | import { 3 | AudioConfig, 4 | ResultReason, 5 | SpeakerAudioDestination, 6 | SpeechConfig, 7 | SpeechSynthesizer, 8 | } from "microsoft-cognitiveservices-speech-sdk"; 9 | import { useRef, useState } from "react"; 10 | import { GetSpeechToken } from "./speech-service"; 11 | 12 | export interface TextToSpeechProps { 13 | stopPlaying: () => void; 14 | textToSpeech: (textToSpeak: string) => void; 15 | isPlaying: boolean; 16 | } 17 | 18 | export const useTextToSpeech = (): TextToSpeechProps => { 19 | const [isPlaying, setIsPlaying] = useState(false); 20 | const playerRef = useRef(); 21 | 22 | const { showError } = useGlobalMessageContext(); 23 | 24 | const stopPlaying = () => { 25 | setIsPlaying(false); 26 | if (playerRef.current) { 27 | playerRef.current.pause(); 28 | } 29 | }; 30 | 31 | const textToSpeech = async (textToSpeak: string) => { 32 | if (isPlaying) { 33 | stopPlaying(); 34 | } 35 | 36 | const tokenObj = await GetSpeechToken(); 37 | 38 | if (tokenObj.error) { 39 | showError(tokenObj.errorMessage); 40 | return; 41 | } 42 | 43 | const speechConfig = SpeechConfig.fromAuthorizationToken( 44 | tokenObj.token, 45 | tokenObj.region 46 | ); 47 | playerRef.current = new SpeakerAudioDestination(); 48 | 49 | var audioConfig = AudioConfig.fromSpeakerOutput(playerRef.current); 50 | let synthesizer = new SpeechSynthesizer(speechConfig, audioConfig); 51 | 52 | playerRef.current.onAudioEnd = () => { 53 | setIsPlaying(false); 54 | }; 55 | 56 | synthesizer.speakTextAsync( 57 | textToSpeak, 58 | (result) => { 59 | if (result.reason === ResultReason.SynthesizingAudioCompleted) { 60 | setIsPlaying(true); 61 | } else { 62 | showError(result.errorDetails); 63 | setIsPlaying(false); 64 | } 65 | synthesizer.close(); 66 | }, 67 | function (err) { 68 | console.log("err - " + err); 69 | synthesizer.close(); 70 | } 71 | ); 72 | }; 73 | 74 | return { stopPlaying, textToSpeech, isPlaying }; 75 | }; 76 | -------------------------------------------------------------------------------- /src/azurechat/features/chat/chat-ui/chat-ui.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FC } from "react"; 4 | import { useChatContext } from "./chat-context"; 5 | import { ChatMessageEmptyState } from "./chat-empty-state/chat-message-empty-state"; 6 | import ChatInput from "./chat-input/chat-input"; 7 | import { ChatMessageContainer } from "./chat-message-container"; 8 | import { DepartmentConfig, DeploymentConfig } from "../chat-services/models"; 9 | 10 | interface Prop { 11 | deployments: DeploymentConfig[]; 12 | departments: DepartmentConfig[]; 13 | } 14 | 15 | export const ChatUI: FC = (props) => { 16 | const { messages } = useChatContext(); 17 | 18 | return ( 19 |
20 | {messages.length !== 0 ? ( 21 | 22 | ) : ( 23 | 24 | )} 25 | 26 | 27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/azurechat/features/chat/chat-ui/markdown/citation-action.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { simpleSearch } from "@/features/chat/chat-services/azure-cog-search/azure-cog-vector-store"; 4 | 5 | export const CitationAction = async ( 6 | previousState: any, 7 | formData: FormData 8 | ) => { 9 | const result = await simpleSearch({ 10 | filter: `id eq '${formData.get("id")}'`, 11 | }); 12 | 13 | if (result.length === 0) return
Not found
; 14 | 15 | const firstResult = result[0]; 16 | 17 | return ( 18 |
19 |
20 |
Idd
21 |
{firstResult.id}
22 |
23 |
24 |
File name
25 |
{firstResult.metadata}
26 |
27 |

{firstResult.pageContent}

28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/azurechat/features/chat/chat-ui/markdown/citation-slider.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { 3 | Sheet, 4 | SheetContent, 5 | SheetHeader, 6 | SheetTitle, 7 | SheetTrigger, 8 | } from "@/components/ui/sheet"; 9 | import { FC } from "react"; 10 | import { useFormState } from "react-dom"; 11 | import { CitationAction } from "./citation-action"; 12 | 13 | interface SliderProps { 14 | name: string; 15 | index: number; 16 | id: string; 17 | } 18 | 19 | export const CitationSlider: FC = (props) => { 20 | const [node, formAction] = useFormState(CitationAction, null); 21 | return ( 22 |
23 | 24 | 25 | 26 | 35 | 36 | 37 | 38 | Citation 39 | 40 |
{node}
41 |
42 |
43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/azurechat/features/chat/chat-ui/markdown/citation.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { FC } from "react"; 3 | import { CitationSlider } from "./citation-slider"; 4 | 5 | interface Citation { 6 | name: string; 7 | id: string; 8 | } 9 | 10 | interface Props { 11 | items: Citation[]; 12 | } 13 | 14 | export const citation = { 15 | render: "Citation", 16 | selfClosing: true, 17 | attributes: { 18 | items: { 19 | type: Array, 20 | }, 21 | }, 22 | }; 23 | 24 | export const Citation: FC = (props: Props) => { 25 | // group citations by name 26 | const citations = props.items.reduce((acc, citation) => { 27 | const { name } = citation; 28 | if (!acc[name]) { 29 | acc[name] = []; 30 | } 31 | acc[name].push(citation); 32 | return acc; 33 | }, {} as Record); 34 | 35 | return ( 36 |
37 | {Object.entries(citations).map(([name, items], index: number) => { 38 | return ( 39 |
40 |
{name}
41 |
42 | {items.map((item, index: number) => { 43 | return ( 44 |
45 | 50 |
51 | ); 52 | })} 53 |
54 |
55 | ); 56 | })} 57 |
58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /src/azurechat/features/common/appconfig.ts: -------------------------------------------------------------------------------- 1 | import { AppConfigurationClient } from "@azure/app-configuration" 2 | import { DepartmentConfig, DeploymentConfig } from "../chat/chat-services/models"; 3 | import { GetCredential } from "./managedIdentity"; 4 | 5 | const appConfig = (): AppConfigurationClient => { 6 | const credential = GetCredential(); 7 | const configurationEndpoint = `${process.env.APPCONFIG_ENDPOINT}` 8 | 9 | const client = new AppConfigurationClient( 10 | configurationEndpoint, 11 | credential 12 | ); 13 | 14 | return client; 15 | } 16 | 17 | const appConfigStore = appConfig(); 18 | 19 | export async function GetDeployments() { 20 | const deploymentsJson = await appConfigStore.getConfigurationSetting({ 21 | key: "AzureChat:Deployments" 22 | }); 23 | 24 | const deployments = JSON.parse(deploymentsJson.value as string) as DeploymentConfig[]; 25 | return deployments; 26 | } 27 | 28 | export async function GetDepartments() { 29 | const departmentsJson = await appConfigStore.getConfigurationSetting({ 30 | key: "AzureChat:Departments" 31 | }); 32 | 33 | const departments = JSON.parse(departmentsJson.value as string) as DepartmentConfig[]; 34 | return departments; 35 | } 36 | 37 | export async function GetSingleValue(key: string) { 38 | const singleValue = await appConfigStore.getConfigurationSetting({ 39 | key: key 40 | }); 41 | 42 | return singleValue.value as string; 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/azurechat/features/common/cosmos.ts: -------------------------------------------------------------------------------- 1 | import { Container, CosmosClient } from "@azure/cosmos"; 2 | import { GetSingleValue } from "./appconfig"; 3 | import { GetCredential } from "./managedIdentity"; 4 | 5 | 6 | const DB_NAME = "chat"; 7 | const CONTAINER_NAME = "history"; 8 | 9 | const credential = GetCredential(); 10 | 11 | 12 | export const initDBContainer = async () => { 13 | 14 | const endpoint = await GetSingleValue("AzureChat:CosmosDbEndPoint"); 15 | 16 | const client = new CosmosClient({ 17 | endpoint, 18 | aadCredentials: credential 19 | }); 20 | 21 | const database = (await client.database(DB_NAME).read()).database; 22 | const container = (await database.container(CONTAINER_NAME).read()).container; 23 | 24 | return container; 25 | }; 26 | 27 | export class CosmosDBContainer { 28 | private static instance: CosmosDBContainer; 29 | private container: Promise; 30 | 31 | private constructor(endpoint: string) { 32 | 33 | const client = new CosmosClient({ 34 | endpoint, 35 | aadCredentials: credential 36 | }); 37 | 38 | this.container = new Promise((resolve, reject) => { 39 | client.database(DB_NAME).read() 40 | .then((databaseResponse) => { 41 | databaseResponse.database.container(CONTAINER_NAME).read() 42 | .then((containerResponse) => { 43 | resolve(containerResponse.container); 44 | }); 45 | }) 46 | .catch((err) => { 47 | reject(err); 48 | }); 49 | }); 50 | } 51 | 52 | public static async getInstance(): Promise { 53 | if (!CosmosDBContainer.instance) { 54 | const endpoint = await GetSingleValue("AzureChat:CosmosDbEndPoint"); 55 | 56 | CosmosDBContainer.instance = new CosmosDBContainer(endpoint); 57 | } 58 | 59 | return CosmosDBContainer.instance; 60 | } 61 | 62 | public async getContainer(): Promise { 63 | return await this.container; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/azurechat/features/common/keyvault.ts: -------------------------------------------------------------------------------- 1 | import { SecretClient } from "@azure/keyvault-secrets"; 2 | import { GetSingleValue } from "./appconfig"; 3 | import { GetCredential } from "./managedIdentity"; 4 | 5 | 6 | 7 | const keyVault = (url: string): SecretClient => { 8 | const credential = GetCredential(); 9 | const client = new SecretClient( 10 | url, 11 | credential 12 | ); 13 | return client; 14 | } 15 | 16 | export async function GetAPIKey(department: string) { 17 | const url = await GetSingleValue("AzureChat:Keyvault"); 18 | const client = keyVault(url); 19 | const apiKey = await client.getSecret(department); 20 | return apiKey.value; 21 | } 22 | 23 | export async function GetKey(keyName: string) { 24 | const url = await GetSingleValue("AzureChat:Keyvault"); 25 | const client = keyVault(url); 26 | const key = await client.getSecret(keyName); 27 | return key.value as string; 28 | } -------------------------------------------------------------------------------- /src/azurechat/features/common/managedIdentity.ts: -------------------------------------------------------------------------------- 1 | import { DefaultAzureCredential } from "@azure/identity" 2 | 3 | export function GetCredential() { 4 | return new DefaultAzureCredential(); 5 | } -------------------------------------------------------------------------------- /src/azurechat/features/common/openai.ts: -------------------------------------------------------------------------------- 1 | import { OpenAI } from "openai"; 2 | 3 | export const OpenAIInstance = (apiKey : string, apiVersion: string) => { 4 | const openai = new OpenAI({ 5 | apiKey: apiKey, 6 | defaultQuery: { "api-version": apiVersion }, 7 | defaultHeaders: { "api-key": apiKey }, 8 | }); 9 | return openai; 10 | }; 11 | 12 | export const OpenAIEmbeddingInstance = (apiKey: string, apiVersion: string) => { 13 | const openai = new OpenAI({ 14 | apiKey: apiKey, 15 | defaultQuery: { "api-version": apiVersion }, 16 | defaultHeaders: { "api-key": apiKey }, 17 | }); 18 | return openai; 19 | }; 20 | -------------------------------------------------------------------------------- /src/azurechat/features/common/util.ts: -------------------------------------------------------------------------------- 1 | import { customAlphabet } from "nanoid"; 2 | 3 | export const uniqueId = () => { 4 | const alphabet = 5 | "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 6 | const nanoid = customAlphabet(alphabet, 36); 7 | return nanoid(); 8 | }; 9 | -------------------------------------------------------------------------------- /src/azurechat/features/global-config/global-client-config-context.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { createContext, useContext } from "react"; 3 | 4 | interface GlobalConfigProps { 5 | speechEnabled: boolean; 6 | } 7 | 8 | const GlobalConfigContext = createContext(null); 9 | 10 | interface IConfig { 11 | speechEnabled?: string; 12 | } 13 | 14 | export const GlobalConfigProvider = ({ 15 | config, 16 | children, 17 | }: { 18 | config: IConfig; 19 | children: React.ReactNode; 20 | }) => { 21 | const speechEnabled = config.speechEnabled 22 | ? config.speechEnabled === "true" 23 | : false; 24 | 25 | return ( 26 | 31 | {children} 32 | 33 | ); 34 | }; 35 | 36 | export const useGlobalConfigContext = () => { 37 | const context = useContext(GlobalConfigContext); 38 | if (!context) { 39 | throw new Error("GlobalConfigContext is null"); 40 | } 41 | 42 | return context; 43 | }; 44 | -------------------------------------------------------------------------------- /src/azurechat/features/global-message/global-message-context.tsx: -------------------------------------------------------------------------------- 1 | import { toast } from "@/components/ui/use-toast"; 2 | import { ToastAction } from "@radix-ui/react-toast"; 3 | import { createContext, useContext } from "react"; 4 | 5 | interface GlobalMessageProps { 6 | showError: (error: string, reload?: () => void) => void; 7 | showSuccess: (message: MessageProp) => void; 8 | } 9 | 10 | const GlobalMessageContext = createContext(null); 11 | 12 | interface MessageProp { 13 | title: string; 14 | description: string; 15 | } 16 | 17 | export const GlobalMessageProvider = ({ 18 | children, 19 | }: { 20 | children: React.ReactNode; 21 | }) => { 22 | const showError = (error: string, reload?: () => void) => { 23 | toast({ 24 | variant: "destructive", 25 | description: error, 26 | action: reload ? ( 27 | { 30 | reload(); 31 | }} 32 | > 33 | Try again 34 | 35 | ) : undefined, 36 | }); 37 | }; 38 | 39 | const showSuccess = (message: MessageProp) => { 40 | toast(message); 41 | }; 42 | 43 | return ( 44 | 50 | {children} 51 | 52 | ); 53 | }; 54 | 55 | export const useGlobalMessageContext = () => { 56 | const context = useContext(GlobalMessageContext); 57 | if (!context) { 58 | throw new Error("GlobalErrorContext is null"); 59 | } 60 | 61 | return context; 62 | }; 63 | -------------------------------------------------------------------------------- /src/azurechat/features/loading-skeleton.tsx: -------------------------------------------------------------------------------- 1 | export const LoadingSkeleton = () => { 2 | return ( 3 |
4 | 11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/azurechat/features/main-menu/menu-context.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useState } from "react"; 2 | 3 | interface MenuContextProps { 4 | isMenuOpen: boolean; 5 | toggleMenu: () => void; 6 | } 7 | 8 | export const MenuContext = createContext({ 9 | isMenuOpen: true, 10 | toggleMenu: () => {}, 11 | }); 12 | 13 | export const MenuProvider = ({ children }: { children: React.ReactNode }) => { 14 | const [isMenuOpen, setIsMenuOpen] = useState(true); 15 | 16 | const toggleMenu = () => { 17 | setIsMenuOpen(!isMenuOpen); 18 | }; 19 | 20 | return ( 21 | 22 | {children} 23 | 24 | ); 25 | }; 26 | 27 | export const useMenuContext = () => useContext(MenuContext); 28 | -------------------------------------------------------------------------------- /src/azurechat/features/main-menu/menu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | LayoutDashboard, 6 | MessageCircle, 7 | PanelLeftClose, 8 | PanelRightClose, 9 | Triangle, 10 | } from "lucide-react"; 11 | import Link from "next/link"; 12 | import { ThemeToggle } from "../theme/theme-toggle"; 13 | import { UserProfile } from "../user-profile"; 14 | 15 | import { useSession } from "next-auth/react"; 16 | import { UpdateIndicator } from "../change-log/update-indicator"; 17 | import { useMenuContext } from "./menu-context"; 18 | 19 | export const MainMenu = () => { 20 | const { data: session } = useSession(); 21 | const { isMenuOpen, toggleMenu } = useMenuContext(); 22 | return ( 23 |
24 |
25 | 32 | 41 | 50 | {session?.user?.isAdmin ? ( 51 | 60 | ) : ( 61 | <> 62 | )} 63 | 73 |
74 |
75 | 76 | 77 |
78 |
79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /src/azurechat/features/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SessionProvider } from "next-auth/react"; 4 | import { GlobalMessageProvider } from "./global-message/global-message-context"; 5 | import { MenuProvider } from "./main-menu/menu-context"; 6 | 7 | export const Providers = ({ children }: { children: React.ReactNode }) => { 8 | return ( 9 | 10 | 11 | {children} 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/azurechat/features/reporting/chat-reporting-ui.tsx: -------------------------------------------------------------------------------- 1 | import ChatRow from "@/components/chat/chat-row"; 2 | import { Card } from "@/components/ui/card"; 3 | import { FC } from "react"; 4 | import { AI_NAME } from "../theme/customise"; 5 | import { FindAllChatsInThread, FindChatThreadByID } from "./reporting-service"; 6 | import { ChatMessageModel } from "../chat/chat-services/models"; 7 | 8 | interface Props { 9 | chatId: string; 10 | } 11 | 12 | export const ChatReportingUI: FC = async (props) => { 13 | const chatThreads = await FindChatThreadByID(props.chatId); 14 | const chats = await FindAllChatsInThread(props.chatId); 15 | const chatThread = chatThreads[0]; 16 | 17 | return ( 18 | 19 |
20 |
21 |
22 | {chats.map((message: ChatMessageModel, index: number) => ( 23 | 30 | ))} 31 |
32 |
33 |
34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/azurechat/features/reporting/reporting-service.ts: -------------------------------------------------------------------------------- 1 | import { SqlQuerySpec } from "@azure/cosmos"; 2 | import { 3 | CHAT_THREAD_ATTRIBUTE, 4 | ChatMessageModel, 5 | ChatThreadModel, 6 | MESSAGE_ATTRIBUTE, 7 | } from "../chat/chat-services/models"; 8 | import { CosmosDBContainer } from "../common/cosmos"; 9 | 10 | export const FindAllChatThreadsForReporting = async ( 11 | pageSize = 10, 12 | pageNumber = 0 13 | ) => { 14 | const instance = await CosmosDBContainer.getInstance(); 15 | const container = await instance.getContainer(); 16 | 17 | const querySpec: SqlQuerySpec = { 18 | query: `SELECT * FROM root r WHERE r.type=@type ORDER BY r.createdAt DESC OFFSET ${ 19 | pageNumber * pageSize 20 | } LIMIT ${pageSize}`, 21 | parameters: [ 22 | { 23 | name: "@type", 24 | value: CHAT_THREAD_ATTRIBUTE, 25 | }, 26 | ], 27 | }; 28 | 29 | const { resources } = await container.items 30 | .query(querySpec, { 31 | maxItemCount: pageSize, 32 | }) 33 | .fetchNext(); 34 | return { resources }; 35 | }; 36 | 37 | export const FindChatThreadByID = async (chatThreadID: string) => { 38 | const instance = await CosmosDBContainer.getInstance(); 39 | const container = await instance.getContainer(); 40 | 41 | 42 | const querySpec: SqlQuerySpec = { 43 | query: "SELECT * FROM root r WHERE r.type=@type AND r.id=@id", 44 | parameters: [ 45 | { 46 | name: "@type", 47 | value: CHAT_THREAD_ATTRIBUTE, 48 | }, 49 | 50 | { 51 | name: "@id", 52 | value: chatThreadID, 53 | }, 54 | ], 55 | }; 56 | 57 | const { resources } = await container.items 58 | .query(querySpec) 59 | .fetchAll(); 60 | 61 | return resources; 62 | }; 63 | 64 | export const FindAllChatsInThread = async (chatThreadID: string) => { 65 | const instance = await CosmosDBContainer.getInstance(); 66 | const container = await instance.getContainer(); 67 | 68 | const querySpec: SqlQuerySpec = { 69 | query: "SELECT * FROM root r WHERE r.type=@type AND r.threadId = @threadId", 70 | parameters: [ 71 | { 72 | name: "@type", 73 | value: MESSAGE_ATTRIBUTE, 74 | }, 75 | { 76 | name: "@threadId", 77 | value: chatThreadID, 78 | }, 79 | ], 80 | }; 81 | const { resources } = await container.items 82 | .query(querySpec) 83 | .fetchAll(); 84 | return resources; 85 | }; 86 | -------------------------------------------------------------------------------- /src/azurechat/features/theme/customise.ts: -------------------------------------------------------------------------------- 1 | export const AI_NAME = "Enterprise AzureAI Chat Demo"; 2 | -------------------------------------------------------------------------------- /src/azurechat/features/theme/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Laptop2, Moon, Sun } from "lucide-react"; 4 | import { useTheme } from "next-themes"; 5 | 6 | import { Tabs, TabsList, TabsTrigger } from "../../components/ui/tabs"; 7 | 8 | export function ThemeToggle() { 9 | const { setTheme, theme } = useTheme(); 10 | return ( 11 | 15 | 16 | setTheme("light")} 19 | className="h-[40px] w-[40px] rounded-full" 20 | > 21 | 22 | 23 | setTheme("dark")} 26 | className="h-[40px] w-[40px] rounded-full" 27 | > 28 | 29 | 30 | setTheme("system")} 33 | className="h-[40px] w-[40px] rounded-full" 34 | > 35 | 36 | 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/azurechat/features/user-profile.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Avatar, AvatarImage } from "@/components/ui/avatar"; 4 | import { Button } from "@/components/ui/button"; 5 | import { 6 | DropdownMenu, 7 | DropdownMenuContent, 8 | DropdownMenuItem, 9 | DropdownMenuLabel, 10 | DropdownMenuSeparator, 11 | DropdownMenuTrigger, 12 | } from "@/components/ui/dropdown-menu"; 13 | import { LogOut, UserCircle } from "lucide-react"; 14 | import { signOut, useSession } from "next-auth/react"; 15 | 16 | const UserProfile = () => { 17 | const { data: session } = useSession(); 18 | return ( 19 | 20 | 21 |
22 | 37 |
38 |
39 | 40 | 41 |
42 |

43 | {session?.user?.name} 44 |

45 |

46 | {session?.user?.email} 47 |

48 |

49 | {session?.user?.isAdmin ? "Admin" : ""} 50 |

51 |
52 |
53 | 54 | signOut({callbackUrl: '/' })}> 55 | 56 | Log out 57 | 58 |
59 |
60 | ); 61 | }; 62 | 63 | export { UserProfile }; 64 | -------------------------------------------------------------------------------- /src/azurechat/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/azurechat/middleware.ts: -------------------------------------------------------------------------------- 1 | import { getToken } from "next-auth/jwt"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | 4 | const requireAuth: string[] = ["/chat", "/api","/reporting", "/unauthorized"]; 5 | const requireAdmin: string[] = ["/reporting"]; 6 | 7 | 8 | export async function middleware(request: NextRequest) { 9 | 10 | const res = NextResponse.next(); 11 | const pathname = request.nextUrl.pathname; 12 | 13 | if (requireAuth.some((path) => pathname.startsWith(path))) { 14 | 15 | const token = await getToken({ 16 | req: request 17 | }); 18 | 19 | //check not logged in 20 | if (!token) { 21 | const url = new URL(`/`, request.url); 22 | return NextResponse.redirect(url); 23 | } 24 | 25 | if (requireAdmin.some((path) => pathname.startsWith(path))) { 26 | //check if not authorized 27 | if (!token.isAdmin) { 28 | const url = new URL(`/unauthorized`, request.url); 29 | return NextResponse.rewrite(url); 30 | } 31 | } 32 | } 33 | 34 | return res; 35 | } 36 | 37 | // note that middleware is not applied to api/auth as this is required to logon (i.e. requires anon access) 38 | export const config = { matcher: ["/chat/:path*", "/reporting/:path*", "/api/chat:path*"] }; 39 | -------------------------------------------------------------------------------- /src/azurechat/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | output: "standalone", 4 | }; 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /src/azurechat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "azure-open-ai-accelerator", 3 | "version": "1.2.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@azure/ai-form-recognizer": "^5.0.0", 13 | "@azure/app-configuration": "^1.5.0", 14 | "@azure/cosmos": "^4.0.0", 15 | "@azure/identity": "^4.2.1", 16 | "@azure/keyvault-secrets": "^4.7.0", 17 | "@azure/msal-node": "^2.9.2", 18 | "@markdoc/markdoc": "^0.3.4", 19 | "@radix-ui/react-avatar": "^1.0.4", 20 | "@radix-ui/react-dialog": "^1.0.5", 21 | "@radix-ui/react-dropdown-menu": "^2.0.6", 22 | "@radix-ui/react-label": "^2.0.2", 23 | "@radix-ui/react-select": "^2.0.0", 24 | "@radix-ui/react-slot": "^1.0.2", 25 | "@radix-ui/react-tabs": "^1.0.4", 26 | "@radix-ui/react-toast": "^1.1.5", 27 | "@tailwindcss/typography": "^0.5.10", 28 | "@types/node": "^20.8.9", 29 | "@types/react": "^18.2.33", 30 | "@types/react-dom": "^18.2.14", 31 | "ai": "^2.2.20", 32 | "autoprefixer": "^10.4.16", 33 | "class-variance-authority": "^0.7.0", 34 | "clsx": "^2.0.0", 35 | "eslint": "^8.52.0", 36 | "eslint-config-next": "^14.0.0", 37 | "lucide-react": "^0.290.0", 38 | "microsoft-cognitiveservices-speech-sdk": "^1.32.0", 39 | "nanoid": "^5.0.2", 40 | "next": "^14.1.1", 41 | "next-auth": "^4.24.4", 42 | "next-themes": "^0.2.1", 43 | "openai": "^4.14.2", 44 | "postcss": "^8.4.31", 45 | "react": "^18.2.0", 46 | "react-dom": "^18.2.0", 47 | "react-syntax-highlighter": "^15.5.0", 48 | "tailwind-merge": "^2.0.0", 49 | "tailwindcss": "^3.3.5", 50 | "tailwindcss-animate": "^1.0.7", 51 | "typescript": "^5.2.2" 52 | }, 53 | "devDependencies": { 54 | "@types/prismjs": "^1.26.2", 55 | "@types/react-syntax-highlighter": "^15.5.7" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/azurechat/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/azurechat/public/ai-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/enterprise-azureai/3e6c97fa4e0a04d26a378280a20b7e749fa5c013/src/azurechat/public/ai-icon.png -------------------------------------------------------------------------------- /src/azurechat/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: [ 5 | "./pages/**/*.{ts,tsx}", 6 | "./components/**/*.{ts,tsx}", 7 | "./app/**/*.{ts,tsx}", 8 | "./src/**/*.{ts,tsx}", 9 | "./features/**/*.{ts,tsx}", 10 | ], 11 | theme: { 12 | container: { 13 | center: true, 14 | padding: "2rem", 15 | screens: { 16 | "2xl": "1400px", 17 | }, 18 | }, 19 | extend: { 20 | colors: { 21 | brand: "hsl(var(--brand))", 22 | border: "hsl(var(--border))", 23 | input: "hsl(var(--input))", 24 | ring: "hsl(var(--ring))", 25 | background: "hsl(var(--background))", 26 | foreground: "hsl(var(--foreground))", 27 | primary: { 28 | DEFAULT: "hsl(var(--primary))", 29 | foreground: "hsl(var(--primary-foreground))", 30 | }, 31 | secondary: { 32 | DEFAULT: "hsl(var(--secondary))", 33 | foreground: "hsl(var(--secondary-foreground))", 34 | }, 35 | destructive: { 36 | DEFAULT: "hsl(var(--destructive))", 37 | foreground: "hsl(var(--destructive-foreground))", 38 | }, 39 | muted: { 40 | DEFAULT: "hsl(var(--muted))", 41 | foreground: "hsl(var(--muted-foreground))", 42 | }, 43 | accent: { 44 | DEFAULT: "hsl(var(--accent))", 45 | foreground: "hsl(var(--accent-foreground))", 46 | }, 47 | popover: { 48 | DEFAULT: "hsl(var(--popover))", 49 | foreground: "hsl(var(--popover-foreground))", 50 | }, 51 | card: { 52 | DEFAULT: "hsl(var(--card))", 53 | foreground: "hsl(var(--card-foreground))", 54 | }, 55 | }, 56 | borderRadius: { 57 | lg: "var(--radius)", 58 | md: "calc(var(--radius) - 2px)", 59 | sm: "calc(var(--radius) - 4px)", 60 | }, 61 | keyframes: { 62 | "accordion-down": { 63 | from: { height: 0 }, 64 | to: { height: "var(--radix-accordion-content-height)" }, 65 | }, 66 | "accordion-up": { 67 | from: { height: "var(--radix-accordion-content-height)" }, 68 | to: { height: 0 }, 69 | }, 70 | }, 71 | animation: { 72 | "accordion-down": "accordion-down 0.2s ease-out", 73 | "accordion-up": "accordion-up 0.2s ease-out", 74 | }, 75 | }, 76 | }, 77 | plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")], 78 | }; 79 | -------------------------------------------------------------------------------- /src/azurechat/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "typeRoots": ["./types", "./node_modules/@types"], 18 | "plugins": [ 19 | { 20 | "name": "next" 21 | } 22 | ], 23 | "paths": { 24 | "@/*": ["./*"] 25 | } 26 | }, 27 | "include": ["src/next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 28 | "exclude": ["node_modules"] 29 | } 30 | -------------------------------------------------------------------------------- /src/azurechat/type.ts: -------------------------------------------------------------------------------- 1 | const azureEnvVars = [ 2 | "OPENAI_API_KEY", 3 | "AZURE_OPENAI_API_INSTANCE_NAME", 4 | "AZURE_OPENAI_API_DEPLOYMENT_NAME", 5 | "AZURE_OPENAI_API_VERSION", 6 | "AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME", 7 | "AZURE_COSMOSDB_URI", 8 | "AZURE_COSMOSDB_KEY", 9 | "AZURE_COSMOSDB_DB_NAME", 10 | "AZURE_COSMOSDB_CONTAINER_NAME", 11 | "AZURE_SEARCH_API_KEY", 12 | "AZURE_SEARCH_NAME", 13 | "AZURE_SEARCH_INDEX_NAME", 14 | "AZURE_SEARCH_API_VERSION", 15 | "AUTH_GITHUB_ID", 16 | "AUTH_GITHUB_SECRET", 17 | "AZURE_AD_CLIENT_ID", 18 | "AZURE_AD_CLIENT_SECRET", 19 | "AZURE_AD_TENANT_ID", 20 | "NEXTAUTH_SECRET", 21 | "NEXTAUTH_URL", 22 | "AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT", 23 | "AZURE_DOCUMENT_INTELLIGENCE_KEY", 24 | "ADMIN_EMAIL_ADDRESS", 25 | "AZURE_SPEECH_REGION", 26 | "AZURE_SPEECH_KEY", 27 | "PUBLIC_PUBLIC_SPEECH_ENABLED", 28 | ] as const; 29 | 30 | type RequiredServerEnvKeys = (typeof azureEnvVars)[number]; 31 | 32 | declare global { 33 | namespace NodeJS { 34 | interface ProcessEnv extends Record {} 35 | } 36 | } 37 | 38 | export {}; 39 | -------------------------------------------------------------------------------- /src/azurechat/types/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { DefaultSession } from "next-auth" 2 | 3 | // https://next-auth.js.org/getting-started/typescript#module-augmentation 4 | 5 | declare module "next-auth" { 6 | 7 | interface Session { 8 | user: { 9 | isAdmin: string 10 | } & DefaultSession["user"] 11 | } 12 | 13 | interface User { 14 | isAdmin: string 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/dotnet/AzureAI.Proxy.Client/AzureAI.Proxy.Client.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 892c5923-55d5-4808-bb0d-8304cb06be3d 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Always 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/dotnet/AzureAI.Proxy.Client/Program.cs: -------------------------------------------------------------------------------- 1 | using Azure; 2 | using Azure.AI.OpenAI; 3 | using Microsoft.Extensions.Configuration; 4 | 5 | IConfigurationRoot config = new ConfigurationBuilder() 6 | .AddJsonFile("appsettings.json") 7 | .AddUserSecrets() 8 | .Build(); 9 | 10 | var proxyEndpoint = config["ProxyEndPoint"]; 11 | var apiKey = config["APIKey"]; 12 | 13 | OpenAIClient client = new OpenAIClient( 14 | new Uri(proxyEndpoint), 15 | new AzureKeyCredential(apiKey) 16 | ); 17 | 18 | 19 | var deploymentName = "gpt-35-turbo"; 20 | var chatMessages = new List(); 21 | 22 | var systemChatMessage = new ChatRequestSystemMessage("You are a helpful AI Assistant"); 23 | var userChatMessage = new ChatRequestUserMessage("When was Microsoft Founded and what info can you give me on the founders in a maximum of 100 words"); 24 | 25 | 26 | chatMessages.Add(systemChatMessage); 27 | chatMessages.Add(userChatMessage); 28 | 29 | ChatCompletionsOptions completionOptions = new ChatCompletionsOptions(deploymentName, chatMessages); 30 | Console.WriteLine($"Using endpoint: {proxyEndpoint}"); 31 | 32 | //run the loop to hit rate-limiter 33 | for (int i = 0; i < 7; i++) 34 | { 35 | 36 | Console.WriteLine("Get answer to question: " + userChatMessage.Content); 37 | 38 | var response = await client.GetChatCompletionsAsync(completionOptions); 39 | 40 | Console.WriteLine("Get Chat Completion Result"); 41 | foreach (ChatChoice choice in response.Value.Choices) 42 | { 43 | Console.WriteLine(choice.Message.Content); 44 | } 45 | 46 | } 47 | //end loop 48 | 49 | 50 | Console.WriteLine("Get StreamingChat Completion Result"); 51 | await foreach (StreamingChatCompletionsUpdate chatUpdate in client.GetChatCompletionsStreaming(completionOptions)) 52 | { 53 | 54 | if (!string.IsNullOrEmpty(chatUpdate.ContentUpdate)) 55 | { 56 | Console.Write(chatUpdate.ContentUpdate); 57 | } 58 | } 59 | Console.WriteLine(); 60 | 61 | 62 | 63 | //embedding 64 | string embeddingDeploymentName = "text-embedding-ada-002"; 65 | List embeddingText = new List(); 66 | embeddingText.Add("When was Microsoft Founded?"); 67 | 68 | var embeddingsOptions = new EmbeddingsOptions(embeddingDeploymentName, embeddingText); 69 | var embeddings = await client.GetEmbeddingsAsync(embeddingsOptions); 70 | Console.WriteLine("Get Embeddings Result"); 71 | foreach (float item in embeddings.Value.Data[0].Embedding.ToArray()) 72 | { 73 | Console.WriteLine(item); 74 | } 75 | 76 | 77 | 78 | 79 | 80 | Console.ReadLine(); 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/dotnet/AzureAI.Proxy.Client/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ProxyendPoint": "http://localhost:5227", 3 | "APIKey": "" 4 | } 5 | -------------------------------------------------------------------------------- /src/dotnet/AzureAI.Proxy/AzureAI.Proxy.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | true 8 | Linux 9 | 011a4cc4-5fea-459f-8970-b545b7d995b8 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/dotnet/AzureAI.Proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | 4 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build 5 | ARG BUILD_CONFIGURATION=Release 6 | WORKDIR /src 7 | COPY ["AzureAI.Proxy/AzureAI.Proxy.csproj", "AzureAI.Proxy/"] 8 | RUN dotnet restore "./AzureAI.Proxy/./AzureAI.Proxy.csproj" 9 | COPY . . 10 | WORKDIR "/src/AzureAI.Proxy" 11 | RUN dotnet build "./AzureAI.Proxy.csproj" -c $BUILD_CONFIGURATION -o /app/build 12 | 13 | FROM build AS publish 14 | ARG BUILD_CONFIGURATION=Release 15 | RUN dotnet publish "./AzureAI.Proxy.csproj" -c $BUILD_CONFIGURATION -o /app/publish 16 | 17 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final 18 | WORKDIR /app 19 | EXPOSE 8080 20 | COPY --from=publish /app/publish . 21 | ENTRYPOINT ["dotnet","./AzureAI.Proxy.dll"] -------------------------------------------------------------------------------- /src/dotnet/AzureAI.Proxy/Models/LogAnalyticsRecord.cs: -------------------------------------------------------------------------------- 1 | namespace AzureAI.Proxy; 2 | public class LogAnalyticsRecord 3 | { 4 | public DateTime TimeGenerated { get; set; } 5 | public string Consumer { get; set; } 6 | public string Model { get; set; } 7 | public string ObjectType { get; set; } 8 | public int InputTokens { get; set; } 9 | public int OutputTokens { get; set; } 10 | public int TotalTokens { get; set; } 11 | } -------------------------------------------------------------------------------- /src/dotnet/AzureAI.Proxy/Models/ProxyConfig.cs: -------------------------------------------------------------------------------- 1 | namespace AzureAI.Proxy.Models; 2 | 3 | 4 | public class ProxyConfig 5 | { 6 | public List Routes { get; set; } 7 | } 8 | 9 | public class Route 10 | { 11 | public string Name { get; set; } 12 | public List Endpoints { get; set; } 13 | } 14 | 15 | public class Endpoint 16 | { 17 | public string Address { get; set; } 18 | public int Priority { get; set; } 19 | } 20 | -------------------------------------------------------------------------------- /src/dotnet/AzureAI.Proxy/OpenAIHandlers/ChatCompletionChunck.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | 3 | namespace AzureAI.Proxy.OpenAIHandlers; 4 | 5 | public static class ChatCompletionChunck 6 | { 7 | public static void Handle(JsonNode jsonNode, ref LogAnalyticsRecord record) 8 | { 9 | //calculate tokens based on the content...we need a tokenizer to calculate 10 | var modelName = jsonNode["model"].ToString(); 11 | record.Model = modelName; 12 | var choices = jsonNode["choices"]; 13 | var delta = choices[0]["delta"]; 14 | var content = delta["content"]; 15 | //calculate tokens used 16 | if (content != null) 17 | { 18 | record.OutputTokens += Tokens.GetTokensFromString(content.ToString(), modelName); 19 | 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/dotnet/AzureAI.Proxy/OpenAIHandlers/Error.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | 3 | namespace AzureAI.Proxy.OpenAIHandlers; 4 | 5 | public static class Error 6 | { 7 | public static void Handle(JsonNode jsonNode) 8 | { 9 | //nothing yet, figure out later 10 | return; 11 | 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/dotnet/AzureAI.Proxy/OpenAIHandlers/OpenAIAccessToken.cs: -------------------------------------------------------------------------------- 1 | using Azure.Core; 2 | using System.IdentityModel.Tokens.Jwt; 3 | 4 | namespace AzureAI.Proxy.OpenAIHandlers; 5 | 6 | public static class OpenAIAccessToken 7 | { 8 | private const string OPENAI_SCOPE = "https://cognitiveservices.azure.com/.default"; 9 | 10 | public async static Task GetAccessTokenAsync(TokenCredential managedIdenitityCredential, CancellationToken cancellationToken) 11 | { 12 | var accessToken = await managedIdenitityCredential.GetTokenAsync( 13 | new TokenRequestContext( 14 | new[] { OPENAI_SCOPE } 15 | ), 16 | cancellationToken 17 | ); 18 | 19 | return accessToken.Token; 20 | } 21 | 22 | 23 | public static bool IsTokenExpired(string accessToken) 24 | { 25 | var tokenHandler = new JwtSecurityTokenHandler(); 26 | var jwttoken = tokenHandler.ReadToken(accessToken); 27 | var expDate = jwttoken.ValidTo; 28 | 29 | bool result = expDate < DateTime.UtcNow; 30 | return result; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/dotnet/AzureAI.Proxy/OpenAIHandlers/Tokens.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | using System.Text.Json; 3 | using TiktokenSharp; 4 | 5 | namespace AzureAI.Proxy.OpenAIHandlers; 6 | 7 | public static class Tokens 8 | { 9 | public static int GetTokensFromString(string str, string modelName) 10 | { 11 | if (modelName.Contains("gpt-35")) 12 | modelName = modelName.Replace("35", "3.5"); 13 | 14 | var encodingManager = TikToken.EncodingForModel(modelName); 15 | var encoding = encodingManager.Encode(str); 16 | int nrTokens = encoding.Count(); 17 | return nrTokens; 18 | } 19 | 20 | public static LogAnalyticsRecord CalculateChatInputTokens(HttpRequest request, LogAnalyticsRecord record) 21 | { 22 | //Rewind to first position to read the stream again 23 | request.Body.Position = 0; 24 | 25 | StreamReader reader = new StreamReader(request.Body, true); 26 | string bodyText = reader.ReadToEnd(); 27 | 28 | JsonNode jsonNode = JsonSerializer.Deserialize(bodyText); 29 | var modelName = jsonNode["model"].ToString(); 30 | 31 | record.Model = modelName; 32 | 33 | var messages = jsonNode["messages"].AsArray(); 34 | foreach (var message in messages) 35 | { 36 | var content = message["content"].ToString(); 37 | //calculate tokens using a tokenizer. 38 | record.InputTokens += GetTokensFromString(content, modelName); 39 | } 40 | 41 | 42 | 43 | return record; 44 | 45 | 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/dotnet/AzureAI.Proxy/OpenAIHandlers/Usage.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | using AzureAI.Proxy.Models; 3 | 4 | namespace AzureAI.Proxy.OpenAIHandlers 5 | { 6 | public static class Usage 7 | { 8 | public static void Handle(JsonNode jsonNode, ref LogAnalyticsRecord record) 9 | { 10 | //read tokens from responsebody - not streaming, so data is just there 11 | var modelName = jsonNode["model"].ToString(); 12 | record.Model = modelName; 13 | var usage = jsonNode["usage"]; 14 | if (usage["completion_tokens"] != null) 15 | { 16 | record.OutputTokens = int.Parse(usage["completion_tokens"].ToString()); 17 | } 18 | else 19 | { 20 | record.OutputTokens = 0; 21 | } 22 | record.InputTokens = int.Parse(usage["prompt_tokens"].ToString()); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/dotnet/AzureAI.Proxy/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "http": { 4 | "commandName": "Project", 5 | "environmentVariables": { 6 | "ASPNETCORE_ENVIRONMENT": "Development" 7 | }, 8 | "dotnetRunMessages": true, 9 | "applicationUrl": "http://localhost:5227" 10 | }, 11 | "Container (Dockerfile)": { 12 | "commandName": "Docker", 13 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", 14 | "environmentVariables": { 15 | "ASPNETCORE_HTTP_PORTS": "8080" 16 | }, 17 | "publishAllPorts": true 18 | } 19 | }, 20 | "$schema": "http://json.schemastore.org/launchsettings.json" 21 | } -------------------------------------------------------------------------------- /src/dotnet/AzureAI.Proxy/ReverseProxy/ThrottlingHealthPolicy.cs: -------------------------------------------------------------------------------- 1 | using Yarp.ReverseProxy.Health; 2 | using Yarp.ReverseProxy.Model; 3 | 4 | namespace AzureAI.Proxy.ReverseProxy; 5 | 6 | public class ThrottlingHealthPolicy : IPassiveHealthCheckPolicy 7 | { 8 | public static string ThrottlingPolicyName = "ThrottlingPolicy"; 9 | private readonly IDestinationHealthUpdater _healthUpdater; 10 | 11 | public ThrottlingHealthPolicy(IDestinationHealthUpdater healthUpdater) 12 | { 13 | _healthUpdater = healthUpdater; 14 | } 15 | 16 | public string Name => ThrottlingPolicyName; 17 | 18 | public void RequestProxied(HttpContext context, ClusterState cluster, DestinationState destination) 19 | { 20 | var headers = context.Response.Headers; 21 | 22 | if (context.Response.StatusCode is 429 or >= 500) 23 | { 24 | var retryAfterSeconds = 10; 25 | 26 | if (headers.TryGetValue("retry-after", out var retryAfterHeader) && retryAfterHeader.Count > 0 && int.TryParse(retryAfterHeader[0], out var retryAfter)) 27 | { 28 | retryAfterSeconds = retryAfter; 29 | } 30 | else 31 | if (headers.TryGetValue("x-ratelimit-reset-requests", out var ratelimiResetRequests) && ratelimiResetRequests.Count > 0 && int.TryParse(ratelimiResetRequests[0], out var ratelimiResetRequest)) 32 | { 33 | retryAfterSeconds = ratelimiResetRequest; 34 | } 35 | else 36 | if (headers.TryGetValue("x-ratelimit-reset-tokens", out var ratelimitResetTokens) && ratelimitResetTokens.Count > 0 && int.TryParse(ratelimitResetTokens[0], out var ratelimitResetToken)) 37 | { 38 | retryAfterSeconds = ratelimitResetToken; 39 | } 40 | 41 | _healthUpdater.SetPassive(cluster, destination, DestinationHealth.Unhealthy, TimeSpan.FromSeconds(retryAfterSeconds)); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/dotnet/AzureAI.Proxy/Services/ILogIngestionService.cs: -------------------------------------------------------------------------------- 1 | namespace AzureAI.Proxy.Services 2 | { 3 | public interface ILogIngestionService 4 | { 5 | Task LogAsync(LogAnalyticsRecord record); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/dotnet/AzureAI.Proxy/Services/IManagedIdentityService.cs: -------------------------------------------------------------------------------- 1 | using Azure.Core; 2 | 3 | namespace AzureAI.Proxy.Services 4 | { 5 | public interface IManagedIdentityService 6 | { 7 | TokenCredential GetTokenCredential(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/dotnet/AzureAI.Proxy/Services/LogIngestionService.cs: -------------------------------------------------------------------------------- 1 | using Azure; 2 | using Azure.Core; 3 | using Azure.Monitor.Ingestion; 4 | using System.Text.Json; 5 | 6 | namespace AzureAI.Proxy.Services 7 | { 8 | public class LogIngestionService : ILogIngestionService 9 | { 10 | private readonly IConfiguration _config; 11 | //private readonly IManagedIdentityService _managedIdentityService; 12 | private readonly LogsIngestionClient _logsIngestionClient; 13 | private readonly ILogger _logger; 14 | 15 | 16 | 17 | public LogIngestionService( 18 | // IManagedIdentityService managedIdentityService, 19 | LogsIngestionClient logsIngestionClient, 20 | IConfiguration config 21 | , ILogger logger 22 | ) 23 | { 24 | _config = config; 25 | _logsIngestionClient = logsIngestionClient; 26 | _logger = logger; 27 | } 28 | 29 | public async Task LogAsync(LogAnalyticsRecord record) 30 | { 31 | try 32 | { 33 | _logger.LogInformation("Writing logs..."); 34 | var jsonContent = new List(); 35 | jsonContent.Add(record); 36 | 37 | //RBAC Monitoring Metrics Publisher needed 38 | RequestContent content = RequestContent.Create(JsonSerializer.Serialize(jsonContent)); 39 | var ruleId = _config.GetSection("AzureMonitor")["DataCollectionRuleImmutableId"].ToString(); 40 | var stream = _config.GetSection("AzureMonitor")["DataCollectionRuleStream"].ToString(); 41 | 42 | Response response = await _logsIngestionClient.UploadAsync(ruleId, stream, content); 43 | 44 | } 45 | catch (Exception ex) 46 | { 47 | _logger.LogError($"Writing to LogAnalytics Failed: {ex.Message}"); 48 | } 49 | 50 | 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/dotnet/AzureAI.Proxy/Services/ManagedIdentityService.cs: -------------------------------------------------------------------------------- 1 | using Azure.Core; 2 | using Azure.Identity; 3 | 4 | namespace AzureAI.Proxy.Services 5 | { 6 | public class ManagedIdentityService : IManagedIdentityService 7 | { 8 | private TokenCredential _credential; 9 | private readonly IConfiguration _config; 10 | private readonly IWebHostEnvironment _environment; 11 | 12 | public ManagedIdentityService(IConfiguration config, IWebHostEnvironment environment) 13 | { 14 | _config = config; 15 | _environment = environment; 16 | } 17 | 18 | public TokenCredential GetTokenCredential() 19 | { 20 | _credential = new DefaultAzureCredential(GetDefaultAzureCredentialOptions()); 21 | return _credential; 22 | } 23 | 24 | private DefaultAzureCredentialOptions GetDefaultAzureCredentialOptions() 25 | { 26 | 27 | DefaultAzureCredentialOptions options = new DefaultAzureCredentialOptions(); 28 | 29 | if (_environment.IsDevelopment()) { 30 | options.ExcludeManagedIdentityCredential = true; 31 | options.ExcludeWorkloadIdentityCredential = true; 32 | } 33 | else 34 | { 35 | options.ExcludeVisualStudioCredential = true; 36 | options.ExcludeVisualStudioCredential = true; 37 | options.ExcludeAzureCliCredential = true; 38 | options.ExcludeAzureDeveloperCliCredential = true; 39 | options.ExcludeAzurePowerShellCredential = true; 40 | options.ExcludeInteractiveBrowserCredential = true; 41 | } 42 | 43 | if (_config["EntraId:TenantId"] is not null) 44 | { 45 | options.TenantId = _config["EntraId:TenantId"]; 46 | } 47 | 48 | if (_config["CLIENT_ID"] is not null) 49 | { 50 | options.ManagedIdentityClientId = _config["CLIENT_ID"]; 51 | } 52 | 53 | return options; 54 | } 55 | 56 | 57 | } 58 | 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/dotnet/AzureAI.Proxy/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "APPCONFIG_ENDPOINT": "https://.azconfig.io" 10 | 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/dotnet/AzureAI.Proxy/proxyconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [ 3 | { 4 | "name": "gpt-35-turbo", 5 | "endpoints": [ 6 | { 7 | "address": "https://rbr-openai-sweden.openai.azure.com/", 8 | "priority": 1 9 | }, 10 | { 11 | "address": "https://rbr-openai-frc.openai.azure.com/", 12 | "priority": 2 13 | } 14 | ] 15 | }, 16 | { 17 | "name": "text-embedding-ada-002", 18 | "endpoints": [ 19 | { 20 | "address": "https://rbr-openai-sweden.openai.azure.com/", 21 | "priority": 1 22 | }, 23 | { 24 | "address": "https://rbr-openai-frc.openai.azure.com/", 25 | "priority": 1 26 | } 27 | ] 28 | }, 29 | { 30 | "name": "gpt-35-turbo-withpolicy", 31 | "endpoints": [ 32 | { 33 | "address": "https://rbr-openai-sweden.openai.azure.com/", 34 | "priority": 1 35 | } 36 | ] 37 | } 38 | ] 39 | } 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/dotnet/AzureAI.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureAI.Proxy", "AzureAI.Proxy\AzureAI.Proxy.csproj", "{37B14733-479E-45B3-A1DD-1319A7540716}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureAI.Proxy.Client", "AzureAI.Proxy.Client\AzureAI.Proxy.Client.csproj", "{6AC9FC04-2C0A-4D7D-95D0-127F0AF7AF5F}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {37B14733-479E-45B3-A1DD-1319A7540716}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {37B14733-479E-45B3-A1DD-1319A7540716}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {37B14733-479E-45B3-A1DD-1319A7540716}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {37B14733-479E-45B3-A1DD-1319A7540716}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {6AC9FC04-2C0A-4D7D-95D0-127F0AF7AF5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {6AC9FC04-2C0A-4D7D-95D0-127F0AF7AF5F}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {6AC9FC04-2C0A-4D7D-95D0-127F0AF7AF5F}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {6AC9FC04-2C0A-4D7D-95D0-127F0AF7AF5F}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {A15612B9-AFDB-43D0-8C45-721B85D078BC} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /tests.http: -------------------------------------------------------------------------------- 1 | @apimName = 2 | @apiPath = openai 3 | @azureOpenAIDeploymentId = gpt-35-turbo 4 | @azureOpenAIApiVersion = 2023-03-15-preview 5 | @prompt = "You are a helpful assistant." 6 | @stream = false 7 | @subscriptionKeyMarketing = 8 | @subscriptionKeyFinance = 9 | 10 | ### Test Azure API Management endpoint with OpenAI backend, with Marketing subscription key 11 | POST https://{{apimName}}.azure-api.net/{{apiPath}}/deployments/{{azureOpenAIDeploymentId}}/chat/completions?api-version={{azureOpenAIApiVersion}} 12 | Content-Type: application/json 13 | api-key: {{subscriptionKeyMarketing}} 14 | 15 | { 16 | "messages": [ 17 | { 18 | "role": "user", 19 | "content": {{prompt}} 20 | } 21 | ], 22 | "stream": {{stream}} 23 | } 24 | 25 | ### Test Azure API Management endpoint with OpenAI backend, with Finance subscription key 26 | POST https://{{apimName}}.azure-api.net/{{apiPath}}/deployments/{{azureOpenAIDeploymentId}}/chat/completions?api-version={{azureOpenAIApiVersion}} 27 | Content-Type: application/json 28 | api-key: {{subscriptionKeyFinance}} 29 | 30 | { 31 | "messages": [ 32 | { 33 | "role": "user", 34 | "content": {{prompt}} 35 | } 36 | ], 37 | "stream": {{stream}} 38 | } --------------------------------------------------------------------------------