├── .azdo └── pipelines │ └── azure-dev.yml ├── .devcontainer └── devcontainer.json ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── SECURITY.md └── workflows │ ├── azure-dev.yml │ └── template-validation.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── azure.yaml ├── docs ├── RAG.md ├── architecture.png ├── deploy_customization.md └── webapp_screenshot.png ├── infra ├── abbreviations.json ├── api.bicep ├── core │ ├── ai │ │ └── cognitiveservices.bicep │ ├── config │ │ └── configstore.bicep │ ├── database │ │ ├── cosmos │ │ │ ├── cosmos-account.bicep │ │ │ ├── mongo │ │ │ │ ├── cosmos-mongo-account.bicep │ │ │ │ └── cosmos-mongo-db.bicep │ │ │ └── sql │ │ │ │ ├── cosmos-sql-account.bicep │ │ │ │ ├── cosmos-sql-db.bicep │ │ │ │ ├── cosmos-sql-role-assign.bicep │ │ │ │ └── cosmos-sql-role-def.bicep │ │ ├── mysql │ │ │ └── flexibleserver.bicep │ │ ├── postgresql │ │ │ └── flexibleserver.bicep │ │ └── sqlserver │ │ │ └── sqlserver.bicep │ ├── gateway │ │ └── apim.bicep │ ├── host │ │ ├── ai-environment.bicep │ │ ├── aks-agent-pool.bicep │ │ ├── aks-managed-cluster.bicep │ │ ├── aks.bicep │ │ ├── appservice-appsettings.bicep │ │ ├── appservice.bicep │ │ ├── appserviceplan.bicep │ │ ├── container-app-upsert.bicep │ │ ├── container-app.bicep │ │ ├── container-apps-environment.bicep │ │ ├── container-apps.bicep │ │ ├── functions.bicep │ │ ├── ml-online-endpoint.bicep │ │ └── staticwebapp.bicep │ ├── monitor │ │ ├── applicationinsights-dashboard.bicep │ │ ├── applicationinsights.bicep │ │ ├── loganalytics.bicep │ │ └── monitoring.bicep │ ├── networking │ │ ├── cdn-endpoint.bicep │ │ ├── cdn-profile.bicep │ │ └── cdn.bicep │ ├── search │ │ └── search-services.bicep │ ├── security │ │ ├── aks-managed-cluster-access.bicep │ │ ├── appinsights-access.bicep │ │ ├── configstore-access.bicep │ │ ├── keyvault-access.bicep │ │ ├── keyvault-secret.bicep │ │ ├── keyvault.bicep │ │ ├── registry-access.bicep │ │ └── role.bicep │ ├── storage │ │ └── storage-account.bicep │ └── testing │ │ └── loadtesting.bicep ├── main.bicep ├── main.json └── main.parameters.json ├── pyproject.toml ├── requirements-dev.txt ├── scripts ├── resolve_model_quota.ps1 ├── resolve_model_quota.sh ├── set_default_models.ps1 ├── set_default_models.sh ├── setup_credential.ps1 ├── setup_credential.sh ├── validate_env_vars.ps1 ├── validate_env_vars.sh ├── write_env.ps1 └── write_env.sh ├── src ├── .dockerignore ├── .env.sample ├── Dockerfile ├── __init__.py ├── api │ ├── __init__.py │ ├── data │ │ └── embeddings.csv │ ├── main.py │ ├── routes.py │ ├── search_index_manager.py │ ├── static │ │ ├── assets │ │ │ └── template-images │ │ │ │ └── Avatar_Default.svg │ │ ├── react │ │ │ └── .vite │ │ │ │ └── manifest.json │ │ └── styles.css │ ├── templates │ │ └── index.html │ └── util.py ├── frontend │ ├── package.json │ ├── pnpm-lock.yaml │ ├── src │ │ ├── components │ │ │ ├── App.tsx │ │ │ ├── agents │ │ │ │ ├── AgentIcon.module.css │ │ │ │ ├── AgentIcon.tsx │ │ │ │ ├── AgentPreview.module.css │ │ │ │ ├── AgentPreview.tsx │ │ │ │ ├── AgentPreviewChatBot.module.css │ │ │ │ ├── AgentPreviewChatBot.tsx │ │ │ │ ├── AssistantMessage.module.css │ │ │ │ ├── AssistantMessage.tsx │ │ │ │ ├── UsageInfo.module.css │ │ │ │ ├── UsageInfo.tsx │ │ │ │ ├── UserMessage.module.css │ │ │ │ ├── UserMessage.tsx │ │ │ │ ├── chatbot │ │ │ │ │ ├── ChatInput.module.css │ │ │ │ │ ├── ChatInput.tsx │ │ │ │ │ └── types.ts │ │ │ │ └── hooks │ │ │ │ │ └── useFormatTimestamp.ts │ │ │ └── core │ │ │ │ ├── Markdown.module.css │ │ │ │ ├── Markdown.tsx │ │ │ │ ├── MenuButton │ │ │ │ ├── MenuButton.module.css │ │ │ │ └── MenuButton.tsx │ │ │ │ ├── SettingsPanel.module.css │ │ │ │ ├── SettingsPanel.tsx │ │ │ │ ├── ThinkBlock.module.css │ │ │ │ ├── ThinkBlock.tsx │ │ │ │ └── theme │ │ │ │ ├── ThemeContext.ts │ │ │ │ ├── ThemePicker.tsx │ │ │ │ ├── ThemeProvider.tsx │ │ │ │ ├── themes.ts │ │ │ │ └── useThemeProvider.ts │ │ ├── css-modules.d.ts │ │ ├── main.tsx │ │ ├── svg.d.ts │ │ └── types │ │ │ └── chat.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── gunicorn.conf.py ├── pyproject.toml └── requirements.txt └── tests └── test_search_index_manager.py /.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 and service connection for connecting to Azure, simply run `azd pipeline config --provider azdo` 9 | # Task "Install azd" needs to install setup-azd extension for azdo - https://marketplace.visualstudio.com/items?itemName=ms-azuretools.azd 10 | # See below for alternative task to install azd if you can't install above task in your organization 11 | 12 | pool: 13 | vmImage: ubuntu-latest 14 | 15 | steps: 16 | - task: setup-azd@0 17 | displayName: Install azd 18 | 19 | # If you can't install above task in your organization, you can comment it and uncomment below task to install azd 20 | # - task: Bash@3 21 | # displayName: Install azd 22 | # inputs: 23 | # targetType: 'inline' 24 | # script: | 25 | # curl -fsSL https://aka.ms/install-azd.sh | bash 26 | 27 | # azd delegate auth to az to use service connection with AzureCLI@2 28 | - pwsh: | 29 | azd config set auth.useAzCliAuth "true" 30 | displayName: Configure AZD to Use AZ CLI Authentication. 31 | 32 | - task: AzureCLI@2 33 | displayName: Provision Infrastructure 34 | inputs: 35 | azureSubscription: azconnection 36 | scriptType: bash 37 | scriptLocation: inlineScript 38 | inlineScript: | 39 | azd provision --no-prompt 40 | env: 41 | AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) 42 | AZURE_ENV_NAME: $(AZURE_ENV_NAME) 43 | AZURE_LOCATION: $(AZURE_LOCATION) 44 | # Project specific environment variables 45 | AZURE_RESOURCE_GROUP: $(AZURE_RESOURCE_GROUP) 46 | AZURE_AIHUB_NAME: $(AZURE_AIHUB_NAME) 47 | AZURE_AIPROJECT_NAME: $(AZURE_AIPROJECT_NAME) 48 | AZURE_AISERVICES_NAME: $(AZURE_AISERVICES_NAME) 49 | AZURE_SEARCH_SERVICE_NAME: $(AZURE_SEARCH_SERVICE_NAME) 50 | AZURE_APPLICATION_INSIGHTS_NAME: $(AZURE_APPLICATION_INSIGHTS_NAME) 51 | AZURE_CONTAINER_REGISTRY_NAME: $(AZURE_CONTAINER_REGISTRY_NAME) 52 | AZURE_KEYVAULT_NAME: $(AZURE_KEYVAULT_NAME) 53 | AZURE_STORAGE_ACCOUNT_NAME: $(AZURE_STORAGE_ACCOUNT_NAME) 54 | AZURE_LOG_ANALYTICS_WORKSPACE_NAME: $(AZURE_LOG_ANALYTICS_WORKSPACE_NAME) 55 | USE_CONTAINER_REGISTRY: $(USE_CONTAINER_REGISTRY) 56 | USE_APPLICATION_INSIGHTS: $(USE_APPLICATION_INSIGHTS) 57 | USE_SEARCH_SERVICE: $(USE_SEARCH_SERVICE) 58 | AZURE_AI_CHAT_DEPLOYMENT_NAME: $(AZURE_AI_CHAT_DEPLOYMENT_NAME) 59 | AZURE_AI_CHAT_DEPLOYMENT_SKU: $(AZURE_AI_CHAT_DEPLOYMENT_SKU) 60 | AZURE_AI_CHAT_DEPLOYMENT_CAPACITY: $(AZURE_AI_CHAT_DEPLOYMENT_CAPACITY) 61 | AZURE_AI_CHAT_MODEL_FORMAT: $(AZURE_AI_CHAT_MODEL_FORMAT) 62 | AZURE_AI_CHAT_MODEL_NAME: $(AZURE_AI_CHAT_MODEL) 63 | AZURE_AI_CHAT_MODEL_VERSION: $(AZURE_AI_CHAT_MODEL_VERSION) 64 | AZURE_AI_EMBED_DEPLOYMENT_NAME: $(AZURE_AI_EMBED_DEPLOYMENT_NAME) 65 | AZURE_AI_EMBED_DEPLOYMENT_SKU: $(AZURE_AI_EMBED_DEPLOYMENT_SKU) 66 | AZURE_AI_EMBED_DEPLOYMENT_CAPACITY: $(AZURE_AI_EMBED_DEPLOYMENT_CAPACITY) 67 | AZURE_AI_EMBED_MODEL_FORMAT: $(AZURE_AI_EMBED_MODEL_FORMAT) 68 | AZURE_AI_EMBED_MODEL_NAME: $(AZURE_AI_EMBED_MODEL_NAME) 69 | AZURE_AI_EMBED_MODEL_VERSION: $(AZURE_AI_EMBED_MODEL_VERSION) 70 | AZURE_EXISTING_AIPROJECT_RESOURCE_ID: $(AZURE_EXISTING_AIPROJECT_RESOURCE_ID) 71 | - task: AzureCLI@2 72 | displayName: Deploy Application 73 | inputs: 74 | azureSubscription: azconnection 75 | scriptType: bash 76 | scriptLocation: inlineScript 77 | inlineScript: | 78 | azd deploy --no-prompt 79 | env: 80 | AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) 81 | AZURE_ENV_NAME: $(AZURE_ENV_NAME) 82 | AZURE_LOCATION: $(AZURE_LOCATION) 83 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-started-with-ai-chat", 3 | "image": "mcr.microsoft.com/devcontainers/python:3.11-bullseye", 4 | "forwardPorts": [50505], 5 | "features": { 6 | "ghcr.io/devcontainers/features/azure-cli:1.2.6": {}, 7 | "ghcr.io/azure/azure-dev/azd:latest": {} 8 | }, 9 | "customizations": { 10 | "vscode": { 11 | "extensions": [ 12 | "ms-azuretools.azure-dev", 13 | "ms-azuretools.vscode-bicep", 14 | "ms-python.python", 15 | "ms-toolsai.jupyter", 16 | "GitHub.vscode-github-actions" 17 | ] 18 | } 19 | }, 20 | "postCreateCommand": "python3 -m pip install -e src", 21 | "remoteUser": "vscode", 22 | "hostRequirements": { 23 | "memory": "8gb" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/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 10 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to get-started-with-ai-chat 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 6 | 7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | - [Code of Conduct](#coc) 16 | - [Issues and Bugs](#issue) 17 | - [Feature Requests](#feature) 18 | - [Submission Guidelines](#submit) 19 | 20 | ## Code of Conduct 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Found an Issue? 24 | If you find a bug in the source code or a mistake in the documentation, you can help us by 25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 26 | [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Want a Feature? 29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 30 | Repository. If you would like to *implement* a new feature, please submit an issue with 31 | a proposal for your work first, to be sure that we can use it. 32 | 33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 34 | 35 | ## Submission Guidelines 36 | 37 | ### Submitting an Issue 38 | Before you submit an issue, search the archive, maybe your question was already answered. 39 | 40 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 41 | Help us to maximize the effort we can spend fixing issues and adding new 42 | features, by not reporting duplicate issues. Providing the following information will increase the 43 | chances of your issue being dealt with quickly: 44 | 45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 46 | * **Version** - what version is affected (e.g. 0.1.2) 47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 48 | * **Browsers and Operating System** - is this a problem with all browsers? 49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 50 | * **Related Issues** - has a similar issue been reported before? 51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 52 | causing the problem (line of code or commit) 53 | 54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new]. 55 | 56 | ### Submitting a Pull Request (PR) 57 | Before you submit your Pull Request (PR) consider the following guidelines: 58 | 59 | * Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR 60 | that relates to your submission. You don't want to duplicate effort. 61 | 62 | * Make your changes in a new git fork: 63 | 64 | * Commit your changes using a descriptive commit message 65 | * Push your fork to GitHub: 66 | * In GitHub, create a pull request 67 | * If we suggest changes then: 68 | * Make the required updates. 69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 70 | 71 | ```shell 72 | git rebase master -i 73 | git push -f 74 | ``` 75 | 76 | That's it! Thank you for your contribution! 77 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | > Please provide us with the following information: 5 | > --------------------------------------------------------------- 6 | 7 | ### This issue is for a: (mark with an `x`) 8 | ``` 9 | - [ ] bug report -> please search issues before submitting 10 | - [ ] feature request 11 | - [ ] documentation issue or request 12 | - [ ] regression (a behavior that used to work and stopped in a new release) 13 | ``` 14 | 15 | ### Minimal steps to reproduce 16 | > 17 | 18 | ### Any log messages given by the failure 19 | > 20 | 21 | ### Expected/desired behavior 22 | > 23 | 24 | ### OS and Version? 25 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) 26 | 27 | ### Versions 28 | > 29 | 30 | ### Mention any other details that might be useful 31 | 32 | > --------------------------------------------------------------- 33 | > Thanks! We'll be in touch soon. 34 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | 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/). 4 | 5 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](), please report it to us as described below. 6 | 7 | ## Reporting Security Issues 8 | 9 | **Please do not report security vulnerabilities through public GitHub issues.** 10 | 11 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 12 | 13 | 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://www.microsoft.com/msrc/pgp-key-msrc). 14 | 15 | 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://www.microsoft.com/msrc). 16 | 17 | 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: 18 | 19 | - Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 20 | - Full paths of source file(s) related to the manifestation of the issue 21 | - The location of the affected source code (tag/branch/commit or direct URL) 22 | - Any special configuration required to reproduce the issue 23 | - Step-by-step instructions to reproduce the issue 24 | - Proof-of-concept or exploit code (if possible) 25 | - Impact of the issue, including how an attacker might exploit the issue 26 | 27 | This information will help us triage your report more quickly. 28 | 29 | 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://microsoft.com/msrc/bounty) page for more details about our active programs. 30 | 31 | ## Preferred Languages 32 | 33 | We prefer all communications to be in English. 34 | 35 | ## Policy 36 | 37 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/msrc/cvd). 38 | -------------------------------------------------------------------------------- /.github/workflows/azure-dev.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Azure 2 | 3 | # Run when commits are pushed to main 4 | on: 5 | workflow_dispatch: 6 | # push: 7 | # # Run when commits are pushed to mainline branch (main or master) 8 | # # Set this to the mainline branch you are using 9 | # branches: 10 | # - main 11 | 12 | # Set up permissions for deploying with secretless Azure federated credentials 13 | # https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux#set-up-azure-login-with-openid-connect-authentication 14 | permissions: 15 | id-token: write 16 | contents: read 17 | 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | env: 23 | AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} 24 | AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} 25 | AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} 26 | AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} 27 | AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} 28 | AZURE_RESOURCE_GROUP: ${{ vars.AZURE_RESOURCE_GROUP }} 29 | AZURE_AIHUB_NAME: ${{ vars.AZURE_AIHUB_NAME }} 30 | AZURE_AIPROJECT_NAME: ${{ vars.AZURE_AIPROJECT_NAME }} 31 | AZURE_AISERVICES_NAME: ${{ vars.AZURE_AISERVICES_NAME }} 32 | AZURE_SEARCH_SERVICE_NAME: ${{ vars.AZURE_SEARCH_SERVICE_NAME }} 33 | AZURE_APPLICATION_INSIGHTS_NAME: ${{ vars.AZURE_APPLICATION_INSIGHTS_NAME }} 34 | AZURE_CONTAINER_REGISTRY_NAME: ${{ vars.AZURE_CONTAINER_REGISTRY_NAME }} 35 | AZURE_KEYVAULT_NAME: ${{ vars.AZURE_KEYVAULT_NAME }} 36 | AZURE_STORAGE_ACCOUNT_NAME: ${{ vars.AZURE_STORAGE_ACCOUNT_NAME }} 37 | AZURE_LOG_ANALYTICS_WORKSPACE_NAME: ${{ vars.AZURE_LOG_ANALYTICS_WORKSPACE_NAME }} 38 | USE_CONTAINER_REGISTRY: ${{ vars.USE_CONTAINER_REGISTRY }} 39 | USE_APPLICATION_INSIGHTS: ${{ vars.USE_APPLICATION_INSIGHTS }} 40 | USE_SEARCH_SERVICE: ${{ vars.USE_SEARCH_SERVICE }} 41 | AZURE_AI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_AI_CHAT_DEPLOYMENT_NAME }} 42 | AZURE_AI_CHAT_DEPLOYMENT_SKU: ${{ vars.AZURE_AI_CHAT_DEPLOYMENT_SKU }} 43 | AZURE_AI_CHAT_DEPLOYMENT_CAPACITY: ${{ vars.AZURE_AI_CHAT_DEPLOYMENT_CAPACITY }} 44 | AZURE_AI_CHAT_MODEL_FORMAT: ${{ vars.AZURE_AI_CHAT_MODEL_FORMAT }} 45 | AZURE_AI_CHAT_MODEL_NAME: ${{ vars.AZURE_AI_CHAT_MODEL_NAME }} 46 | AZURE_AI_CHAT_MODEL_VERSION: ${{ vars.AZURE_AI_CHAT_MODEL_VERSION }} 47 | AZURE_AI_EMBED_DEPLOYMENT_NAME: ${{ vars.AZURE_AI_EMBED_DEPLOYMENT_NAME }} 48 | AZURE_AI_EMBED_DEPLOYMENT_SKU: ${{ vars.AZURE_AI_EMBED_DEPLOYMENT_SKU }} 49 | AZURE_AI_EMBED_DEPLOYMENT_CAPACITY: ${{ vars.AZURE_AI_EMBED_DEPLOYMENT_CAPACITY }} 50 | AZURE_AI_EMBED_MODEL_FORMAT: ${{ vars.AZURE_AI_EMBED_MODEL_FORMAT }} 51 | AZURE_AI_EMBED_MODEL_NAME: ${{ vars.AZURE_AI_EMBED_MODEL_NAME }} 52 | AZURE_AI_EMBED_MODEL_VERSION: ${{ vars.AZURE_AI_EMBED_MODEL_VERSION }} 53 | AZURE_EXISTING_AIPROJECT_RESOURCE_ID: ${{ vars.AZURE_EXISTING_AIPROJECT_RESOURCE_ID }} 54 | steps: 55 | - name: Checkout 56 | uses: actions/checkout@v4 57 | - name: Install azd 58 | uses: Azure/setup-azd@v2.0.0 59 | - name: Log in with Azure (Federated Credentials) 60 | run: | 61 | azd auth login ` 62 | --client-id "$Env:AZURE_CLIENT_ID" ` 63 | --federated-credential-provider "github" ` 64 | --tenant-id "$Env:AZURE_TENANT_ID" 65 | shell: pwsh 66 | 67 | 68 | - name: Provision Infrastructure 69 | run: azd provision --no-prompt 70 | env: 71 | AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ secrets.AZD_INITIAL_ENVIRONMENT_CONFIG }} 72 | 73 | - name: Deploy Application 74 | run: azd deploy --no-prompt 75 | -------------------------------------------------------------------------------- /.github/workflows/template-validation.yml: -------------------------------------------------------------------------------- 1 | name: Template Validation 2 | on: 3 | workflow_dispatch: 4 | 5 | permissions: 6 | contents: read 7 | id-token: write 8 | pull-requests: write 9 | 10 | jobs: 11 | template_validation_job: 12 | runs-on: ubuntu-latest 13 | name: template validation 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - uses: microsoft/template-validation-action@v0.3.2 18 | id: validation 19 | env: 20 | AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} 21 | AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} 22 | AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} 23 | AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} 24 | AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | # Project-specific variables (matches azure-dev.yaml/azure.yaml/main.parameters.json) 27 | AZURE_RESOURCE_GROUP: ${{ vars.AZURE_RESOURCE_GROUP }} 28 | AZURE_AIHUB_NAME: ${{ vars.AZURE_AIHUB_NAME }} 29 | AZURE_AIPROJECT_NAME: ${{ vars.AZURE_AIPROJECT_NAME }} 30 | AZURE_AISERVICES_NAME: ${{ vars.AZURE_AISERVICES_NAME }} 31 | AZURE_SEARCH_SERVICE_NAME: ${{ vars.AZURE_SEARCH_SERVICE_NAME }} 32 | AZURE_APPLICATION_INSIGHTS_NAME: ${{ vars.AZURE_APPLICATION_INSIGHTS_NAME }} 33 | AZURE_CONTAINER_REGISTRY_NAME: ${{ vars.AZURE_CONTAINER_REGISTRY_NAME }} 34 | AZURE_KEYVAULT_NAME: ${{ vars.AZURE_KEYVAULT_NAME }} 35 | AZURE_STORAGE_ACCOUNT_NAME: ${{ vars.AZURE_STORAGE_ACCOUNT_NAME }} 36 | AZURE_LOG_ANALYTICS_WORKSPACE_NAME: ${{ vars.AZURE_LOG_ANALYTICS_WORKSPACE_NAME }} 37 | USE_CONTAINER_REGISTRY: ${{ vars.USE_CONTAINER_REGISTRY }} 38 | USE_APPLICATION_INSIGHTS: ${{ vars.USE_APPLICATION_INSIGHTS }} 39 | USE_SEARCH_SERVICE: ${{ vars.USE_SEARCH_SERVICE }} 40 | AZURE_AI_CHAT_DEPLOYMENT: ${{ vars.AZURE_AI_CHAT_DEPLOYMENT_NAME }} 41 | AZURE_AI_CHAT_DEPLOYMENT_SKU: ${{ vars.AZURE_AI_CHAT_DEPLOYMENT_SKU }} 42 | AZURE_AI_CHAT_DEPLOYMENT_CAPACITY: ${{ vars.AZURE_AI_CHAT_DEPLOYMENT_CAPACITY }} 43 | AZURE_AI_CHAT_MODEL_FORMAT: ${{ vars.AZURE_AI_CHAT_MODEL_FORMAT }} 44 | AZURE_AI_CHAT_MODEL_NAME: ${{ vars.AZURE_AI_CHAT_MODEL_NAME }} 45 | AZURE_AI_CHAT_MODEL_VERSION: ${{ vars.AZURE_AI_CHAT_MODEL_VERSION }} 46 | AZURE_AI_EMBED_DEPLOYMENT_NAME: ${{ vars.AZURE_AI_EMBED_DEPLOYMENT_NAME }} 47 | AZURE_AI_EMBED_DEPLOYMENT_SKU: ${{ vars.AZURE_AI_EMBED_DEPLOYMENT_SKU }} 48 | AZURE_AI_EMBED_DEPLOYMENT_CAPACITY: ${{ vars.AZURE_AI_EMBED_DEPLOYMENT_CAPACITY }} 49 | AZURE_AI_EMBED_MODEL_FORMAT: ${{ vars.AZURE_AI_EMBED_MODEL_FORMAT }} 50 | AZURE_AI_EMBED_MODEL_NAME: ${{ vars.AZURE_AI_EMBED_MODEL_NAME }} 51 | AZURE_AI_EMBED_MODEL_VERSION: ${{ vars.AZURE_AI_EMBED_MODEL_VERSION }} 52 | AZURE_EXISTING_AIPROJECT_RESOURCE_ID: ${{ vars.AZURE_EXISTING_AIPROJECT_RESOURCE_ID }} 53 | - name: print result 54 | run: cat ${{ steps.validation.outputs.resultFile }} 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .nox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | *.py,cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | cover/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | db.sqlite3-journal 61 | 62 | # Flask stuff 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | .pybuilder/ 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # Environments 84 | .env 85 | .venv* 86 | env/ 87 | venv/ 88 | ENV/ 89 | env.bak/ 90 | venv.bak/ 91 | .env 92 | 93 | # Pyenv, Pipenv, Poetry, PDM configurations 94 | .python-version 95 | Pipfile.lock 96 | poetry.lock 97 | pdm.lock 98 | .pdm.toml 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Spyder, Rope project settings 111 | .spyderproject 112 | .spyproject 113 | .ropeproject 114 | 115 | # Mkdocs documentation 116 | /site 117 | 118 | # Type checkers, MyPy, Pyre, Pytype 119 | .mypy_cache/ 120 | .dmypy.json 121 | dmypy.json 122 | .pyre/ 123 | .pytype/ 124 | 125 | # Cython debug symbols 126 | cython_debug/ 127 | 128 | # IDEs and editors 129 | .idea/ # Uncomment if you want to ignore PyCharm settings 130 | .vscode/ 131 | 132 | # Infrastructure and Deployment 133 | .azure 134 | kustomization.yaml 135 | kind 136 | kustomize 137 | .terraform* 138 | *.tfstate 139 | *.tfstate.backup 140 | *_env 141 | *.data 142 | .runs/ 143 | **/node_modules/ 144 | package-lock.json 145 | src/api/api/agents/writer/.promptflow/ 146 | flow.flex.yaml 147 | 148 | # Node.js dependencies 149 | node_modules/ 150 | .pnpm-store/ 151 | .pnpm-debug.log 152 | 153 | # React build output 154 | src/api/static/react/ 155 | .vscode/launch.json 156 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.5.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | exclude: ^tests/snapshots 8 | - id: trailing-whitespace 9 | - repo: https://github.com/astral-sh/ruff-pre-commit 10 | rev: v0.1.0 11 | hooks: 12 | # Run the linter. 13 | - id: ruff 14 | args: [ --fix ] 15 | # Run the formatter. 16 | - id: ruff-format 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2022 (c) Microsoft Corporation. 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 -------------------------------------------------------------------------------- /azure.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json 2 | 3 | name: azd-get-started-with-ai-chat 4 | metadata: 5 | template: azd-get-started-with-ai-chat@1.0.0 6 | 7 | hooks: 8 | preup: 9 | posix: 10 | shell: sh 11 | run: chmod u+r+x ./scripts/validate_env_vars.sh; ./scripts/validate_env_vars.sh 12 | interactive: true 13 | continueOnError: false 14 | windows: 15 | shell: pwsh 16 | run: ./scripts/validate_env_vars.ps1 17 | interactive: true 18 | continueOnError: false 19 | preprovision: 20 | posix: 21 | shell: sh 22 | run: chmod u+r+x ./scripts/set_default_models.sh; chmod u+r+x ./scripts/resolve_model_quota.sh; ./scripts/set_default_models.sh 23 | interactive: true 24 | continueOnError: false 25 | windows: 26 | shell: pwsh 27 | run: ./scripts/set_default_models.ps1 28 | interactive: true 29 | continueOnError: false 30 | postprovision: 31 | windows: 32 | shell: pwsh 33 | run: ./scripts/write_env.ps1; ./scripts/setup_credential.ps1 34 | continueOnError: true 35 | interactive: true 36 | posix: 37 | shell: sh 38 | run: chmod u+r+x ./scripts/write_env.sh; chmod u+r+x ./scripts/setup_credential.sh; ./scripts/write_env.sh; ./scripts/setup_credential.sh 39 | continueOnError: true 40 | interactive: true 41 | 42 | 43 | pipeline: 44 | variables: 45 | - AZURE_RESOURCE_GROUP 46 | - AZURE_AIHUB_NAME 47 | - AZURE_AIPROJECT_NAME 48 | - AZURE_AISERVICES_NAME 49 | - AZURE_SEARCH_SERVICE_NAME 50 | - AZURE_APPLICATION_INSIGHTS_NAME 51 | - AZURE_CONTAINER_REGISTRY_NAME 52 | - AZURE_KEYVAULT_NAME 53 | - AZURE_STORAGE_ACCOUNT_NAME 54 | - AZURE_LOG_ANALYTICS_WORKSPACE_NAME 55 | - USE_CONTAINER_REGISTRY 56 | - USE_APPLICATION_INSIGHTS 57 | - USE_SEARCH_SERVICE 58 | - AZURE_AI_CHAT_DEPLOYMENT_NAME 59 | - AZURE_AI_CHAT_DEPLOYMENT_SKU 60 | - AZURE_AI_CHAT_DEPLOYMENT_CAPACITY 61 | - AZURE_AI_CHAT_MODEL_NAME 62 | - AZURE_AI_CHAT_MODEL_FORMAT 63 | - AZURE_AI_CHAT_MODEL_VERSION 64 | - AZURE_AI_EMBED_DEPLOYMENT_NAME 65 | - AZURE_AI_EMBED_DEPLOYMENT_SKU 66 | - AZURE_AI_EMBED_DEPLOYMENT_CAPACITY 67 | - AZURE_AI_EMBED_MODEL_NAME 68 | - AZURE_AI_EMBED_MODEL_FORMAT 69 | - AZURE_AI_EMBED_MODEL_VERSION 70 | - AZURE_EXISTING_AIPROJECT_RESOURCE_ID 71 | -------------------------------------------------------------------------------- /docs/RAG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Retrieval-Augmented Generation (RAG) Setup Guide 4 | ## Overview 5 | The Retrieval-Augmented Generation (RAG) feature helps improve the responses from your application by combining the power of large language models (LLMs) with extra context retrieved from an external data source. Simply put, when you ask a question, the application first searches through a set of relevant documents (stored as embeddings) and then uses this context to provide a more accurate and relevant response. If no relevant context is found, the application returns the LLM response directly. 6 | This RAG feature is optional and is disabled by default. If you prefer to use it, simply set the environment variable `USE_AZURE_AI_SEARCH_SERVICE` to `true`. Doing so will also trigger the deployment of Azure AI Search resources. 7 | 8 | ## How does RAG work in this application? 9 | In our provided example, the application includes a sample dataset containing information about hiking products. This data was split into sentences, and each sentence was transformed into numerical representations called embeddings. These embeddings were created using OpenAI's `text-embedding-3-small` model with `dimensions=100`. The resulting embeddings file (`embeddings.csv`) is located in the `api/data` folder. 10 | 11 | When you ask a question, the application: 12 | 13 | 1. Searches these embeddings for information relevant to your query. 14 | 2. Identifies relevant context if available. 15 | 3. Combines the retrieved context with the LLM to provide a better answer. 16 | 17 | ## If you want to use your own dataset 18 | To create a custom embeddings file with your own data, you can use the provided helper class `SearchIndexManager`. Below is a straightforward way to build your own embeddings: 19 | ```python 20 | from .api.search_index_manager import SearchIndexManager 21 | 22 | search_index_manager = SearchIndexManager ( 23 | endpoint=self.search_endpoint, 24 | credential=your_credentials, 25 | index_name=index_name, 26 | dimensions=100, 27 | model="text-embedding-3-small", 28 | embeddings_client=embedding_client, 29 | ) 30 | await search_index_manager.build_embeddings_file( 31 | input_directory='data', 32 | output_file='data/embeddings.csv' 33 | sentences_per_embedding=4 34 | ) 35 | ``` 36 | - Make sure to replace `your_search_endpoint`, `your_credentials`, `your_index_name`, and `embedding_client` with your own Azure service details. 37 | - Your input data should be placed in the folder specified by `input_directory`. 38 | - `sentences_per_embedding parameter`, specifies the number of sentences used to construct the embedding. The larger this number, the broader the context that will be identified during the similarity search. 39 | 40 | ## Deploying the Application with RAG enabled 41 | To deploy your application using the RAG feature, set the following environment variables locally: 42 | In power shell: 43 | ``` 44 | $env:USE_AZURE_AI_SEARCH_SERVICE="true" 45 | $env:AZURE_AI_SEARCH_INDEX_NAME="index_sample" 46 | $env:AZURE_AI_EMBED_DEPLOYMENT_NAME="text-embedding-3-small" 47 | ``` 48 | 49 | In bash: 50 | ``` 51 | export USE_AZURE_AI_SEARCH_SERVICE="true" 52 | export AZURE_AI_SEARCH_INDEX_NAME="index_sample" 53 | export AZURE_AI_EMBED_DEPLOYMENT_NAME="text-embedding-3-small" 54 | ``` 55 | 56 | In cmd: 57 | ``` 58 | set USE_AZURE_AI_SEARCH_SERVICE=true 59 | set AZURE_AI_SEARCH_INDEX_NAME=index_sample 60 | set AZURE_AI_EMBED_DEPLOYMENT_NAME=text-embedding-3-small 61 | ``` 62 | 63 | - `USE_AZURE_AI_SEARCH_SERVICE`: Enables (default) or disables RAG. 64 | - `AZURE_AI_SEARCH_INDEX_NAME`: The Azure Search Index the application will use. 65 | - `AZURE_AI_EMBED_DEPLOYMENT_NAME`: The Azure embedding deployment used to create embeddings. 66 | 67 | **Note:** If either `AZURE_AI_SEARCH_INDEX_NAME` or `AZURE_AI_EMBED_DEPLOYMENT_NAME` is not provided, or the Azure AI Search service connection is unavailable, the application will run without using the RAG feature. 68 | 69 | ## Creating the Azure Search Index 70 | 71 | To utilize RAG, you must have an Azure search index. By default, the application uses `index_sample` as the index name. You can create an index either by following these official Azure [instructions](https://learn.microsoft.com/azure/ai-services/agents/how-to/tools/azure-ai-search?tabs=azurecli%2Cpython&pivots=overview-azure-ai-search), or programmatically with the provided helper methods: 72 | ```python 73 | # Create Azure Search Index (if it does not yet exist) 74 | search_index_manager.ensure_index_created(vector_index_dimensions) 75 | 76 | # Upload embeddings to the index 77 | search_index_manager.upload_documents(embeddings_path) 78 | ``` 79 | **Important:** If you have already created the index before deploying your application, the system will skip this step and directly use your existing Azure Search Index. The parameter `vector_index_dimensions` is only required if dimension information was not already provided when initially constructing the `SearchIndexManager` object. -------------------------------------------------------------------------------- /docs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/get-started-with-ai-chat/aa5095af96749f2654cd9b07273b9c3f3c9aa1d6/docs/architecture.png -------------------------------------------------------------------------------- /docs/deploy_customization.md: -------------------------------------------------------------------------------- 1 | 2 | # Azure AI Foundry Starter Template: Deployment customization 3 | 4 | This document describes how to customize the deployment of the Azure AI Foundry Starter Template. Once you follow the steps here, you can run `azd up` as described in the [Deploying](./../README.md#deploying-steps) steps. 5 | 6 | * [Disabling resources](#disabling-resources) 7 | * [Customizing resource names](#customizing-resource-names) 8 | * [Customizing model deployments](#customizing-model-deployments) 9 | 10 | ## Disabling resources 11 | 12 | * To disable AI Search, run `azd env set USE_SEARCH_SERVICE false` 13 | * To disable Application Insights, run `azd env set USE_APPLICATION_INSIGHTS false` 14 | * To disable Container Registry, run `azd env set USE_CONTAINER_REGISTRY false` 15 | 16 | Then run `azd up` to deploy the remaining resources. 17 | 18 | ## Customizing resource names 19 | 20 | By default this template will use a default naming convention to prevent naming collisions within Azure. 21 | To override default naming conventions the following can be set. 22 | 23 | * `AZURE_EXISTING_AIPROJECT_CONNECTION_STRING` - An existing connection string to be use. If specified, resources for AI Foundry Hub, AI Foundry Project, and Azure AI service will not be created. 24 | * `AZURE_AIHUB_NAME` - The name of the AI Foundry Hub resource 25 | * `AZURE_AIPROJECT_NAME` - The name of the AI Foundry Project 26 | * `AZURE_AISERVICES_NAME` - The name of the Azure AI service 27 | * `AZURE_SEARCH_SERVICE_NAME` - The name of the Azure Search service 28 | * `AZURE_STORAGE_ACCOUNT_NAME` - The name of the Storage Account 29 | * `AZURE_KEYVAULT_NAME` - The name of the Key Vault 30 | * `AZURE_CONTAINER_REGISTRY_NAME` - The name of the container registry 31 | * `AZURE_APPLICATION_INSIGHTS_NAME` - The name of the Application Insights instance 32 | * `AZURE_LOG_ANALYTICS_WORKSPACE_NAME` - The name of the Log Analytics workspace used by Application Insights 33 | 34 | To override any of those resource names, run `azd env set ` before running `azd up`. 35 | 36 | ## Customizing model deployments 37 | 38 | To customize the model deployments, you can set the following environment variables: 39 | 40 | ### Using a different chat model 41 | 42 | Change the chat deployment name: 43 | 44 | ```shell 45 | azd env set AZURE_AI_CHAT_DEPLOYMENT_NAME Phi-3.5-MoE-instruct 46 | ``` 47 | 48 | Change the chat model format (either OpenAI or Microsoft): 49 | 50 | ```shell 51 | azd env set AZURE_AI_CHAT_MODEL_FORMAT Microsoft 52 | ``` 53 | 54 | Change the chat model name: 55 | 56 | ```shell 57 | azd env set AZURE_AI_CHAT_MODEL_NAME Phi-3.5-MoE-instruct 58 | ``` 59 | 60 | Set the version of the chat model: 61 | 62 | ```shell 63 | azd env set AZURE_AI_CHAT_MODEL_VERSION 2 64 | ``` 65 | 66 | ### Setting capacity and deployment SKU 67 | 68 | For quota regions, you may find yourself needing to modify the default capacity and deployment SKU using environment variables as below. The default tokens per minute deployed in this template is 80,000 for chat model and 50,000 for the embedding model that is enough for all operations. If the region has quota less the these numbers, you will be prompt to input a lower capacity up to the available limit. 69 | 70 | Change the default capacity (in thousands of tokens per minute) of the chat deployment: 71 | 72 | ```shell 73 | azd env set AZURE_AI_CHAT_DEPLOYMENT_CAPACITY 50 74 | ``` 75 | 76 | Change the SKU of the chat deployment: 77 | 78 | ```shell 79 | azd env set AZURE_AI_CHAT_DEPLOYMENT_SKU Standard 80 | ``` 81 | 82 | Change the default capacity (in thousands of tokens per minute) of the embeddings deployment: 83 | 84 | ```shell 85 | azd env set AZURE_AI_EMBED_DEPLOYMENT_CAPACITY 50 86 | ``` 87 | 88 | Change the SKU of the embeddings deployment: 89 | 90 | ```shell 91 | azd env set AZURE_AI_EMBED_DEPLOYMENT_SKU Standard 92 | ``` 93 | 94 | ## Bringing an existing AI project resource 95 | 96 | If you have an existing AI project resource, you can bring it into the Azure AI Foundry Starter Template by setting the following environment variable: 97 | 98 | ```shell 99 | azd env set AZURE_EXISTING_AIPROJECT_CONNECTION_STRING "" 100 | ``` 101 | 102 | You can find the connection string on the overview page of your Azure AI project. 103 | 104 | If you do not have a deployment named "gpt-4o-mini" in your existing AI project, you should either create one in Azure AI Foundry or follow the steps in [Customizing model deployments](#customizing-model-deployments) to specify a different model. 105 | -------------------------------------------------------------------------------- /docs/webapp_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/get-started-with-ai-chat/aa5095af96749f2654cd9b07273b9c3f3c9aa1d6/docs/webapp_screenshot.png -------------------------------------------------------------------------------- /infra/api.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | param identityName string 6 | param containerAppsEnvironmentName string 7 | param azureExistingAIProjectResourceId string 8 | param chatDeploymentName string 9 | param embeddingDeploymentName string 10 | param aiSearchIndexName string 11 | param embeddingDeploymentDimensions string 12 | param searchServiceEndpoint string 13 | param projectName string 14 | param enableAzureMonitorTracing bool 15 | param azureTracingGenAIContentRecordingEnabled bool 16 | param projectEndpoint string 17 | 18 | resource apiIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { 19 | name: identityName 20 | location: location 21 | } 22 | 23 | var env = [ 24 | { 25 | name: 'AZURE_CLIENT_ID' 26 | value: apiIdentity.properties.clientId 27 | } 28 | { 29 | name: 'AZURE_EXISTING_AIPROJECT_RESOURCE_ID' 30 | value: azureExistingAIProjectResourceId 31 | } 32 | { 33 | name: 'AZURE_AI_CHAT_DEPLOYMENT_NAME' 34 | value: chatDeploymentName 35 | } 36 | { 37 | name: 'AZURE_AI_EMBED_DEPLOYMENT_NAME' 38 | value: embeddingDeploymentName 39 | } 40 | { 41 | name: 'AZURE_AI_SEARCH_INDEX_NAME' 42 | value: aiSearchIndexName 43 | } 44 | { 45 | name: 'AZURE_AI_EMBED_DIMENSIONS' 46 | value: embeddingDeploymentDimensions 47 | } 48 | { 49 | name: 'RUNNING_IN_PRODUCTION' 50 | value: 'true' 51 | } 52 | { 53 | name: 'AZURE_AI_SEARCH_ENDPOINT' 54 | value: searchServiceEndpoint 55 | } 56 | { 57 | name: 'ENABLE_AZURE_MONITOR_TRACING' 58 | value: enableAzureMonitorTracing 59 | } 60 | { 61 | name: 'AZURE_TRACING_GEN_AI_CONTENT_RECORDING_ENABLED' 62 | value: azureTracingGenAIContentRecordingEnabled 63 | } 64 | { 65 | name: 'AZURE_EXISTING_AIPROJECT_ENDPOINT' 66 | value: projectEndpoint 67 | } 68 | ] 69 | 70 | 71 | module app 'core/host/container-app-upsert.bicep' = { 72 | name: 'container-app-module' 73 | params: { 74 | name: name 75 | location: location 76 | tags: tags 77 | identityName: apiIdentity.name 78 | containerAppsEnvironmentName: containerAppsEnvironmentName 79 | targetPort: 50505 80 | env: env 81 | projectName: projectName 82 | } 83 | } 84 | 85 | 86 | output SERVICE_API_IDENTITY_PRINCIPAL_ID string = apiIdentity.properties.principalId 87 | output SERVICE_API_NAME string = app.outputs.name 88 | output SERVICE_API_URI string = app.outputs.uri 89 | -------------------------------------------------------------------------------- /infra/core/ai/cognitiveservices.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Cognitive Services instance.' 2 | param aiServiceName string 3 | param aiProjectName string 4 | param location string = resourceGroup().location 5 | param tags object = {} 6 | @description('The custom subdomain name used to access the API. Defaults to the value of the name parameter.') 7 | param customSubDomainName string = aiServiceName 8 | param disableLocalAuth bool = false 9 | param deployments array = [] 10 | param appInsightsId string 11 | param appInsightConnectionString string 12 | param appInsightConnectionName string 13 | param storageAccountId string 14 | param storageAccountConnectionName string 15 | 16 | @allowed([ 'Enabled', 'Disabled' ]) 17 | param publicNetworkAccess string = 'Enabled' 18 | param sku object = { 19 | name: 'S0' 20 | } 21 | 22 | param allowedIpRules array = [] 23 | param networkAcls object = empty(allowedIpRules) ? { 24 | defaultAction: 'Allow' 25 | } : { 26 | ipRules: allowedIpRules 27 | defaultAction: 'Deny' 28 | } 29 | 30 | resource account 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = { 31 | name: aiServiceName 32 | location: location 33 | sku: sku 34 | kind: 'AIServices' 35 | identity: { 36 | type: 'SystemAssigned' 37 | } 38 | tags: tags 39 | properties: { 40 | allowProjectManagement: true 41 | customSubDomainName: customSubDomainName 42 | networkAcls: networkAcls 43 | publicNetworkAccess: publicNetworkAccess 44 | disableLocalAuth: disableLocalAuth 45 | } 46 | } 47 | 48 | // Creates the Azure Foundry connection to your Azure App Insights resource 49 | resource appInsightConnection 'Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview' = { 50 | name: appInsightConnectionName 51 | parent: account 52 | properties: { 53 | category: 'AppInsights' 54 | target: appInsightsId 55 | authType: 'ApiKey' 56 | isSharedToAll: true 57 | credentials: { 58 | key: appInsightConnectionString 59 | } 60 | metadata: { 61 | ApiType: 'Azure' 62 | ResourceId: appInsightsId 63 | } 64 | } 65 | } 66 | 67 | // Creates the Azure Foundry connection to your Azure Storage resource 68 | resource storageAccountConnection 'Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview' = { 69 | name: storageAccountConnectionName 70 | parent: account 71 | properties: { 72 | category: 'AzureStorageAccount' 73 | target: storageAccountId 74 | authType: 'AAD' 75 | isSharedToAll: true 76 | metadata: { 77 | ApiType: 'Azure' 78 | ResourceId: storageAccountId 79 | } 80 | } 81 | } 82 | 83 | resource aiProject 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview' = { 84 | parent: account 85 | name: aiProjectName 86 | location: location 87 | tags: tags 88 | identity: { 89 | type: 'SystemAssigned' 90 | } 91 | properties: { 92 | description: 'AI Project' 93 | displayName: 'AI Project' 94 | } 95 | } 96 | 97 | @batchSize(1) 98 | resource aiServicesDeployments 'Microsoft.CognitiveServices/accounts/deployments@2024-10-01' = [for deployment in deployments: { 99 | parent: account 100 | name: deployment.name 101 | properties: { 102 | model: deployment.model 103 | raiPolicyName: contains(deployment, 'raiPolicyName') ? deployment.raiPolicyName : null 104 | } 105 | sku: contains(deployment, 'sku') ? deployment.sku : { 106 | name: 'Standard' 107 | capacity: 20 108 | } 109 | }] 110 | 111 | 112 | 113 | output endpoint string = account.properties.endpoint 114 | output endpoints object = account.properties.endpoints 115 | output id string = account.id 116 | output name string = account.name 117 | output projectResourceId string = aiProject.id 118 | output projectName string = aiProject.name 119 | output serviceName string = account.name 120 | output projectEndpoint string = aiProject.properties.endpoints['AI Foundry API'] 121 | output PrincipalId string = account.identity.principalId 122 | output accountPrincipalId string = account.identity.principalId 123 | output projectPrincipalId string = aiProject.identity.principalId 124 | -------------------------------------------------------------------------------- /infra/core/config/configstore.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure App Configuration store.' 2 | 3 | @description('The name for the Azure App Configuration store') 4 | param name string 5 | 6 | @description('The Azure region/location for the Azure App Configuration store') 7 | param location string = resourceGroup().location 8 | 9 | @description('Custom tags to apply to the Azure App Configuration store') 10 | param tags object = {} 11 | 12 | @description('Specifies the names of the key-value resources. The name is a combination of key and label with $ as delimiter. The label is optional.') 13 | param keyValueNames array = [] 14 | 15 | @description('Specifies the values of the key-value resources.') 16 | param keyValueValues array = [] 17 | 18 | @description('The principal ID to grant access to the Azure App Configuration store') 19 | param principalId string 20 | 21 | resource configStore 'Microsoft.AppConfiguration/configurationStores@2023-03-01' = { 22 | name: name 23 | location: location 24 | sku: { 25 | name: 'standard' 26 | } 27 | tags: tags 28 | } 29 | 30 | resource configStoreKeyValue 'Microsoft.AppConfiguration/configurationStores/keyValues@2023-03-01' = [for (item, i) in keyValueNames: { 31 | parent: configStore 32 | name: item 33 | properties: { 34 | value: keyValueValues[i] 35 | tags: tags 36 | } 37 | }] 38 | 39 | module configStoreAccess '../security/configstore-access.bicep' = { 40 | name: 'app-configuration-access' 41 | params: { 42 | configStoreName: name 43 | principalId: principalId 44 | } 45 | dependsOn: [configStore] 46 | } 47 | 48 | output endpoint string = configStore.properties.endpoint 49 | -------------------------------------------------------------------------------- /infra/core/database/cosmos/cosmos-account.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Cosmos DB account.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING' 7 | param keyVaultName string 8 | 9 | @allowed([ 'GlobalDocumentDB', 'MongoDB', 'Parse' ]) 10 | param kind string 11 | 12 | resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' = { 13 | name: name 14 | kind: kind 15 | location: location 16 | tags: tags 17 | properties: { 18 | consistencyPolicy: { defaultConsistencyLevel: 'Session' } 19 | locations: [ 20 | { 21 | locationName: location 22 | failoverPriority: 0 23 | isZoneRedundant: false 24 | } 25 | ] 26 | databaseAccountOfferType: 'Standard' 27 | enableAutomaticFailover: false 28 | enableMultipleWriteLocations: false 29 | apiProperties: (kind == 'MongoDB') ? { serverVersion: '4.2' } : {} 30 | capabilities: [ { name: 'EnableServerless' } ] 31 | } 32 | } 33 | 34 | resource cosmosConnectionString 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { 35 | parent: keyVault 36 | name: connectionStringKey 37 | properties: { 38 | value: cosmos.listConnectionStrings().connectionStrings[0].connectionString 39 | } 40 | } 41 | 42 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { 43 | name: keyVaultName 44 | } 45 | 46 | output connectionStringKey string = connectionStringKey 47 | output endpoint string = cosmos.properties.documentEndpoint 48 | output id string = cosmos.id 49 | output name string = cosmos.name 50 | -------------------------------------------------------------------------------- /infra/core/database/cosmos/mongo/cosmos-mongo-account.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Cosmos DB for MongoDB account.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | param keyVaultName string 7 | param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING' 8 | 9 | module cosmos '../../cosmos/cosmos-account.bicep' = { 10 | name: 'cosmos-account' 11 | params: { 12 | name: name 13 | location: location 14 | connectionStringKey: connectionStringKey 15 | keyVaultName: keyVaultName 16 | kind: 'MongoDB' 17 | tags: tags 18 | } 19 | } 20 | 21 | output connectionStringKey string = cosmos.outputs.connectionStringKey 22 | output endpoint string = cosmos.outputs.endpoint 23 | output id string = cosmos.outputs.id 24 | -------------------------------------------------------------------------------- /infra/core/database/cosmos/mongo/cosmos-mongo-db.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Cosmos DB for MongoDB account with a database.' 2 | param accountName string 3 | param databaseName string 4 | param location string = resourceGroup().location 5 | param tags object = {} 6 | 7 | param collections array = [] 8 | param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING' 9 | param keyVaultName string 10 | 11 | module cosmos 'cosmos-mongo-account.bicep' = { 12 | name: 'cosmos-mongo-account' 13 | params: { 14 | name: accountName 15 | location: location 16 | keyVaultName: keyVaultName 17 | tags: tags 18 | connectionStringKey: connectionStringKey 19 | } 20 | } 21 | 22 | resource database 'Microsoft.DocumentDB/databaseAccounts/mongodbDatabases@2022-08-15' = { 23 | name: '${accountName}/${databaseName}' 24 | tags: tags 25 | properties: { 26 | resource: { id: databaseName } 27 | } 28 | 29 | resource list 'collections' = [for collection in collections: { 30 | name: collection.name 31 | properties: { 32 | resource: { 33 | id: collection.id 34 | shardKey: { _id: collection.shardKey } 35 | indexes: [ { key: { keys: [ collection.indexKey ] } } ] 36 | } 37 | } 38 | }] 39 | 40 | dependsOn: [ 41 | cosmos 42 | ] 43 | } 44 | 45 | output connectionStringKey string = connectionStringKey 46 | output databaseName string = databaseName 47 | output endpoint string = cosmos.outputs.endpoint 48 | -------------------------------------------------------------------------------- /infra/core/database/cosmos/sql/cosmos-sql-account.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Cosmos DB for NoSQL account.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | param keyVaultName string 7 | 8 | module cosmos '../../cosmos/cosmos-account.bicep' = { 9 | name: 'cosmos-account' 10 | params: { 11 | name: name 12 | location: location 13 | tags: tags 14 | keyVaultName: keyVaultName 15 | kind: 'GlobalDocumentDB' 16 | } 17 | } 18 | 19 | output connectionStringKey string = cosmos.outputs.connectionStringKey 20 | output endpoint string = cosmos.outputs.endpoint 21 | output id string = cosmos.outputs.id 22 | output name string = cosmos.outputs.name 23 | -------------------------------------------------------------------------------- /infra/core/database/cosmos/sql/cosmos-sql-db.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Cosmos DB for NoSQL account with a database.' 2 | param accountName string 3 | param databaseName string 4 | param location string = resourceGroup().location 5 | param tags object = {} 6 | 7 | param containers array = [] 8 | param keyVaultName string 9 | param principalIds array = [] 10 | 11 | module cosmos 'cosmos-sql-account.bicep' = { 12 | name: 'cosmos-sql-account' 13 | params: { 14 | name: accountName 15 | location: location 16 | tags: tags 17 | keyVaultName: keyVaultName 18 | } 19 | } 20 | 21 | resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2022-05-15' = { 22 | name: '${accountName}/${databaseName}' 23 | properties: { 24 | resource: { id: databaseName } 25 | } 26 | 27 | resource list 'containers' = [for container in containers: { 28 | name: container.name 29 | properties: { 30 | resource: { 31 | id: container.id 32 | partitionKey: { paths: [ container.partitionKey ] } 33 | } 34 | options: {} 35 | } 36 | }] 37 | 38 | dependsOn: [ 39 | cosmos 40 | ] 41 | } 42 | 43 | module roleDefinition 'cosmos-sql-role-def.bicep' = { 44 | name: 'cosmos-sql-role-definition' 45 | params: { 46 | accountName: accountName 47 | } 48 | dependsOn: [ 49 | cosmos 50 | database 51 | ] 52 | } 53 | 54 | // We need batchSize(1) here because sql role assignments have to be done sequentially 55 | @batchSize(1) 56 | module userRole 'cosmos-sql-role-assign.bicep' = [for principalId in principalIds: if (!empty(principalId)) { 57 | name: 'cosmos-sql-user-role-${uniqueString(principalId)}' 58 | params: { 59 | accountName: accountName 60 | roleDefinitionId: roleDefinition.outputs.id 61 | principalId: principalId 62 | } 63 | dependsOn: [ 64 | cosmos 65 | database 66 | ] 67 | }] 68 | 69 | output accountId string = cosmos.outputs.id 70 | output accountName string = cosmos.outputs.name 71 | output connectionStringKey string = cosmos.outputs.connectionStringKey 72 | output databaseName string = databaseName 73 | output endpoint string = cosmos.outputs.endpoint 74 | output roleDefinitionId string = roleDefinition.outputs.id 75 | -------------------------------------------------------------------------------- /infra/core/database/cosmos/sql/cosmos-sql-role-assign.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates a SQL role assignment under an Azure Cosmos DB account.' 2 | param accountName string 3 | 4 | param roleDefinitionId string 5 | param principalId string = '' 6 | 7 | resource role 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2022-05-15' = { 8 | parent: cosmos 9 | name: guid(roleDefinitionId, principalId, cosmos.id) 10 | properties: { 11 | principalId: principalId 12 | roleDefinitionId: roleDefinitionId 13 | scope: cosmos.id 14 | } 15 | } 16 | 17 | resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' existing = { 18 | name: accountName 19 | } 20 | -------------------------------------------------------------------------------- /infra/core/database/cosmos/sql/cosmos-sql-role-def.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates a SQL role definition under an Azure Cosmos DB account.' 2 | param accountName string 3 | 4 | resource roleDefinition 'Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions@2022-08-15' = { 5 | parent: cosmos 6 | name: guid(cosmos.id, accountName, 'sql-role') 7 | properties: { 8 | assignableScopes: [ 9 | cosmos.id 10 | ] 11 | permissions: [ 12 | { 13 | dataActions: [ 14 | 'Microsoft.DocumentDB/databaseAccounts/readMetadata' 15 | 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*' 16 | 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*' 17 | ] 18 | notDataActions: [] 19 | } 20 | ] 21 | roleName: 'Reader Writer' 22 | type: 'CustomRole' 23 | } 24 | } 25 | 26 | resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' existing = { 27 | name: accountName 28 | } 29 | 30 | output id string = roleDefinition.id 31 | -------------------------------------------------------------------------------- /infra/core/database/mysql/flexibleserver.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Database for MySQL - Flexible Server.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | param sku object 7 | param storage object 8 | param administratorLogin string 9 | @secure() 10 | param administratorLoginPassword string 11 | param highAvailabilityMode string = 'Disabled' 12 | param databaseNames array = [] 13 | param allowAzureIPsFirewall bool = false 14 | param allowAllIPsFirewall bool = false 15 | param allowedSingleIPs array = [] 16 | 17 | // MySQL version 18 | param version string 19 | 20 | resource mysqlServer 'Microsoft.DBforMySQL/flexibleServers@2023-06-30' = { 21 | location: location 22 | tags: tags 23 | name: name 24 | sku: sku 25 | properties: { 26 | version: version 27 | administratorLogin: administratorLogin 28 | administratorLoginPassword: administratorLoginPassword 29 | storage: storage 30 | highAvailability: { 31 | mode: highAvailabilityMode 32 | } 33 | } 34 | 35 | resource database 'databases' = [for name in databaseNames: { 36 | name: name 37 | }] 38 | 39 | resource firewall_all 'firewallRules' = if (allowAllIPsFirewall) { 40 | name: 'allow-all-IPs' 41 | properties: { 42 | startIpAddress: '0.0.0.0' 43 | endIpAddress: '255.255.255.255' 44 | } 45 | } 46 | 47 | resource firewall_azure 'firewallRules' = if (allowAzureIPsFirewall) { 48 | name: 'allow-all-azure-internal-IPs' 49 | properties: { 50 | startIpAddress: '0.0.0.0' 51 | endIpAddress: '0.0.0.0' 52 | } 53 | } 54 | 55 | resource firewall_single 'firewallRules' = [for ip in allowedSingleIPs: { 56 | name: 'allow-single-${replace(ip, '.', '')}' 57 | properties: { 58 | startIpAddress: ip 59 | endIpAddress: ip 60 | } 61 | }] 62 | 63 | } 64 | 65 | output MYSQL_DOMAIN_NAME string = mysqlServer.properties.fullyQualifiedDomainName 66 | -------------------------------------------------------------------------------- /infra/core/database/postgresql/flexibleserver.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Database for PostgreSQL - Flexible Server.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | param sku object 7 | param storage object 8 | param administratorLogin string 9 | @secure() 10 | param administratorLoginPassword string 11 | param databaseNames array = [] 12 | param allowAzureIPsFirewall bool = false 13 | param allowAllIPsFirewall bool = false 14 | param allowedSingleIPs array = [] 15 | 16 | // PostgreSQL version 17 | param version string 18 | 19 | // Latest official version 2022-12-01 does not have Bicep types available 20 | resource postgresServer 'Microsoft.DBforPostgreSQL/flexibleServers@2022-12-01' = { 21 | location: location 22 | tags: tags 23 | name: name 24 | sku: sku 25 | properties: { 26 | version: version 27 | administratorLogin: administratorLogin 28 | administratorLoginPassword: administratorLoginPassword 29 | storage: storage 30 | highAvailability: { 31 | mode: 'Disabled' 32 | } 33 | } 34 | 35 | resource database 'databases' = [for name in databaseNames: { 36 | name: name 37 | }] 38 | 39 | resource firewall_all 'firewallRules' = if (allowAllIPsFirewall) { 40 | name: 'allow-all-IPs' 41 | properties: { 42 | startIpAddress: '0.0.0.0' 43 | endIpAddress: '255.255.255.255' 44 | } 45 | } 46 | 47 | resource firewall_azure 'firewallRules' = if (allowAzureIPsFirewall) { 48 | name: 'allow-all-azure-internal-IPs' 49 | properties: { 50 | startIpAddress: '0.0.0.0' 51 | endIpAddress: '0.0.0.0' 52 | } 53 | } 54 | 55 | resource firewall_single 'firewallRules' = [for ip in allowedSingleIPs: { 56 | name: 'allow-single-${replace(ip, '.', '')}' 57 | properties: { 58 | startIpAddress: ip 59 | endIpAddress: ip 60 | } 61 | }] 62 | 63 | } 64 | 65 | output POSTGRES_DOMAIN_NAME string = postgresServer.properties.fullyQualifiedDomainName 66 | -------------------------------------------------------------------------------- /infra/core/database/sqlserver/sqlserver.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure SQL Server instance.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | param appUser string = 'appUser' 7 | param databaseName string 8 | param keyVaultName string 9 | param sqlAdmin string = 'sqlAdmin' 10 | param connectionStringKey string = 'AZURE-SQL-CONNECTION-STRING' 11 | 12 | @secure() 13 | param sqlAdminPassword string 14 | @secure() 15 | param appUserPassword string 16 | 17 | resource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = { 18 | name: name 19 | location: location 20 | tags: tags 21 | properties: { 22 | version: '12.0' 23 | minimalTlsVersion: '1.2' 24 | publicNetworkAccess: 'Enabled' 25 | administratorLogin: sqlAdmin 26 | administratorLoginPassword: sqlAdminPassword 27 | } 28 | 29 | resource database 'databases' = { 30 | name: databaseName 31 | location: location 32 | } 33 | 34 | resource firewall 'firewallRules' = { 35 | name: 'Azure Services' 36 | properties: { 37 | // Allow all clients 38 | // Note: range [0.0.0.0-0.0.0.0] means "allow all Azure-hosted clients only". 39 | // This is not sufficient, because we also want to allow direct access from developer machine, for debugging purposes. 40 | startIpAddress: '0.0.0.1' 41 | endIpAddress: '255.255.255.254' 42 | } 43 | } 44 | } 45 | 46 | resource sqlDeploymentScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = { 47 | name: '${name}-deployment-script' 48 | location: location 49 | kind: 'AzureCLI' 50 | properties: { 51 | azCliVersion: '2.37.0' 52 | retentionInterval: 'PT1H' // Retain the script resource for 1 hour after it ends running 53 | timeout: 'PT5M' // Five minutes 54 | cleanupPreference: 'OnSuccess' 55 | environmentVariables: [ 56 | { 57 | name: 'APPUSERNAME' 58 | value: appUser 59 | } 60 | { 61 | name: 'APPUSERPASSWORD' 62 | secureValue: appUserPassword 63 | } 64 | { 65 | name: 'DBNAME' 66 | value: databaseName 67 | } 68 | { 69 | name: 'DBSERVER' 70 | value: sqlServer.properties.fullyQualifiedDomainName 71 | } 72 | { 73 | name: 'SQLCMDPASSWORD' 74 | secureValue: sqlAdminPassword 75 | } 76 | { 77 | name: 'SQLADMIN' 78 | value: sqlAdmin 79 | } 80 | ] 81 | 82 | scriptContent: ''' 83 | wget https://github.com/microsoft/go-sqlcmd/releases/download/v0.8.1/sqlcmd-v0.8.1-linux-x64.tar.bz2 84 | tar x -f sqlcmd-v0.8.1-linux-x64.tar.bz2 -C . 85 | 86 | cat < ./initDb.sql 87 | drop user if exists ${APPUSERNAME} 88 | go 89 | create user ${APPUSERNAME} with password = '${APPUSERPASSWORD}' 90 | go 91 | alter role db_owner add member ${APPUSERNAME} 92 | go 93 | SCRIPT_END 94 | 95 | ./sqlcmd -S ${DBSERVER} -d ${DBNAME} -U ${SQLADMIN} -i ./initDb.sql 96 | ''' 97 | } 98 | } 99 | 100 | resource sqlAdminPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { 101 | parent: keyVault 102 | name: 'sqlAdminPassword' 103 | properties: { 104 | value: sqlAdminPassword 105 | } 106 | } 107 | 108 | resource appUserPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { 109 | parent: keyVault 110 | name: 'appUserPassword' 111 | properties: { 112 | value: appUserPassword 113 | } 114 | } 115 | 116 | resource sqlAzureConnectionStringSercret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { 117 | parent: keyVault 118 | name: connectionStringKey 119 | properties: { 120 | value: '${connectionString}; Password=${appUserPassword}' 121 | } 122 | } 123 | 124 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { 125 | name: keyVaultName 126 | } 127 | 128 | var connectionString = 'Server=${sqlServer.properties.fullyQualifiedDomainName}; Database=${sqlServer::database.name}; User=${appUser}' 129 | output connectionStringKey string = connectionStringKey 130 | output databaseName string = sqlServer::database.name 131 | -------------------------------------------------------------------------------- /infra/core/gateway/apim.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure API Management instance.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | @description('The email address of the owner of the service') 7 | @minLength(1) 8 | param publisherEmail string = 'noreply@microsoft.com' 9 | 10 | @description('The name of the owner of the service') 11 | @minLength(1) 12 | param publisherName string = 'n/a' 13 | 14 | @description('The pricing tier of this API Management service') 15 | @allowed([ 16 | 'Consumption' 17 | 'Developer' 18 | 'Standard' 19 | 'Premium' 20 | ]) 21 | param sku string = 'Consumption' 22 | 23 | @description('The instance size of this API Management service.') 24 | @allowed([ 0, 1, 2 ]) 25 | param skuCount int = 0 26 | 27 | @description('Azure Application Insights Name') 28 | param applicationInsightsName string 29 | 30 | resource apimService 'Microsoft.ApiManagement/service@2021-08-01' = { 31 | name: name 32 | location: location 33 | tags: union(tags, { 'azd-service-name': name }) 34 | sku: { 35 | name: sku 36 | capacity: (sku == 'Consumption') ? 0 : ((sku == 'Developer') ? 1 : skuCount) 37 | } 38 | properties: { 39 | publisherEmail: publisherEmail 40 | publisherName: publisherName 41 | // Custom properties are not supported for Consumption SKU 42 | customProperties: sku == 'Consumption' ? {} : { 43 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA': 'false' 44 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA': 'false' 45 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_128_GCM_SHA256': 'false' 46 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_256_CBC_SHA256': 'false' 47 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_128_CBC_SHA256': 'false' 48 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_256_CBC_SHA': 'false' 49 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_128_CBC_SHA': 'false' 50 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TripleDes168': 'false' 51 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls10': 'false' 52 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls11': 'false' 53 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Ssl30': 'false' 54 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls10': 'false' 55 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls11': 'false' 56 | 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Ssl30': 'false' 57 | } 58 | } 59 | } 60 | 61 | resource apimLogger 'Microsoft.ApiManagement/service/loggers@2021-12-01-preview' = if (!empty(applicationInsightsName)) { 62 | name: 'app-insights-logger' 63 | parent: apimService 64 | properties: { 65 | credentials: { 66 | instrumentationKey: applicationInsights.properties.InstrumentationKey 67 | } 68 | description: 'Logger to Azure Application Insights' 69 | isBuffered: false 70 | loggerType: 'applicationInsights' 71 | resourceId: applicationInsights.id 72 | } 73 | } 74 | 75 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) { 76 | name: applicationInsightsName 77 | } 78 | 79 | output apimServiceName string = apimService.name 80 | -------------------------------------------------------------------------------- /infra/core/host/ai-environment.bicep: -------------------------------------------------------------------------------- 1 | @minLength(1) 2 | @description('Primary location for all resources') 3 | param location string 4 | 5 | @description('The AI Project resource name.') 6 | param aiProjectName string 7 | @description('The Storage Account resource name.') 8 | param storageAccountName string 9 | @description('The AI Services resource name.') 10 | param aiServicesName string 11 | @description('The AI Services model deployments.') 12 | param aiServiceModelDeployments array = [] 13 | @description('The Log Analytics resource name.') 14 | param logAnalyticsName string = '' 15 | @description('The Application Insights resource name.') 16 | param applicationInsightsName string = '' 17 | @description('The Azure Search resource name.') 18 | param searchServiceName string = '' 19 | @description('The Application Insights connection name.') 20 | param appInsightConnectionName string 21 | param tags object = {} 22 | param userPrincipalId string 23 | 24 | module storageAccount '../storage/storage-account.bicep' = { 25 | name: 'storageAccount' 26 | params: { 27 | location: location 28 | tags: tags 29 | name: storageAccountName 30 | containers: [ 31 | { 32 | name: 'default' 33 | } 34 | ] 35 | files: [ 36 | { 37 | name: 'default' 38 | } 39 | ] 40 | queues: [ 41 | { 42 | name: 'default' 43 | } 44 | ] 45 | tables: [ 46 | { 47 | name: 'default' 48 | } 49 | ] 50 | deleteRetentionPolicy: { 51 | allowPermanentDelete: false 52 | enabled: false 53 | } 54 | shareDeleteRetentionPolicy: { 55 | enabled: true 56 | days: 7 57 | } 58 | } 59 | } 60 | 61 | module logAnalytics '../monitor/loganalytics.bicep' = 62 | if (!empty(logAnalyticsName)) { 63 | name: 'logAnalytics' 64 | params: { 65 | location: location 66 | tags: tags 67 | name: logAnalyticsName 68 | } 69 | } 70 | 71 | module applicationInsights '../monitor/applicationinsights.bicep' = 72 | if (!empty(applicationInsightsName) && !empty(logAnalyticsName)) { 73 | name: 'applicationInsights' 74 | params: { 75 | location: location 76 | tags: tags 77 | name: applicationInsightsName 78 | logAnalyticsWorkspaceId: !empty(logAnalyticsName) ? logAnalytics.outputs.id : '' 79 | } 80 | } 81 | 82 | 83 | module cognitiveServices '../ai/cognitiveservices.bicep' = { 84 | name: 'cognitiveServices' 85 | params: { 86 | location: location 87 | tags: tags 88 | aiServiceName: aiServicesName 89 | aiProjectName: aiProjectName 90 | deployments: aiServiceModelDeployments 91 | appInsightsId: applicationInsights.outputs.id 92 | appInsightConnectionName: appInsightConnectionName 93 | appInsightConnectionString: applicationInsights.outputs.connectionString 94 | storageAccountId: storageAccount.outputs.id 95 | storageAccountConnectionName: storageAccount.outputs.name 96 | } 97 | } 98 | 99 | module backendStorageBlobDataContributorRoleAssignment '../../core/security/role.bicep' = { 100 | name: 'backend-role-storage-blob-data-contributor' 101 | params: { 102 | principalType: 'ServicePrincipal' 103 | principalId: cognitiveServices.outputs.accountPrincipalId 104 | roleDefinitionId: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' // Storage Blob Data Contributor 105 | } 106 | } 107 | 108 | module userStorageBlobDataContributorRoleAssignment '../../core/security/role.bicep' = { 109 | name: 'user-role-storage-blob-data-contributor' 110 | params: { 111 | principalType: 'User' 112 | principalId: userPrincipalId 113 | roleDefinitionId: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' // Storage Blob Data Contributor 114 | } 115 | } 116 | 117 | module searchService '../search/search-services.bicep' = 118 | if (!empty(searchServiceName)) { 119 | dependsOn: [cognitiveServices] 120 | name: 'searchService' 121 | params: { 122 | location: location 123 | tags: tags 124 | name: searchServiceName 125 | semanticSearch: 'free' 126 | authOptions: { aadOrApiKey: { aadAuthFailureMode: 'http401WithBearerChallenge'}} 127 | } 128 | } 129 | 130 | 131 | // Outputs 132 | output storageAccountId string = storageAccount.outputs.id 133 | output storageAccountName string = storageAccount.outputs.name 134 | 135 | output applicationInsightsId string = !empty(applicationInsightsName) ? applicationInsights.outputs.id : '' 136 | output applicationInsightsName string = !empty(applicationInsightsName) ? applicationInsights.outputs.name : '' 137 | output logAnalyticsWorkspaceId string = !empty(logAnalyticsName) ? logAnalytics.outputs.id : '' 138 | output logAnalyticsWorkspaceName string = !empty(logAnalyticsName) ? logAnalytics.outputs.name : '' 139 | 140 | output aiServiceId string = cognitiveServices.outputs.id 141 | output aiServicesName string = cognitiveServices.outputs.name 142 | output aiServiceEndpoint string = cognitiveServices.outputs.endpoints['OpenAI Language Model Instance API'] 143 | output aiProjectEndpoint string = cognitiveServices.outputs.projectEndpoint 144 | output aiServicePrincipalId string = cognitiveServices.outputs.accountPrincipalId 145 | 146 | output searchServiceId string = !empty(searchServiceName) ? searchService.outputs.id : '' 147 | output searchServiceName string = !empty(searchServiceName) ? searchService.outputs.name : '' 148 | output searchServiceEndpoint string = !empty(searchServiceName) ? searchService.outputs.endpoint : '' 149 | 150 | output projectResourceId string = cognitiveServices.outputs.projectResourceId 151 | -------------------------------------------------------------------------------- /infra/core/host/aks-agent-pool.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Adds an agent pool to an Azure Kubernetes Service (AKS) cluster.' 2 | param clusterName string 3 | 4 | @description('The agent pool name') 5 | param name string 6 | 7 | @description('The agent pool configuration') 8 | param config object 9 | 10 | resource aksCluster 'Microsoft.ContainerService/managedClusters@2023-10-02-preview' existing = { 11 | name: clusterName 12 | } 13 | 14 | resource nodePool 'Microsoft.ContainerService/managedClusters/agentPools@2023-10-02-preview' = { 15 | parent: aksCluster 16 | name: name 17 | properties: config 18 | } 19 | -------------------------------------------------------------------------------- /infra/core/host/aks-managed-cluster.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Kubernetes Service (AKS) cluster with a system agent pool.' 2 | @description('The name for the AKS managed cluster') 3 | param name string 4 | 5 | @description('The name of the resource group for the managed resources of the AKS cluster') 6 | param nodeResourceGroupName string = '' 7 | 8 | @description('The Azure region/location for the AKS resources') 9 | param location string = resourceGroup().location 10 | 11 | @description('Custom tags to apply to the AKS resources') 12 | param tags object = {} 13 | 14 | @description('Kubernetes Version') 15 | param kubernetesVersion string = '1.27.7' 16 | 17 | @description('Whether RBAC is enabled for local accounts') 18 | param enableRbac bool = true 19 | 20 | // Add-ons 21 | @description('Whether web app routing (preview) add-on is enabled') 22 | param webAppRoutingAddon bool = true 23 | 24 | // AAD Integration 25 | @description('Enable Azure Active Directory integration') 26 | param enableAad bool = false 27 | 28 | @description('Enable RBAC using AAD') 29 | param enableAzureRbac bool = false 30 | 31 | @description('The Tenant ID associated to the Azure Active Directory') 32 | param aadTenantId string = tenant().tenantId 33 | 34 | @description('The load balancer SKU to use for ingress into the AKS cluster') 35 | @allowed([ 'basic', 'standard' ]) 36 | param loadBalancerSku string = 'standard' 37 | 38 | @description('Network plugin used for building the Kubernetes network.') 39 | @allowed([ 'azure', 'kubenet', 'none' ]) 40 | param networkPlugin string = 'azure' 41 | 42 | @description('Network policy used for building the Kubernetes network.') 43 | @allowed([ 'azure', 'calico' ]) 44 | param networkPolicy string = 'azure' 45 | 46 | @description('If set to true, getting static credentials will be disabled for this cluster.') 47 | param disableLocalAccounts bool = false 48 | 49 | @description('The managed cluster SKU.') 50 | @allowed([ 'Free', 'Paid', 'Standard' ]) 51 | param sku string = 'Free' 52 | 53 | @description('Configuration of AKS add-ons') 54 | param addOns object = {} 55 | 56 | @description('The log analytics workspace id used for logging & monitoring') 57 | param workspaceId string = '' 58 | 59 | @description('The node pool configuration for the System agent pool') 60 | param systemPoolConfig object 61 | 62 | @description('The DNS prefix to associate with the AKS cluster') 63 | param dnsPrefix string = '' 64 | 65 | resource aks 'Microsoft.ContainerService/managedClusters@2023-10-02-preview' = { 66 | name: name 67 | location: location 68 | tags: tags 69 | identity: { 70 | type: 'SystemAssigned' 71 | } 72 | sku: { 73 | name: 'Base' 74 | tier: sku 75 | } 76 | properties: { 77 | nodeResourceGroup: !empty(nodeResourceGroupName) ? nodeResourceGroupName : 'rg-mc-${name}' 78 | kubernetesVersion: kubernetesVersion 79 | dnsPrefix: empty(dnsPrefix) ? '${name}-dns' : dnsPrefix 80 | enableRBAC: enableRbac 81 | aadProfile: enableAad ? { 82 | managed: true 83 | enableAzureRBAC: enableAzureRbac 84 | tenantID: aadTenantId 85 | } : null 86 | agentPoolProfiles: [ 87 | systemPoolConfig 88 | ] 89 | networkProfile: { 90 | loadBalancerSku: loadBalancerSku 91 | networkPlugin: networkPlugin 92 | networkPolicy: networkPolicy 93 | } 94 | disableLocalAccounts: disableLocalAccounts && enableAad 95 | addonProfiles: addOns 96 | ingressProfile: { 97 | webAppRouting: { 98 | enabled: webAppRoutingAddon 99 | } 100 | } 101 | } 102 | } 103 | 104 | var aksDiagCategories = [ 105 | 'cluster-autoscaler' 106 | 'kube-controller-manager' 107 | 'kube-audit-admin' 108 | 'guard' 109 | ] 110 | 111 | // TODO: Update diagnostics to be its own module 112 | // Blocking issue: https://github.com/Azure/bicep/issues/622 113 | // Unable to pass in a `resource` scope or unable to use string interpolation in resource types 114 | resource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceId)) { 115 | name: 'aks-diagnostics' 116 | scope: aks 117 | properties: { 118 | workspaceId: workspaceId 119 | logs: [for category in aksDiagCategories: { 120 | category: category 121 | enabled: true 122 | }] 123 | metrics: [ 124 | { 125 | category: 'AllMetrics' 126 | enabled: true 127 | } 128 | ] 129 | } 130 | } 131 | 132 | @description('The resource name of the AKS cluster') 133 | output clusterName string = aks.name 134 | 135 | @description('The AKS cluster identity') 136 | output clusterIdentity object = { 137 | clientId: aks.properties.identityProfile.kubeletidentity.clientId 138 | objectId: aks.properties.identityProfile.kubeletidentity.objectId 139 | resourceId: aks.properties.identityProfile.kubeletidentity.resourceId 140 | } 141 | -------------------------------------------------------------------------------- /infra/core/host/appservice-appsettings.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Updates app settings for an Azure App Service.' 2 | @description('The name of the app service resource within the current resource group scope') 3 | param name string 4 | 5 | @description('The app settings to be applied to the app service') 6 | @secure() 7 | param appSettings object 8 | 9 | resource appService 'Microsoft.Web/sites@2022-03-01' existing = { 10 | name: name 11 | } 12 | 13 | resource settings 'Microsoft.Web/sites/config@2022-03-01' = { 14 | name: 'appsettings' 15 | parent: appService 16 | properties: appSettings 17 | } 18 | -------------------------------------------------------------------------------- /infra/core/host/appservice.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure App Service in an existing Azure App Service plan.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | // Reference Properties 7 | param applicationInsightsName string = '' 8 | param appServicePlanId string 9 | param keyVaultName string = '' 10 | param managedIdentity bool = !empty(keyVaultName) 11 | 12 | // Runtime Properties 13 | @allowed([ 14 | 'dotnet', 'dotnetcore', 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom' 15 | ]) 16 | param runtimeName string 17 | param runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}' 18 | param runtimeVersion string 19 | 20 | // Microsoft.Web/sites Properties 21 | param kind string = 'app,linux' 22 | 23 | // Microsoft.Web/sites/config 24 | param allowedOrigins array = [] 25 | param alwaysOn bool = true 26 | param appCommandLine string = '' 27 | @secure() 28 | param appSettings object = {} 29 | param clientAffinityEnabled bool = false 30 | param enableOryxBuild bool = contains(kind, 'linux') 31 | param functionAppScaleLimit int = -1 32 | param linuxFxVersion string = runtimeNameAndVersion 33 | param minimumElasticInstanceCount int = -1 34 | param numberOfWorkers int = -1 35 | param scmDoBuildDuringDeployment bool = false 36 | param use32BitWorkerProcess bool = false 37 | param ftpsState string = 'FtpsOnly' 38 | param healthCheckPath string = '' 39 | 40 | resource appService 'Microsoft.Web/sites@2022-03-01' = { 41 | name: name 42 | location: location 43 | tags: tags 44 | kind: kind 45 | properties: { 46 | serverFarmId: appServicePlanId 47 | siteConfig: { 48 | linuxFxVersion: linuxFxVersion 49 | alwaysOn: alwaysOn 50 | ftpsState: ftpsState 51 | minTlsVersion: '1.2' 52 | appCommandLine: appCommandLine 53 | numberOfWorkers: numberOfWorkers != -1 ? numberOfWorkers : null 54 | minimumElasticInstanceCount: minimumElasticInstanceCount != -1 ? minimumElasticInstanceCount : null 55 | use32BitWorkerProcess: use32BitWorkerProcess 56 | functionAppScaleLimit: functionAppScaleLimit != -1 ? functionAppScaleLimit : null 57 | healthCheckPath: healthCheckPath 58 | cors: { 59 | allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins) 60 | } 61 | } 62 | clientAffinityEnabled: clientAffinityEnabled 63 | httpsOnly: true 64 | } 65 | 66 | identity: { type: managedIdentity ? 'SystemAssigned' : 'None' } 67 | 68 | resource basicPublishingCredentialsPoliciesFtp 'basicPublishingCredentialsPolicies' = { 69 | name: 'ftp' 70 | properties: { 71 | allow: false 72 | } 73 | } 74 | 75 | resource basicPublishingCredentialsPoliciesScm 'basicPublishingCredentialsPolicies' = { 76 | name: 'scm' 77 | properties: { 78 | allow: false 79 | } 80 | } 81 | } 82 | 83 | // Updates to the single Microsoft.sites/web/config resources that need to be performed sequentially 84 | // sites/web/config 'appsettings' 85 | module configAppSettings 'appservice-appsettings.bicep' = { 86 | name: '${name}-appSettings' 87 | params: { 88 | name: appService.name 89 | appSettings: union(appSettings, 90 | { 91 | SCM_DO_BUILD_DURING_DEPLOYMENT: string(scmDoBuildDuringDeployment) 92 | ENABLE_ORYX_BUILD: string(enableOryxBuild) 93 | }, 94 | runtimeName == 'python' && appCommandLine == '' ? { PYTHON_ENABLE_GUNICORN_MULTIWORKERS: 'true'} : {}, 95 | !empty(applicationInsightsName) ? { APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString } : {}, 96 | !empty(keyVaultName) ? { AZURE_KEY_VAULT_ENDPOINT: keyVault.properties.vaultUri } : {}) 97 | } 98 | } 99 | 100 | // sites/web/config 'logs' 101 | resource configLogs 'Microsoft.Web/sites/config@2022-03-01' = { 102 | name: 'logs' 103 | parent: appService 104 | properties: { 105 | applicationLogs: { fileSystem: { level: 'Verbose' } } 106 | detailedErrorMessages: { enabled: true } 107 | failedRequestsTracing: { enabled: true } 108 | httpLogs: { fileSystem: { enabled: true, retentionInDays: 1, retentionInMb: 35 } } 109 | } 110 | dependsOn: [configAppSettings] 111 | } 112 | 113 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = if (!(empty(keyVaultName))) { 114 | name: keyVaultName 115 | } 116 | 117 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) { 118 | name: applicationInsightsName 119 | } 120 | 121 | output identityPrincipalId string = managedIdentity ? appService.identity.principalId : '' 122 | output name string = appService.name 123 | output uri string = 'https://${appService.properties.defaultHostName}' 124 | -------------------------------------------------------------------------------- /infra/core/host/appserviceplan.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure App Service plan.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | param kind string = '' 7 | param reserved bool = true 8 | param sku object 9 | 10 | resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = { 11 | name: name 12 | location: location 13 | tags: tags 14 | sku: sku 15 | kind: kind 16 | properties: { 17 | reserved: reserved 18 | } 19 | } 20 | 21 | output id string = appServicePlan.id 22 | output name string = appServicePlan.name 23 | -------------------------------------------------------------------------------- /infra/core/host/container-app-upsert.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates or updates an existing Azure Container App.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | @description('The environment name for the container apps') 7 | param containerAppsEnvironmentName string 8 | 9 | @description('The number of CPU cores allocated to a single container instance, e.g., 0.5') 10 | param containerCpuCoreCount string = '0.5' 11 | 12 | @description('The maximum number of replicas to run. Must be at least 1.') 13 | @minValue(1) 14 | param containerMaxReplicas int = 10 15 | 16 | @description('The amount of memory allocated to a single container instance, e.g., 1Gi') 17 | param containerMemory string = '1.0Gi' 18 | 19 | @description('The minimum number of replicas to run. Must be at least 1.') 20 | @minValue(1) 21 | param containerMinReplicas int = 1 22 | 23 | @description('The name of the container') 24 | param containerName string = 'main' 25 | 26 | @allowed([ 'http', 'grpc' ]) 27 | @description('The protocol used by Dapr to connect to the app, e.g., HTTP or gRPC') 28 | param daprAppProtocol string = 'http' 29 | 30 | @description('Enable or disable Dapr for the container app') 31 | param daprEnabled bool = false 32 | 33 | @description('The Dapr app ID') 34 | param daprAppId string = containerName 35 | 36 | @description('Specifies if Ingress is enabled for the container app') 37 | param ingressEnabled bool = true 38 | 39 | @description('The type of identity for the resource') 40 | @allowed([ 'None', 'SystemAssigned', 'UserAssigned' ]) 41 | param identityType string = 'None' 42 | 43 | @description('The name of the user-assigned identity') 44 | param identityName string = '' 45 | 46 | @description('The secrets required for the container') 47 | @secure() 48 | param secrets object = {} 49 | 50 | @description('The environment variables for the container') 51 | param env array = [] 52 | 53 | @description('Specifies if the resource ingress is exposed externally') 54 | param external bool = true 55 | 56 | @description('The service binds associated with the container') 57 | param serviceBinds array = [] 58 | 59 | @description('The target port for the container') 60 | param targetPort int = 80 61 | 62 | param projectName string 63 | 64 | module app 'container-app.bicep' = { 65 | name: '${deployment().name}-update' 66 | params: { 67 | name: name 68 | location: location 69 | tags: tags 70 | identityType: identityType 71 | identityName: identityName 72 | ingressEnabled: ingressEnabled 73 | containerName: containerName 74 | containerAppsEnvironmentName: containerAppsEnvironmentName 75 | containerCpuCoreCount: containerCpuCoreCount 76 | containerMemory: containerMemory 77 | containerMinReplicas: containerMinReplicas 78 | containerMaxReplicas: containerMaxReplicas 79 | daprEnabled: daprEnabled 80 | daprAppId: daprAppId 81 | daprAppProtocol: daprAppProtocol 82 | secrets: secrets 83 | external: external 84 | env: env 85 | targetPort: targetPort 86 | serviceBinds: serviceBinds 87 | dependOn: projectName 88 | } 89 | } 90 | 91 | output defaultDomain string = app.outputs.defaultDomain 92 | output name string = app.outputs.name 93 | output uri string = app.outputs.uri 94 | -------------------------------------------------------------------------------- /infra/core/host/container-apps-environment.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Container Apps environment.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | @description('Name of the Application Insights resource') 7 | param applicationInsightsName string = '' 8 | 9 | @description('Specifies if Dapr is enabled') 10 | param daprEnabled bool = false 11 | 12 | @description('Name of the Log Analytics workspace') 13 | param logAnalyticsWorkspaceName string 14 | 15 | resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' = { 16 | name: name 17 | location: location 18 | tags: tags 19 | properties: { 20 | appLogsConfiguration: { 21 | destination: 'log-analytics' 22 | logAnalyticsConfiguration: { 23 | customerId: logAnalyticsWorkspace.properties.customerId 24 | sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey 25 | } 26 | } 27 | daprAIInstrumentationKey: daprEnabled && !empty(applicationInsightsName) ? applicationInsights.properties.InstrumentationKey : '' 28 | } 29 | } 30 | 31 | resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = { 32 | name: logAnalyticsWorkspaceName 33 | } 34 | 35 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (daprEnabled && !empty(applicationInsightsName)) { 36 | name: applicationInsightsName 37 | } 38 | 39 | output defaultDomain string = containerAppsEnvironment.properties.defaultDomain 40 | output id string = containerAppsEnvironment.id 41 | output name string = containerAppsEnvironment.name 42 | -------------------------------------------------------------------------------- /infra/core/host/container-apps.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Container Apps environment.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | param containerAppsEnvironmentName string 7 | param logAnalyticsWorkspaceName string 8 | param applicationInsightsName string = '' 9 | 10 | module containerAppsEnvironment 'container-apps-environment.bicep' = { 11 | name: '${name}-container-apps-environment' 12 | params: { 13 | name: containerAppsEnvironmentName 14 | location: location 15 | tags: tags 16 | logAnalyticsWorkspaceName: logAnalyticsWorkspaceName 17 | applicationInsightsName: applicationInsightsName 18 | } 19 | } 20 | 21 | output defaultDomain string = containerAppsEnvironment.outputs.defaultDomain 22 | output environmentName string = containerAppsEnvironment.outputs.name 23 | output environmentId string = containerAppsEnvironment.outputs.id 24 | -------------------------------------------------------------------------------- /infra/core/host/functions.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Function in an existing Azure App Service plan.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | // Reference Properties 7 | param applicationInsightsName string = '' 8 | param appServicePlanId string 9 | param keyVaultName string = '' 10 | param managedIdentity bool = !empty(keyVaultName) 11 | param storageAccountName string 12 | 13 | // Runtime Properties 14 | @allowed([ 15 | 'dotnet', 'dotnetcore', 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom' 16 | ]) 17 | param runtimeName string 18 | param runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}' 19 | param runtimeVersion string 20 | 21 | // Function Settings 22 | @allowed([ 23 | '~4', '~3', '~2', '~1' 24 | ]) 25 | param extensionVersion string = '~4' 26 | 27 | // Microsoft.Web/sites Properties 28 | param kind string = 'functionapp,linux' 29 | 30 | // Microsoft.Web/sites/config 31 | param allowedOrigins array = [] 32 | param alwaysOn bool = true 33 | param appCommandLine string = '' 34 | @secure() 35 | param appSettings object = {} 36 | param clientAffinityEnabled bool = false 37 | param enableOryxBuild bool = contains(kind, 'linux') 38 | param functionAppScaleLimit int = -1 39 | param linuxFxVersion string = runtimeNameAndVersion 40 | param minimumElasticInstanceCount int = -1 41 | param numberOfWorkers int = -1 42 | param scmDoBuildDuringDeployment bool = true 43 | param use32BitWorkerProcess bool = false 44 | param healthCheckPath string = '' 45 | 46 | module functions 'appservice.bicep' = { 47 | name: '${name}-functions' 48 | params: { 49 | name: name 50 | location: location 51 | tags: tags 52 | allowedOrigins: allowedOrigins 53 | alwaysOn: alwaysOn 54 | appCommandLine: appCommandLine 55 | applicationInsightsName: applicationInsightsName 56 | appServicePlanId: appServicePlanId 57 | appSettings: union(appSettings, { 58 | AzureWebJobsStorage: 'DefaultEndpointsProtocol=https;AccountName=${storage.name};AccountKey=${storage.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}' 59 | FUNCTIONS_EXTENSION_VERSION: extensionVersion 60 | FUNCTIONS_WORKER_RUNTIME: runtimeName 61 | }) 62 | clientAffinityEnabled: clientAffinityEnabled 63 | enableOryxBuild: enableOryxBuild 64 | functionAppScaleLimit: functionAppScaleLimit 65 | healthCheckPath: healthCheckPath 66 | keyVaultName: keyVaultName 67 | kind: kind 68 | linuxFxVersion: linuxFxVersion 69 | managedIdentity: managedIdentity 70 | minimumElasticInstanceCount: minimumElasticInstanceCount 71 | numberOfWorkers: numberOfWorkers 72 | runtimeName: runtimeName 73 | runtimeVersion: runtimeVersion 74 | runtimeNameAndVersion: runtimeNameAndVersion 75 | scmDoBuildDuringDeployment: scmDoBuildDuringDeployment 76 | use32BitWorkerProcess: use32BitWorkerProcess 77 | } 78 | } 79 | 80 | resource storage 'Microsoft.Storage/storageAccounts@2021-09-01' existing = { 81 | name: storageAccountName 82 | } 83 | 84 | output identityPrincipalId string = managedIdentity ? functions.outputs.identityPrincipalId : '' 85 | output name string = functions.outputs.name 86 | output uri string = functions.outputs.uri 87 | -------------------------------------------------------------------------------- /infra/core/host/ml-online-endpoint.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Container Registry.' 2 | param name string 3 | param serviceName string 4 | param location string = resourceGroup().location 5 | param tags object = {} 6 | param aiProjectName string 7 | param aiHubName string 8 | param keyVaultName string 9 | param kind string = 'Managed' 10 | param authMode string = 'Key' 11 | 12 | resource endpoint 'Microsoft.MachineLearningServices/workspaces/onlineEndpoints@2023-10-01' = { 13 | name: name 14 | location: location 15 | parent: workspace 16 | kind: kind 17 | tags: union(tags, { 'azd-service-name': serviceName }) 18 | identity: { 19 | type: 'SystemAssigned' 20 | } 21 | properties: { 22 | authMode: authMode 23 | } 24 | } 25 | 26 | var azureMLDataScientist = resourceId('Microsoft.Authorization/roleDefinitions', 'f6c7c914-8db3-469d-8ca1-694a8f32e121') 27 | 28 | resource azureMLDataScientistRoleHub 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 29 | name: guid(subscription().id, resourceGroup().id, aiHubName, name, azureMLDataScientist) 30 | scope: hubWorkspace 31 | properties: { 32 | principalId: endpoint.identity.principalId 33 | principalType: 'ServicePrincipal' 34 | roleDefinitionId: azureMLDataScientist 35 | } 36 | } 37 | 38 | resource azureMLDataScientistRoleWorkspace 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 39 | name: guid(subscription().id, resourceGroup().id, aiProjectName, name, azureMLDataScientist) 40 | scope: workspace 41 | properties: { 42 | principalId: endpoint.identity.principalId 43 | principalType: 'ServicePrincipal' 44 | roleDefinitionId: azureMLDataScientist 45 | } 46 | } 47 | 48 | var azureMLWorkspaceConnectionSecretsReader = resourceId( 49 | 'Microsoft.Authorization/roleDefinitions', 50 | 'ea01e6af-a1c1-4350-9563-ad00f8c72ec5' 51 | ) 52 | 53 | resource azureMLWorkspaceConnectionSecretsReaderRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 54 | name: guid(subscription().id, resourceGroup().id, aiProjectName, name, azureMLWorkspaceConnectionSecretsReader) 55 | scope: endpoint 56 | properties: { 57 | principalId: endpoint.identity.principalId 58 | principalType: 'ServicePrincipal' 59 | roleDefinitionId: azureMLWorkspaceConnectionSecretsReader 60 | } 61 | } 62 | 63 | module keyVaultAccess '../security/keyvault-access.bicep' = { 64 | name: '${name}-keyvault-access' 65 | params: { 66 | keyVaultName: keyVaultName 67 | principalId: endpoint.identity.principalId 68 | } 69 | } 70 | 71 | resource hubWorkspace 'Microsoft.MachineLearningServices/workspaces@2023-08-01-preview' existing = { 72 | name: aiHubName 73 | } 74 | 75 | resource workspace 'Microsoft.MachineLearningServices/workspaces@2023-08-01-preview' existing = { 76 | name: aiProjectName 77 | } 78 | 79 | output name string = endpoint.name 80 | output scoringEndpoint string = endpoint.properties.scoringUri 81 | output swaggerEndpoint string = endpoint.properties.swaggerUri 82 | output principalId string = endpoint.identity.principalId 83 | -------------------------------------------------------------------------------- /infra/core/host/staticwebapp.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Static Web Apps instance.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | param sku object = { 7 | name: 'Free' 8 | tier: 'Free' 9 | } 10 | 11 | resource web 'Microsoft.Web/staticSites@2022-03-01' = { 12 | name: name 13 | location: location 14 | tags: tags 15 | sku: sku 16 | properties: { 17 | provider: 'Custom' 18 | } 19 | } 20 | 21 | output name string = web.name 22 | output uri string = 'https://${web.properties.defaultHostname}' 23 | -------------------------------------------------------------------------------- /infra/core/monitor/applicationinsights.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Application Insights instance based on an existing Log Analytics workspace.' 2 | param name string 3 | param dashboardName string = '' 4 | param location string = resourceGroup().location 5 | param tags object = {} 6 | param logAnalyticsWorkspaceId string 7 | 8 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { 9 | name: name 10 | location: location 11 | tags: tags 12 | kind: 'web' 13 | properties: { 14 | Application_Type: 'web' 15 | WorkspaceResourceId: logAnalyticsWorkspaceId 16 | } 17 | } 18 | 19 | module applicationInsightsDashboard 'applicationinsights-dashboard.bicep' = if (!empty(dashboardName)) { 20 | name: 'application-insights-dashboard' 21 | params: { 22 | name: dashboardName 23 | location: location 24 | applicationInsightsName: applicationInsights.name 25 | } 26 | } 27 | 28 | output connectionString string = applicationInsights.properties.ConnectionString 29 | output id string = applicationInsights.id 30 | output instrumentationKey string = applicationInsights.properties.InstrumentationKey 31 | output name string = applicationInsights.name 32 | -------------------------------------------------------------------------------- /infra/core/monitor/loganalytics.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates a Log Analytics workspace.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { 7 | name: name 8 | location: location 9 | tags: tags 10 | properties: any({ 11 | retentionInDays: 30 12 | features: { 13 | searchVersion: 1 14 | } 15 | sku: { 16 | name: 'PerGB2018' 17 | } 18 | }) 19 | } 20 | 21 | output id string = logAnalytics.id 22 | output name string = logAnalytics.name 23 | -------------------------------------------------------------------------------- /infra/core/monitor/monitoring.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Application Insights instance and a Log Analytics workspace.' 2 | param logAnalyticsName string 3 | param applicationInsightsName string 4 | param applicationInsightsDashboardName string = '' 5 | param location string = resourceGroup().location 6 | param tags object = {} 7 | 8 | module logAnalytics 'loganalytics.bicep' = { 9 | name: 'loganalytics' 10 | params: { 11 | name: logAnalyticsName 12 | location: location 13 | tags: tags 14 | } 15 | } 16 | 17 | module applicationInsights 'applicationinsights.bicep' = { 18 | name: 'applicationinsights' 19 | params: { 20 | name: applicationInsightsName 21 | location: location 22 | tags: tags 23 | dashboardName: applicationInsightsDashboardName 24 | logAnalyticsWorkspaceId: logAnalytics.outputs.id 25 | } 26 | } 27 | 28 | output applicationInsightsConnectionString string = applicationInsights.outputs.connectionString 29 | output applicationInsightsId string = applicationInsights.outputs.id 30 | output applicationInsightsInstrumentationKey string = applicationInsights.outputs.instrumentationKey 31 | output applicationInsightsName string = applicationInsights.outputs.name 32 | output logAnalyticsWorkspaceId string = logAnalytics.outputs.id 33 | output logAnalyticsWorkspaceName string = logAnalytics.outputs.name 34 | -------------------------------------------------------------------------------- /infra/core/networking/cdn-endpoint.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Adds an endpoint to an Azure CDN profile.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | @description('The name of the CDN profile resource') 7 | @minLength(1) 8 | param cdnProfileName string 9 | 10 | @description('Delivery policy rules') 11 | param deliveryPolicyRules array = [] 12 | 13 | @description('The origin URL for the endpoint') 14 | @minLength(1) 15 | param originUrl string 16 | 17 | resource endpoint 'Microsoft.Cdn/profiles/endpoints@2022-05-01-preview' = { 18 | parent: cdnProfile 19 | name: name 20 | location: location 21 | tags: tags 22 | properties: { 23 | originHostHeader: originUrl 24 | isHttpAllowed: false 25 | isHttpsAllowed: true 26 | queryStringCachingBehavior: 'UseQueryString' 27 | optimizationType: 'GeneralWebDelivery' 28 | origins: [ 29 | { 30 | name: replace(originUrl, '.', '-') 31 | properties: { 32 | hostName: originUrl 33 | originHostHeader: originUrl 34 | priority: 1 35 | weight: 1000 36 | enabled: true 37 | } 38 | } 39 | ] 40 | deliveryPolicy: { 41 | rules: deliveryPolicyRules 42 | } 43 | } 44 | } 45 | 46 | resource cdnProfile 'Microsoft.Cdn/profiles@2022-05-01-preview' existing = { 47 | name: cdnProfileName 48 | } 49 | 50 | output id string = endpoint.id 51 | output name string = endpoint.name 52 | output uri string = 'https://${endpoint.properties.hostName}' 53 | -------------------------------------------------------------------------------- /infra/core/networking/cdn-profile.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure CDN profile.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | @description('The pricing tier of this CDN profile') 7 | @allowed([ 8 | 'Custom_Verizon' 9 | 'Premium_AzureFrontDoor' 10 | 'Premium_Verizon' 11 | 'StandardPlus_955BandWidth_ChinaCdn' 12 | 'StandardPlus_AvgBandWidth_ChinaCdn' 13 | 'StandardPlus_ChinaCdn' 14 | 'Standard_955BandWidth_ChinaCdn' 15 | 'Standard_Akamai' 16 | 'Standard_AvgBandWidth_ChinaCdn' 17 | 'Standard_AzureFrontDoor' 18 | 'Standard_ChinaCdn' 19 | 'Standard_Microsoft' 20 | 'Standard_Verizon' 21 | ]) 22 | param sku string = 'Standard_Microsoft' 23 | 24 | resource profile 'Microsoft.Cdn/profiles@2022-05-01-preview' = { 25 | name: name 26 | location: location 27 | tags: tags 28 | sku: { 29 | name: sku 30 | } 31 | } 32 | 33 | output id string = profile.id 34 | output name string = profile.name 35 | -------------------------------------------------------------------------------- /infra/core/networking/cdn.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure CDN profile with a single endpoint.' 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | @description('Name of the CDN endpoint resource') 6 | param cdnEndpointName string 7 | 8 | @description('Name of the CDN profile resource') 9 | param cdnProfileName string 10 | 11 | @description('Delivery policy rules') 12 | param deliveryPolicyRules array = [] 13 | 14 | @description('Origin URL for the CDN endpoint') 15 | param originUrl string 16 | 17 | module cdnProfile 'cdn-profile.bicep' = { 18 | name: 'cdn-profile' 19 | params: { 20 | name: cdnProfileName 21 | location: location 22 | tags: tags 23 | } 24 | } 25 | 26 | module cdnEndpoint 'cdn-endpoint.bicep' = { 27 | name: 'cdn-endpoint' 28 | params: { 29 | name: cdnEndpointName 30 | location: location 31 | tags: tags 32 | cdnProfileName: cdnProfile.outputs.name 33 | originUrl: originUrl 34 | deliveryPolicyRules: deliveryPolicyRules 35 | } 36 | } 37 | 38 | output endpointName string = cdnEndpoint.outputs.name 39 | output endpointId string = cdnEndpoint.outputs.id 40 | output profileName string = cdnProfile.outputs.name 41 | output profileId string = cdnProfile.outputs.id 42 | output uri string = cdnEndpoint.outputs.uri 43 | -------------------------------------------------------------------------------- /infra/core/search/search-services.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure AI Search instance.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | param sku object = { 6 | name: 'standard' 7 | } 8 | 9 | param authOptions object = {} 10 | param disableLocalAuth bool = false 11 | param disabledDataExfiltrationOptions array = [] 12 | param encryptionWithCmk object = { 13 | enforcement: 'Unspecified' 14 | } 15 | @allowed([ 16 | 'default' 17 | 'highDensity' 18 | ]) 19 | param hostingMode string = 'default' 20 | param networkRuleSet object = { 21 | bypass: 'None' 22 | ipRules: [] 23 | } 24 | param partitionCount int = 1 25 | @allowed([ 26 | 'enabled' 27 | 'disabled' 28 | ]) 29 | param publicNetworkAccess string = 'enabled' 30 | param replicaCount int = 1 31 | @allowed([ 32 | 'disabled' 33 | 'free' 34 | 'standard' 35 | ]) 36 | param semanticSearch string = 'disabled' 37 | 38 | var searchIdentityProvider = (sku.name == 'free') ? null : { 39 | type: 'SystemAssigned' 40 | } 41 | 42 | 43 | resource search 'Microsoft.Search/searchServices@2024-06-01-preview' = { 44 | name: name 45 | location: location 46 | tags: tags 47 | sku: { 48 | name: 'basic' 49 | } 50 | identity: searchIdentityProvider 51 | properties: { 52 | authOptions: authOptions 53 | disableLocalAuth: disableLocalAuth 54 | disabledDataExfiltrationOptions: disabledDataExfiltrationOptions 55 | encryptionWithCmk: encryptionWithCmk 56 | hostingMode: hostingMode 57 | networkRuleSet: networkRuleSet 58 | partitionCount: partitionCount 59 | publicNetworkAccess: publicNetworkAccess 60 | replicaCount: replicaCount 61 | semanticSearch: semanticSearch 62 | } 63 | } 64 | 65 | 66 | output id string = search.id 67 | output endpoint string = 'https://${name}.search.windows.net/' 68 | output name string = search.name 69 | output principalId string = !empty(searchIdentityProvider) ? search.identity.principalId : '' 70 | 71 | -------------------------------------------------------------------------------- /infra/core/security/aks-managed-cluster-access.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Assigns RBAC role to the specified AKS cluster and principal.' 2 | param clusterName string 3 | param principalId string 4 | 5 | var aksClusterAdminRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b1ff04bb-8a4e-4dc4-8eb5-8693973ce19b') 6 | 7 | resource aksRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 8 | scope: aksCluster // Use when specifying a scope that is different than the deployment scope 9 | name: guid(subscription().id, resourceGroup().id, principalId, aksClusterAdminRole) 10 | properties: { 11 | roleDefinitionId: aksClusterAdminRole 12 | principalType: 'User' 13 | principalId: principalId 14 | } 15 | } 16 | 17 | resource aksCluster 'Microsoft.ContainerService/managedClusters@2023-10-02-preview' existing = { 18 | name: clusterName 19 | } 20 | -------------------------------------------------------------------------------- /infra/core/security/appinsights-access.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Assigns MonitoringMetricsContributor role to Application Insights.' 2 | param appInsightsName string 3 | param principalId string 4 | @allowed([ 5 | 'Device' 6 | 'ForeignGroup' 7 | 'Group' 8 | 'ServicePrincipal' 9 | 'User' 10 | '' 11 | ]) 12 | param principalType string = '' 13 | 14 | resource appInsights 'Microsoft.Insights/components@2020-02-02' existing = { 15 | name: appInsightsName 16 | } 17 | 18 | var monitoringMetricsContributorRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '749f88d5-cbae-40b8-bcfc-e573ddc772fa') 19 | 20 | resource monitoringMetricsContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 21 | scope: appInsights // Use when specifying a scope that is different than the deployment scope 22 | name: guid(subscription().id, resourceGroup().id, principalId, monitoringMetricsContributorRole) 23 | properties: { 24 | principalType: principalType 25 | roleDefinitionId: monitoringMetricsContributorRole 26 | principalId: principalId 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /infra/core/security/configstore-access.bicep: -------------------------------------------------------------------------------- 1 | @description('Name of Azure App Configuration store') 2 | param configStoreName string 3 | 4 | @description('The principal ID of the service principal to assign the role to') 5 | param principalId string 6 | 7 | resource configStore 'Microsoft.AppConfiguration/configurationStores@2023-03-01' existing = { 8 | name: configStoreName 9 | } 10 | 11 | var configStoreDataReaderRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '516239f1-63e1-4d78-a4de-a74fb236a071') 12 | 13 | resource configStoreDataReaderRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 14 | name: guid(subscription().id, resourceGroup().id, principalId, configStoreDataReaderRole) 15 | scope: configStore 16 | properties: { 17 | roleDefinitionId: configStoreDataReaderRole 18 | principalId: principalId 19 | principalType: 'ServicePrincipal' 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /infra/core/security/keyvault-access.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Assigns an Azure Key Vault access policy.' 2 | param name string = 'add' 3 | 4 | param keyVaultName string 5 | param permissions object = { secrets: [ 'get', 'list' ] } 6 | param principalId string 7 | 8 | resource keyVaultAccessPolicies 'Microsoft.KeyVault/vaults/accessPolicies@2022-07-01' = { 9 | parent: keyVault 10 | name: name 11 | properties: { 12 | accessPolicies: [ { 13 | objectId: principalId 14 | tenantId: subscription().tenantId 15 | permissions: permissions 16 | } ] 17 | } 18 | } 19 | 20 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { 21 | name: keyVaultName 22 | } 23 | -------------------------------------------------------------------------------- /infra/core/security/keyvault-secret.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates or updates a secret in an Azure Key Vault.' 2 | param name string 3 | param tags object = {} 4 | param keyVaultName string 5 | param contentType string = 'string' 6 | @description('The value of the secret. Provide only derived values like blob storage access, but do not hard code any secrets in your templates') 7 | @secure() 8 | param secretValue string 9 | 10 | param enabled bool = true 11 | param exp int = 0 12 | param nbf int = 0 13 | 14 | resource keyVaultSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { 15 | name: name 16 | tags: tags 17 | parent: keyVault 18 | properties: { 19 | attributes: { 20 | enabled: enabled 21 | exp: exp 22 | nbf: nbf 23 | } 24 | contentType: contentType 25 | value: secretValue 26 | } 27 | } 28 | 29 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { 30 | name: keyVaultName 31 | } 32 | -------------------------------------------------------------------------------- /infra/core/security/keyvault.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Key Vault.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | param principalId string = '' 7 | 8 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = { 9 | name: name 10 | location: location 11 | tags: tags 12 | properties: { 13 | tenantId: subscription().tenantId 14 | sku: { family: 'A', name: 'standard' } 15 | accessPolicies: !empty(principalId) ? [ 16 | { 17 | objectId: principalId 18 | permissions: { secrets: [ 'get', 'list' ] } 19 | tenantId: subscription().tenantId 20 | } 21 | ] : [] 22 | } 23 | } 24 | 25 | output endpoint string = keyVault.properties.vaultUri 26 | output id string = keyVault.id 27 | output name string = keyVault.name 28 | -------------------------------------------------------------------------------- /infra/core/security/registry-access.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Assigns ACR Pull permissions to access an Azure Container Registry.' 2 | param containerRegistryName string 3 | param principalId string 4 | 5 | var acrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') 6 | 7 | resource aksAcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 8 | scope: containerRegistry // Use when specifying a scope that is different than the deployment scope 9 | name: guid(subscription().id, resourceGroup().id, principalId, acrPullRole) 10 | properties: { 11 | roleDefinitionId: acrPullRole 12 | principalType: 'ServicePrincipal' 13 | principalId: principalId 14 | } 15 | } 16 | 17 | resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' existing = { 18 | name: containerRegistryName 19 | } 20 | -------------------------------------------------------------------------------- /infra/core/security/role.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates a role assignment for a service principal.' 2 | param principalId string 3 | 4 | @allowed([ 5 | 'Device' 6 | 'ForeignGroup' 7 | 'Group' 8 | 'ServicePrincipal' 9 | 'User' 10 | '' 11 | ]) 12 | param principalType string 13 | param roleDefinitionId string 14 | 15 | resource role 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 16 | name: guid(subscription().id, resourceGroup().id, principalId, roleDefinitionId) 17 | properties: { 18 | principalId: principalId 19 | principalType: principalType 20 | roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /infra/core/storage/storage-account.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure storage account.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | @allowed([ 7 | 'Cool' 8 | 'Hot' 9 | 'Premium' ]) 10 | param accessTier string = 'Hot' 11 | param allowBlobPublicAccess bool = false 12 | param allowCrossTenantReplication bool = true 13 | param allowSharedKeyAccess bool = false 14 | param containers array = [] 15 | param corsRules array = [] 16 | param defaultToOAuthAuthentication bool = false 17 | param deleteRetentionPolicy object = {} 18 | @allowed([ 'AzureDnsZone', 'Standard' ]) 19 | param dnsEndpointType string = 'Standard' 20 | param files array = [] 21 | param kind string = 'StorageV2' 22 | param minimumTlsVersion string = 'TLS1_2' 23 | param queues array = [] 24 | param shareDeleteRetentionPolicy object = {} 25 | param supportsHttpsTrafficOnly bool = true 26 | param tables array = [] 27 | param networkAcls object = { 28 | bypass: 'AzureServices' 29 | defaultAction: 'Allow' 30 | } 31 | @allowed([ 'Enabled', 'Disabled' ]) 32 | param publicNetworkAccess string = 'Enabled' 33 | param sku object = { name: 'Standard_LRS' } 34 | 35 | resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = { 36 | name: name 37 | location: location 38 | tags: tags 39 | kind: kind 40 | sku: sku 41 | properties: { 42 | accessTier: accessTier 43 | allowBlobPublicAccess: allowBlobPublicAccess 44 | allowCrossTenantReplication: allowCrossTenantReplication 45 | allowSharedKeyAccess: allowSharedKeyAccess 46 | defaultToOAuthAuthentication: defaultToOAuthAuthentication 47 | dnsEndpointType: dnsEndpointType 48 | minimumTlsVersion: minimumTlsVersion 49 | networkAcls: networkAcls 50 | publicNetworkAccess: publicNetworkAccess 51 | supportsHttpsTrafficOnly: supportsHttpsTrafficOnly 52 | } 53 | 54 | resource blobServices 'blobServices' = if (!empty(containers)) { 55 | name: 'default' 56 | properties: { 57 | cors: { 58 | corsRules: corsRules 59 | } 60 | deleteRetentionPolicy: deleteRetentionPolicy 61 | } 62 | resource container 'containers' = [for container in containers: { 63 | name: container.name 64 | properties: { 65 | publicAccess: contains(container, 'publicAccess') ? container.publicAccess : 'None' 66 | } 67 | }] 68 | } 69 | 70 | resource fileServices 'fileServices' = if (!empty(files)) { 71 | name: 'default' 72 | properties: { 73 | cors: { 74 | corsRules: corsRules 75 | } 76 | shareDeleteRetentionPolicy: shareDeleteRetentionPolicy 77 | } 78 | } 79 | 80 | resource queueServices 'queueServices' = if (!empty(queues)) { 81 | name: 'default' 82 | properties: { 83 | 84 | } 85 | resource queue 'queues' = [for queue in queues: { 86 | name: queue.name 87 | properties: { 88 | metadata: {} 89 | } 90 | }] 91 | } 92 | 93 | resource tableServices 'tableServices' = if (!empty(tables)) { 94 | name: 'default' 95 | properties: {} 96 | } 97 | } 98 | 99 | output id string = storage.id 100 | output name string = storage.name 101 | output primaryEndpoints object = storage.properties.primaryEndpoints 102 | -------------------------------------------------------------------------------- /infra/core/testing/loadtesting.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param managedIdentity bool = false 4 | param tags object = {} 5 | 6 | resource loadTest 'Microsoft.LoadTestService/loadTests@2022-12-01' = { 7 | name: name 8 | location: location 9 | tags: tags 10 | identity: { type: managedIdentity ? 'SystemAssigned' : 'None' } 11 | properties: { 12 | } 13 | } 14 | 15 | output loadTestingName string = loadTest.name 16 | -------------------------------------------------------------------------------- /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 | "principalId": { 12 | "value": "${AZURE_PRINCIPAL_ID}" 13 | }, 14 | "resourceGroupName": { 15 | "value": "${AZURE_RESOURCE_GROUP}" 16 | }, 17 | "aiHubName": { 18 | "value": "${AZURE_AIHUB_NAME}" 19 | }, 20 | "aiProjectName": { 21 | "value": "${AZURE_AIPROJECT_NAME}" 22 | }, 23 | "aiServicesName": { 24 | "value": "${AZURE_AISERVICES_NAME}" 25 | }, 26 | "searchServiceName": { 27 | "value": "${AZURE_SEARCH_SERVICE_NAME}" 28 | }, 29 | "applicationInsightsName": { 30 | "value": "${AZURE_APPLICATION_INSIGHTS_NAME}" 31 | }, 32 | "containerRegistryName": { 33 | "value": "${AZURE_CONTAINER_REGISTRY_NAME}" 34 | }, 35 | "keyVaultName": { 36 | "value": "${AZURE_KEYVAULT_NAME}" 37 | }, 38 | "storageAccountName": { 39 | "value": "${AZURE_STORAGE_ACCOUNT_NAME}" 40 | }, 41 | "logAnalyticsWorkspaceName": { 42 | "value": "${AZURE_LOG_ANALYTICS_WORKSPACE_NAME}" 43 | }, 44 | "useContainerRegistry": { 45 | "value": "${USE_CONTAINER_REGISTRY=true}" 46 | }, 47 | "useApplicationInsights": { 48 | "value": "${USE_APPLICATION_INSIGHTS=true}" 49 | }, 50 | "useSearchService": { 51 | "value": "${USE_AZURE_AI_SEARCH_SERVICE=false}" 52 | }, 53 | "chatDeploymentName": { 54 | "value": "${AZURE_AI_CHAT_DEPLOYMENT_NAME}" 55 | }, 56 | "chatModelFormat": { 57 | "value": "${AZURE_AI_CHAT_MODEL_FORMAT}" 58 | }, 59 | "chatModelName": { 60 | "value": "${AZURE_AI_CHAT_MODEL_NAME}" 61 | }, 62 | "chatModelVersion": { 63 | "value": "${AZURE_AI_CHAT_MODEL_VERSION}" 64 | }, 65 | "chatDeploymentSku": { 66 | "value": "${AZURE_AI_CHAT_DEPLOYMENT_SKU}" 67 | }, 68 | "chatDeploymentCapacity": { 69 | "value": "${AZURE_AI_CHAT_DEPLOYMENT_CAPACITY}" 70 | }, 71 | "embeddingDeploymentName": { 72 | "value": "${AZURE_AI_EMBED_DEPLOYMENT_NAME}" 73 | }, 74 | "embedModelFormat": { 75 | "value": "${AZURE_AI_EMBED_MODEL_FORMAT}" 76 | }, 77 | "embedModelName": { 78 | "value": "${AZURE_AI_EMBED_MODEL_NAME}" 79 | }, 80 | "embedModelVersion": { 81 | "value": "${AZURE_AI_EMBED_MODEL_VERSION}" 82 | }, 83 | "embedDeploymentSku": { 84 | "value": "${AZURE_AI_EMBED_DEPLOYMENT_SKU}" 85 | }, 86 | "embedDeploymentCapacity": { 87 | "value": "${AZURE_AI_EMBED_DEPLOYMENT_CAPACITY}" 88 | }, 89 | "embeddingDeploymentDimensions": { 90 | "value": "${AZURE_AI_EMBED_DIMENSIONS=100}" 91 | }, 92 | "apiAppExists": { 93 | "value": "${SERVICE_API_RESOURCE_EXISTS=false}" 94 | }, 95 | "azureExistingAIProjectResourceId": { 96 | "value": "${AZURE_EXISTING_AIPROJECT_RESOURCE_ID}" 97 | }, 98 | "aiSearchIndexName": { 99 | "value": "${AZURE_AI_SEARCH_INDEX_NAME=index_sample}" 100 | }, 101 | "enableAzureMonitorTracing": { 102 | "value": "${ENABLE_AZURE_MONITOR_TRACING=false}" 103 | }, 104 | "azureTracingGenAIContentRecordingEnabled": { 105 | "value": "${AZURE_TRACING_GEN_AI_CONTENT_RECORDING_ENABLED=false}" 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | line-length = 120 3 | target-version = "py39" 4 | lint.select = ["E", "F", "I", "UP"] 5 | lint.ignore = ["D203"] 6 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r src/requirements.txt 2 | ruff 3 | pre-commit 4 | -------------------------------------------------------------------------------- /scripts/resolve_model_quota.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [string]$Location, 3 | [string]$Model, 4 | [string]$Format, 5 | [string]$DeploymentType, 6 | [string]$CapacityEnvVarName, 7 | [int]$Capacity 8 | ) 9 | 10 | # Verify all required parameters are provided 11 | $MissingParams = @() 12 | 13 | if (-not $Location) { 14 | $MissingParams += "location" 15 | } 16 | 17 | if (-not $Model) { 18 | $MissingParams += "model" 19 | } 20 | 21 | if (-not $Capacity) { 22 | $MissingParams += "capacity" 23 | } 24 | 25 | if (-not $Format) { 26 | $MissingParams += "format" 27 | } 28 | 29 | if (-not $DeploymentType) { 30 | $MissingParams += "deployment-type" 31 | } 32 | 33 | if ($MissingParams.Count -gt 0) { 34 | Write-Error "❌ ERROR: Missing required parameters: $($MissingParams -join ', ')" 35 | Write-Host "Usage: .\resolve_model_quota.ps1 -Location -Model -Format -Capacity -CapacityEnvVarName [-DeploymentType ]" 36 | exit 1 37 | } 38 | 39 | if ($DeploymentType -ne "Standard" -and $DeploymentType -ne "GlobalStandard") { 40 | Write-Error "❌ ERROR: Invalid deployment type: $DeploymentType. Allowed values are 'Standard' or 'GlobalStandard'." 41 | exit 1 42 | } 43 | 44 | $ModelType = "$Format.$DeploymentType.$Model" 45 | 46 | Write-Host "🔍 Checking quota for $ModelType in $Location ..." 47 | 48 | # Get model quota information 49 | $ModelInfo = az cognitiveservices usage list --location $Location --query "[?name.value=='$ModelType'] | [0]" --output json | ConvertFrom-Json 50 | if (-not $ModelInfo) { 51 | Write-Error "❌ ERROR: No quota information found for model: $Model in location: $Location for model type: $ModelType." 52 | exit 1 53 | } 54 | 55 | 56 | $CurrentValue =$ModelInfo.currentValue 57 | $Limit = $ModelInfo.limit 58 | 59 | $CurrentValue = [int]($CurrentValue -replace '\.0+$', '') # Remove decimals 60 | $Limit = [int]($Limit -replace '\.0+$', '') # Remove decimals 61 | 62 | $Available = $Limit - $CurrentValue 63 | Write-Host "✅ Model available - Model: $ModelType | Used: $CurrentValue | Limit: $Limit | Available: $Available" 64 | 65 | 66 | if ($Available -lt $Capacity) { 67 | 68 | # Determine newCapacity based on user prompt or availability 69 | # This logic assumes it will replace the subsequent lines that also set $newCapacity. 70 | if ($Available -ge 1) { 71 | $validInput = $false 72 | # $newCapacity will be set by user input if $Available >= 1 73 | do { 74 | $userInput = Read-Host "⚠️ ERROR: Insufficient quota. Available: $Available (in thousands of tokens per minute). Ideal is $Capacity. Please enter a new capacity (integer between 1 and $Available): " 75 | 76 | $parsedInt = 0 # Variable to hold the parsed integer 77 | if ([int]::TryParse($userInput, [ref]$parsedInt)) { 78 | if ($parsedInt -ge 1 -and $parsedInt -le $Available) { 79 | $newCapacity = $parsedInt # Set $newCapacity to the user's valid choice 80 | $validInput = $true 81 | } else { 82 | Write-Warning "Invalid input. '$parsedInt' is not between 1 and $Available. Please try again." 83 | } 84 | } else { 85 | Write-Warning "Invalid input: '$userInput' is not a valid integer. Please try again." 86 | } 87 | } while (-not $validInput) 88 | azd env set $CapacityEnvVarName $newCapacity 89 | } else { 90 | # This case handles when $Available is 0 or less (though quota is typically non-negative). 91 | # Prompting for "between 1 and $Available" is not possible. 92 | Write-Error "❌ ERROR: Insufficient quota for model: $Model in location: $Location. Available: less than 1 (in thousands of tokens per minute), Requested: $Capacity." 93 | exit 1 94 | } 95 | 96 | } else { 97 | Write-Host "✅ Sufficient quota for model: $Model in location: $Location. Available: $Available, Requested: $Capacity." 98 | } 99 | 100 | exit 0 101 | -------------------------------------------------------------------------------- /scripts/resolve_model_quota.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Initialize variables 4 | Location="" 5 | Model="" 6 | Format="" 7 | DeploymentType="" 8 | CapacityEnvVarName="" 9 | Capacity="" 10 | 11 | # Parse arguments 12 | while [[ $# -gt 0 ]]; do 13 | case "$1" in 14 | -Location) 15 | Location="$2" 16 | shift 2 17 | ;; 18 | -Model) 19 | Model="$2" 20 | shift 2 21 | ;; 22 | -DeploymentType) 23 | DeploymentType="$2" 24 | shift 2 25 | ;; 26 | -CapacityEnvVarName) 27 | CapacityEnvVarName="$2" 28 | shift 2 29 | ;; 30 | -Capacity) 31 | Capacity="$2" 32 | shift 2 33 | ;; 34 | -Format) 35 | Format="$2" 36 | shift 2 37 | ;; 38 | *) 39 | echo "❌ ERROR: Unknown parameter: $1" 40 | exit 1 41 | ;; 42 | esac 43 | done 44 | 45 | # Check for missing required parameters 46 | MissingParams=() 47 | [[ -z "$Location" ]] && MissingParams+=("location") 48 | [[ -z "$Model" ]] && MissingParams+=("model") 49 | [[ -z "$Capacity" ]] && MissingParams+=("capacity") 50 | [[ -z "$DeploymentType" ]] && MissingParams+=("deployment-type") 51 | 52 | if [[ ${#MissingParams[@]} -gt 0 ]]; then 53 | echo "❌ ERROR: Missing required parameters: ${MissingParams[*]}" 54 | echo "Usage: ./resolve_model_quota.sh -Location -Model -Format -Capacity -CapacityEnvVarName [-DeploymentType ]" 55 | exit 1 56 | fi 57 | 58 | if [[ "$DeploymentType" != "Standard" && "$DeploymentType" != "GlobalStandard" ]]; then 59 | echo "❌ ERROR: Invalid deployment type: $DeploymentType. Allowed values are 'Standard' or 'GlobalStandard'." 60 | exit 1 61 | fi 62 | 63 | ModelType="$Format.$DeploymentType.$Model" 64 | 65 | echo "🔍 Checking quota for $ModelType in $Location ..." 66 | 67 | ModelInfo=$(az cognitiveservices usage list --location "$Location" --query "[?name.value=='$ModelType']" --output json | tr '[:upper:]' '[:lower:]') 68 | 69 | if [ -z "$ModelInfo" ]; then 70 | echo "❌ ERROR: No quota information found for model: $Model in location: $Location for model type: $ModelType." 71 | exit 1 72 | fi 73 | 74 | CurrentValue=$(echo "$ModelInfo" | awk -F': ' '/"currentvalue"/ {print $2}' | tr -d ',' | tr -d ' ') 75 | Limit=$(echo "$ModelInfo" | awk -F': ' '/"limit"/ {print $2}' | tr -d ',' | tr -d ' ') 76 | 77 | CurrentValue=${CurrentValue:-0} 78 | Limit=${Limit:-0} 79 | 80 | CurrentValue=$(echo "$CurrentValue" | cut -d'.' -f1) 81 | Limit=$(echo "$Limit" | cut -d'.' -f1) 82 | 83 | Available=$((Limit - CurrentValue)) 84 | echo "✅ Model available - Model: $ModelType | Used: $CurrentValue | Limit: $Limit | Available: $Available" 85 | 86 | if [ "$Available" -lt "$Capacity" ]; then 87 | if [ "$Available" -ge 1 ]; then 88 | validInput=false 89 | while [ "$validInput" = false ]; do 90 | read -p "⚠️ ERROR: Insufficient quota. Available: $Available (in thousands of tokens per minute). Ideal was $Capacity. Please enter a new capacity (integer between 1 and $Available): " userInput 91 | 92 | if [[ "$userInput" =~ ^[0-9]+$ ]]; then 93 | if [ "$userInput" -ge 1 ] && [ "$userInput" -le "$Available" ]; then 94 | newCapacity=$userInput 95 | validInput=true 96 | else 97 | echo "⚠️ WARNING: Invalid input. '$userInput' is not between 1 and $Available. Please try again." >&2 98 | fi 99 | else 100 | echo "⚠️ WARNING: Invalid input: '$userInput' is not a valid integer. Please try again." >&2 101 | fi 102 | done 103 | azd env set "$CapacityEnvVarName" "$newCapacity" 104 | else 105 | echo "❌ ERROR: Insufficient quota for model: $Model in location: $Location. Available: less than 1 (in thousands of tokens per minute), Requested: $Capacity." >&2 106 | exit 1 107 | fi 108 | else 109 | echo "✅ Sufficient quota for model: $Model in location: $Location. Available: $Available, Requested: $Capacity." 110 | fi 111 | 112 | echo "Set exit code to 0" 113 | exit 0 114 | -------------------------------------------------------------------------------- /scripts/set_default_models.ps1: -------------------------------------------------------------------------------- 1 | $SubscriptionId = ([System.Environment]::GetEnvironmentVariable('AZURE_SUBSCRIPTION_ID', "Process")) 2 | $Location = ([System.Environment]::GetEnvironmentVariable('AZURE_LOCATION', "Process")) 3 | 4 | $Errors = 0 5 | 6 | if (-not $SubscriptionId) { 7 | Write-Error "❌ ERROR: Missing AZURE_SUBSCRIPTION_ID" 8 | $Errors++ 9 | } 10 | 11 | if (-not $Location) { 12 | Write-Error "❌ ERROR: Missing AZURE_LOCATION" 13 | $Errors++ 14 | } 15 | 16 | if ($Errors -gt 0) { 17 | exit 1 18 | } 19 | 20 | 21 | $defaultEnvVars = @{ 22 | AZURE_AI_EMBED_DEPLOYMENT_NAME = 'text-embedding-3-small' 23 | AZURE_AI_EMBED_MODEL_NAME = 'text-embedding-3-small' 24 | AZURE_AI_EMBED_MODEL_FORMAT = 'OpenAI' 25 | AZURE_AI_EMBED_MODEL_VERSION = '1' 26 | AZURE_AI_EMBED_DEPLOYMENT_SKU = 'Standard' 27 | AZURE_AI_EMBED_DEPLOYMENT_CAPACITY = '50' 28 | AZURE_AI_CHAT_DEPLOYMENT_NAME = 'gpt-4o-mini' 29 | AZURE_AI_CHAT_MODEL_NAME = 'gpt-4o-mini' 30 | AZURE_AI_CHAT_MODEL_VERSION = '2024-07-18' 31 | AZURE_AI_CHAT_MODEL_FORMAT = 'OpenAI' 32 | AZURE_AI_CHAT_DEPLOYMENT_SKU = 'GlobalStandard' 33 | AZURE_AI_CHAT_DEPLOYMENT_CAPACITY = '80' 34 | } 35 | 36 | $envVars = @{} 37 | 38 | foreach ($key in $defaultEnvVars.Keys) { 39 | $val = [System.Environment]::GetEnvironmentVariable($key, "Process") 40 | $envVars[$key] = $val 41 | if (-not $val) { 42 | $envVars[$key] = $defaultEnvVars[$key] 43 | } 44 | azd env set $key $envVars[$key] 45 | } 46 | 47 | # --- If we do not use existing AI Project, we don't deploy models, so skip validation --- 48 | $resourceId = [System.Environment]::GetEnvironmentVariable('AZURE_EXISTING_AIPROJECT_RESOURCE_ID', "Process") 49 | if (-not [string]::IsNullOrEmpty($resourceId)) { 50 | Write-Host "✅ AZURE_EXISTING_AIPROJECT_RESOURCE_ID is set, skipping model deployment validation." 51 | exit 0 52 | } 53 | 54 | $chatDeployment = @{ 55 | name = $envVars.AZURE_AI_CHAT_DEPLOYMENT_NAME 56 | model = @{ 57 | name = $envVars.AZURE_AI_CHAT_MODEL_NAME 58 | version = $envVars.AZURE_AI_CHAT_MODEL_VERSION 59 | format = $envVars.AZURE_AI_CHAT_MODEL_FORMAT 60 | } 61 | sku = @{ 62 | name = $envVars.AZURE_AI_CHAT_DEPLOYMENT_SKU 63 | capacity = $envVars.AZURE_AI_CHAT_DEPLOYMENT_CAPACITY 64 | } 65 | capacity_env_var_name = 'AZURE_AI_CHAT_DEPLOYMENT_CAPACITY' 66 | } 67 | 68 | 69 | 70 | $aiModelDeployments = @($chatDeployment) 71 | 72 | $useSearchService = ([System.Environment]::GetEnvironmentVariable('USE_AZURE_AI_SEARCH_SERVICE', "Process")) 73 | 74 | if ($useSearchService -eq 'true') { 75 | $embedDeployment = @{ 76 | name = $envVars.AZURE_AI_EMBED_DEPLOYMENT_NAME 77 | model = @{ 78 | name = $envVars.AZURE_AI_EMBED_MODEL_NAME 79 | version = $envVars.AZURE_AI_EMBED_MODEL_VERSION 80 | format = $envVars.AZURE_AI_EMBED_MODEL_FORMAT 81 | } 82 | sku = @{ 83 | name = $envVars.AZURE_AI_EMBED_DEPLOYMENT_SKU 84 | capacity = $envVars.AZURE_AI_EMBED_DEPLOYMENT_CAPACITY 85 | min_capacity = 30 86 | } 87 | capacity_env_var_name = 'AZURE_AI_EMBED_DEPLOYMENT_CAPACITY' 88 | } 89 | 90 | $aiModelDeployments += $embedDeployment 91 | } 92 | 93 | 94 | az account set --subscription $SubscriptionId 95 | Write-Host "🎯 Active Subscription: $(az account show --query '[name, id]' --output tsv)" 96 | 97 | $QuotaAvailable = $true 98 | 99 | foreach ($deployment in $aiModelDeployments) { 100 | $name = $deployment.name 101 | $model = $deployment.model.name 102 | $type = $deployment.sku.name 103 | $format = $deployment.model.format 104 | $capacity = $deployment.sku.capacity 105 | $capacity_env_var_name = $deployment.capacity_env_var_name 106 | Write-Host "🔍 Validating model deployment: $name ..." 107 | & .\scripts\resolve_model_quota.ps1 -Location $Location -Model $model -Format $format -Capacity $capacity -CapacityEnvVarName $capacity_env_var_name -DeploymentType $type 108 | 109 | # Check if the script failed 110 | if ($LASTEXITCODE -ne 0) { 111 | Write-Error "❌ ERROR: Quota validation failed for model deployment: $name" 112 | $QuotaAvailable = $false 113 | } 114 | } 115 | 116 | 117 | if (-not $QuotaAvailable) { 118 | exit 1 119 | } else { 120 | Write-Host "✅ All model deployments passed quota validation successfully." 121 | exit 0 122 | } -------------------------------------------------------------------------------- /scripts/set_default_models.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # --- Check Required Environment Variables --- 6 | SubscriptionId="${AZURE_SUBSCRIPTION_ID}" 7 | Location="${AZURE_LOCATION}" 8 | 9 | Errors=0 10 | 11 | if [ -z "$SubscriptionId" ]; then 12 | echo "❌ ERROR: Missing AZURE_SUBSCRIPTION_ID" >&2 13 | Errors=$((Errors + 1)) 14 | fi 15 | 16 | if [ -z "$Location" ]; then 17 | echo "❌ ERROR: Missing AZURE_LOCATION" >&2 18 | Errors=$((Errors + 1)) 19 | fi 20 | 21 | if [ "$Errors" -gt 0 ]; then 22 | exit 1 23 | fi 24 | 25 | # --- Default Values --- 26 | declare -A defaultEnvVars=( 27 | [AZURE_AI_EMBED_DEPLOYMENT_NAME]="text-embedding-3-small" 28 | [AZURE_AI_EMBED_MODEL_NAME]="text-embedding-3-small" 29 | [AZURE_AI_EMBED_MODEL_FORMAT]="OpenAI" 30 | [AZURE_AI_EMBED_MODEL_VERSION]="1" 31 | [AZURE_AI_EMBED_DEPLOYMENT_SKU]="Standard" 32 | [AZURE_AI_EMBED_DEPLOYMENT_CAPACITY]="50" 33 | [AZURE_AI_CHAT_DEPLOYMENT_NAME]="gpt-4o-mini" 34 | [AZURE_AI_CHAT_MODEL_NAME]="gpt-4o-mini" 35 | [AZURE_AI_CHAT_MODEL_VERSION]="2024-07-18" 36 | [AZURE_AI_CHAT_MODEL_FORMAT]="OpenAI" 37 | [AZURE_AI_CHAT_DEPLOYMENT_SKU]="GlobalStandard" 38 | [AZURE_AI_CHAT_DEPLOYMENT_CAPACITY]="80" 39 | ) 40 | 41 | # --- Set Env Vars and azd env --- 42 | declare -A envVars 43 | for key in "${!defaultEnvVars[@]}"; do 44 | val="${!key}" 45 | if [ -z "$val" ]; then 46 | val="${defaultEnvVars[$key]}" 47 | fi 48 | envVars[$key]="$val" 49 | azd env set "$key" "$val" 50 | done 51 | 52 | # --- If we do not use existing AI Project, we don't deploy models, so skip validation --- 53 | resourceId="${AZURE_EXISTING_AIPROJECT_RESOURCE_ID}" 54 | if [ -n "$resourceId" ]; then 55 | echo "✅ AZURE_EXISTING_AIPROJECT_RESOURCE_ID is set, skipping model deployment validation." 56 | exit 0 57 | fi 58 | 59 | # --- Build Chat Deployment --- 60 | chatDeployment_name="${AZURE_AI_CHAT_DEPLOYMENT_NAME}" 61 | chatDeployment_model_name="${AZURE_AI_CHAT_MODEL_NAME}" 62 | chatDeployment_model_version="${AZURE_AI_CHAT_MODEL_VERSION}" 63 | chatDeployment_model_format="${AZURE_AI_CHAT_MODEL_FORMAT}" 64 | chatDeployment_sku_name="${AZURE_AI_CHAT_DEPLOYMENT_SKU}" 65 | chatDeployment_capacity="${AZURE_AI_CHAT_DEPLOYMENT_CAPACITY}" 66 | chatDeployment_capacity_env="AZURE_AI_CHAT_DEPLOYMENT_CAPACITY" 67 | 68 | aiModelDeployments=( 69 | "$chatDeployment_name|$chatDeployment_model_name|$chatDeployment_model_version|$chatDeployment_model_format|$chatDeployment_sku_name|$chatDeployment_capacity|$chatDeployment_capacity_env" 70 | ) 71 | 72 | # --- Optional Embed Deployment --- 73 | if [ "$USE_AZURE_AI_SEARCH_SERVICE" == "true" ]; then 74 | embedDeployment_name="${AZURE_AI_EMBED_DEPLOYMENT_NAME}" 75 | embedDeployment_model_name="${AZURE_AI_EMBED_MODEL_NAME}" 76 | embedDeployment_model_version="${AZURE_AI_EMBED_MODEL_VERSION}" 77 | embedDeployment_model_format="${AZURE_AI_EMBED_MODEL_FORMAT}" 78 | embedDeployment_sku_name="${AZURE_AI_EMBED_DEPLOYMENT_SKU}" 79 | embedDeployment_capacity="${AZURE_AI_EMBED_DEPLOYMENT_CAPACITY}" 80 | embedDeployment_capacity_env="AZURE_AI_EMBED_DEPLOYMENT_CAPACITY" 81 | 82 | aiModelDeployments+=( 83 | "$embedDeployment_name|$embedDeployment_model_name|$embedDeployment_model_version|$embedDeployment_model_format|$embedDeployment_sku_name|$embedDeployment_capacity|$embedDeployment_capacity_env" 84 | ) 85 | fi 86 | 87 | # --- Set Subscription --- 88 | az account set --subscription "$SubscriptionId" 89 | echo "🎯 Active Subscription: $(az account show --query '[name, id]' --output tsv)" 90 | 91 | QuotaAvailable=true 92 | 93 | # --- Validate Quota --- 94 | for entry in "${aiModelDeployments[@]}"; do 95 | IFS="|" read -r name model model_version format type capacity capacity_env_var_name <<< "$entry" 96 | echo "🔍 Validating model deployment: $name ..." 97 | ./scripts/resolve_model_quota.sh \ 98 | -Location "$Location" \ 99 | -Model "$model" \ 100 | -Format "$format" \ 101 | -Capacity "$capacity" \ 102 | -CapacityEnvVarName "$capacity_env_var_name" \ 103 | -DeploymentType "$type" 104 | 105 | if [ $? -ne 0 ]; then 106 | echo "❌ ERROR: Quota validation failed for model deployment: $name" >&2 107 | QuotaAvailable=false 108 | fi 109 | done 110 | 111 | # --- Final Check --- 112 | if [ "$QuotaAvailable" != "true" ]; then 113 | exit 1 114 | else 115 | echo "✅ All model deployments passed quota validation successfully." 116 | exit 0 117 | fi -------------------------------------------------------------------------------- /scripts/setup_credential.ps1: -------------------------------------------------------------------------------- 1 | # Prompt for username with validation 2 | do { 3 | $username = Read-Host -Prompt '👤 Create a new username for the web app (no spaces, at least 1 character)' 4 | $usernameInvalid = $false 5 | if ([string]::IsNullOrWhiteSpace($username)) { 6 | Write-Warning "❌ Username cannot be empty or consist only of whitespace." 7 | $usernameInvalid = $true 8 | } elseif ($username -match '\s') { 9 | Write-Warning "❌ Username cannot contain spaces." 10 | $usernameInvalid = $true 11 | } 12 | } while ($usernameInvalid) 13 | 14 | # Prompt for password with validation 15 | do { 16 | $password = Read-Host -Prompt '🔑 Create a new password for the web app (no spaces, at least 1 character)' -AsSecureString 17 | $confirmPassword = Read-Host -Prompt '🔑 Confirm the new password' -AsSecureString 18 | $passwordInvalid = $false 19 | 20 | if ($password.Length -eq 0) { 21 | Write-Warning "❌ Password cannot be empty." 22 | $passwordInvalid = $true 23 | } elseif ($password.Length -ne $confirmPassword.Length) { # Quick check for length difference 24 | Write-Warning "❌ Passwords do not match." 25 | $passwordInvalid = $true 26 | } else { 27 | # Convert SecureStrings to plain text for validation and comparison 28 | $tempBSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password) 29 | $tempPlainPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($tempBSTR) 30 | 31 | $confirmBSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($confirmPassword) 32 | $confirmPlainPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($confirmBSTR) 33 | 34 | if ($tempPlainPassword -ne $confirmPlainPassword) { 35 | Write-Warning "❌ Passwords do not match." 36 | $passwordInvalid = $true 37 | } elseif ($tempPlainPassword -match '\s') { 38 | Write-Warning "❌ Password cannot contain spaces." 39 | $passwordInvalid = $true 40 | } 41 | 42 | # Securely clear the plain text passwords from memory after validation 43 | [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($tempBSTR) 44 | [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($confirmBSTR) 45 | Remove-Variable tempBSTR, tempPlainPassword, confirmBSTR, confirmPlainPassword -ErrorAction SilentlyContinue 46 | } 47 | } while ($passwordInvalid) 48 | 49 | 50 | # Convert the secure string password to plain text 51 | $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password) 52 | $plainPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) 53 | 54 | $resourceGroupName = azd env get-value AZURE_RESOURCE_GROUP 55 | $containerAppName = azd env get-value SERVICE_API_NAME 56 | $subscriptionId = azd env get-value AZURE_SUBSCRIPTION_ID 57 | 58 | az account set --subscription $subscriptionId 59 | Write-Host "🎯 Active Subscription: $(az account show --query '[name, id]' --output tsv)" 60 | 61 | 62 | Write-Host "⏳ Setup username and password in the secrets..." 63 | 64 | # Set the secret 65 | az containerapp secret set ` 66 | --name $containerAppName ` 67 | --resource-group $resourceGroupName ` 68 | --secrets web-app-username=$username web-app-password=$plainPassword ` 69 | > $null 2>&1 70 | 71 | #set the environment variables to reference the secrets 72 | az containerapp update ` 73 | --name $containerAppName ` 74 | --resource-group $resourceGroupName ` 75 | --set-env-vars WEB_APP_USERNAME=secretref:web-app-username WEB_APP_PASSWORD=secretref:web-app-password ` 76 | > $null 2>&1 77 | 78 | 79 | Write-Host "✅ New username and password now are in the secrets" 80 | Write-Host "🔍 Querying the active revision in the container app..." 81 | 82 | # Get the active revision name 83 | $activeRevision = az containerapp revision list ` 84 | --name $containerAppName ` 85 | --resource-group $resourceGroupName ` 86 | --query '[?properties.active==`true`].name' ` 87 | --output tsv 88 | 89 | if (-not $activeRevision) { 90 | Write-Host "❌ No active revision found for the specified Container App." 91 | exit 1 92 | } 93 | 94 | Write-Host "♻️ Restarting revision ca-api-hokq6p6gw7b3c--c8hivx8 ca-api-hokq6p6gw7b3c--0000001...." 95 | 96 | 97 | # Restart the active revision 98 | az containerapp revision restart ` 99 | --name $containerAppName ` 100 | --resource-group $resourceGroupName ` 101 | --revision $activeRevision ` 102 | > $null 2>&1 103 | 104 | Write-Host "✅ Successfully restarted the revision: $activeRevision" 105 | 106 | exit 0 -------------------------------------------------------------------------------- /scripts/setup_credential.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Prompt for username with validation 4 | while true; do 5 | read -rp "👤 Create a new username for the web app (no spaces, at least 1 character): " username 6 | 7 | if [[ -z "$username" || "$username" =~ [[:space:]] ]]; then 8 | echo "❌ Username cannot be empty or contain spaces." >&2 9 | else 10 | break 11 | fi 12 | done 13 | 14 | # Prompt for password with validation 15 | while true; do 16 | read -rsp "🔑 Create a new password for the web app (no spaces, at least 1 character): " password 17 | echo 18 | read -rsp "🔑 Confirm the new password: " confirmPassword 19 | echo 20 | 21 | if [[ -z "$password" ]]; then 22 | echo "❌ Password cannot be empty." >&2 23 | elif [[ "$password" != "$confirmPassword" ]]; then 24 | echo "❌ Passwords do not match." >&2 25 | elif [[ "$password" =~ [[:space:]] ]]; then 26 | echo "❌ Password cannot contain spaces." >&2 27 | else 28 | break 29 | fi 30 | done 31 | 32 | # Get resource group and container app name from azd 33 | resourceGroupName=$(azd env get-value AZURE_RESOURCE_GROUP) 34 | containerAppName=$(azd env get-value SERVICE_API_NAME) 35 | subscriptionId=$(azd env get-value AZURE_SUBSCRIPTION_ID) 36 | 37 | az account set --subscription $subscriptionId 38 | echo "🎯 Active Subscription: $(az account show --query '[name, id]' --output tsv)" 39 | 40 | echo "⏳ Setup username and password in the secrets..." 41 | 42 | # Set the secrets 43 | az containerapp secret set \ 44 | --name "$containerAppName" \ 45 | --resource-group "$resourceGroupName" \ 46 | --secrets web-app-username="$username" web-app-password="$password" \ 47 | > /dev/null 2>&1 48 | 49 | #set the environment variables to reference the secrets 50 | az containerapp update \ 51 | --name "$containerAppName" \ 52 | --resource-group "$resourceGroupName" \ 53 | --set-env-vars WEB_APP_USERNAME=secretref:web-app-username WEB_APP_PASSWORD=secretref:web-app-password \ 54 | > /dev/null 2>&1 55 | 56 | echo "✅ New username and password now are in the secrets" 57 | echo "🔍 Querying the active revision in the container app..." 58 | 59 | # Get the active revision name 60 | activeRevision=$(az containerapp revision list \ 61 | --name "$containerAppName" \ 62 | --resource-group "$resourceGroupName" \ 63 | --query '[?properties.active==`true`].name' \ 64 | --output tsv) 65 | 66 | if [[ -z "$activeRevision" ]]; then 67 | echo "❌ No active revision found for the specified Container App." >&2 68 | exit 1 69 | fi 70 | 71 | echo "♻️ Restarting revision $activeRevision..." 72 | 73 | # Restart the active revision 74 | az containerapp revision restart \ 75 | --name "$containerAppName" \ 76 | --resource-group "$resourceGroupName" \ 77 | --revision "$activeRevision" \ 78 | > /dev/null 2>&1 79 | 80 | echo "✅ Successfully restarted the revision: $activeRevision" 81 | 82 | exit 0 83 | -------------------------------------------------------------------------------- /scripts/validate_env_vars.ps1: -------------------------------------------------------------------------------- 1 | # Define environment variables and their regex patterns 2 | $envValidationRules = @{ 3 | "AZURE_EXISTING_AIPROJECT_RESOURCE_ID" = '^/subscriptions/[0-9a-fA-F-]{36}/resourceGroups/[^/]+/providers/Microsoft\.CognitiveServices/accounts/[^/]+/projects/[^/]+$' 4 | } 5 | 6 | $hasError = $false 7 | 8 | foreach ($envVar in $envValidationRules.Keys) { 9 | $pattern = $envValidationRules[$envVar] 10 | $value = [Environment]::GetEnvironmentVariable($envVar) 11 | 12 | if ($value) { 13 | if ($value -notmatch $pattern) { 14 | Write-Host "❌ Invalid value for '$envVar'. Expected pattern: $pattern" 15 | $hasError = $true 16 | } 17 | } 18 | } 19 | 20 | if ($hasError) { 21 | exit 1 22 | } 23 | -------------------------------------------------------------------------------- /scripts/validate_env_vars.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Define environment variables and their regex patterns 4 | declare -A envValidationRules=( 5 | ["AZURE_EXISTING_AIPROJECT_RESOURCE_ID"]='^/subscriptions/[0-9a-fA-F-]{36}/resourceGroups/[^/]+/providers/Microsoft\.CognitiveServices/accounts/[^/]+/projects/[^/]+$' 6 | ) 7 | 8 | hasError=0 9 | 10 | for envVar in "${!envValidationRules[@]}"; do 11 | pattern="${envValidationRules[$envVar]}" 12 | value="${!envVar}" 13 | 14 | if [[ -n "$value" ]]; then 15 | if ! echo "$value" | grep -Eq "$pattern"; then 16 | echo "❌ Invalid value for '$envVar'. Expected pattern: $pattern" >&2 17 | hasError=1 18 | fi 19 | fi 20 | done 21 | 22 | exit $hasError 23 | -------------------------------------------------------------------------------- /scripts/write_env.ps1: -------------------------------------------------------------------------------- 1 | # Define the .env file path 2 | $envFilePath = "src\.env" 3 | 4 | # Clear the contents of the .env file 5 | Set-Content -Path $envFilePath -Value "" 6 | 7 | # Append new values to the .env file 8 | $azureEnvName = azd env get-value AZURE_ENV_NAME 9 | $aiProjectResourceId = azd env get-value AZURE_EXISTING_AIPROJECT_RESOURCE_ID 10 | $aiProjectEndpoint = azd env get-value AZURE_EXISTING_AIPROJECT_ENDPOINT 11 | $azureAiChatDeploymentName = azd env get-value AZURE_AI_CHAT_DEPLOYMENT_NAME 12 | $azureTenantId = azd env get-value AZURE_TENANT_ID 13 | $azureAIEmbedDeploymentName = azd env get-value AZURE_AI_EMBED_DEPLOYMENT_NAME 14 | $azureAIEmbedDimensions = azd env get-value AZURE_AI_EMBED_DIMENSIONS 15 | $azureAISearchIndexName = azd env get-value AZURE_AI_SEARCH_INDEX_NAME 16 | $azureAISearchEndpoint = azd env get-value AZURE_AI_SEARCH_ENDPOINT 17 | $serviceAPIUri = azd env get-value SERVICE_API_URI 18 | $enableAzureMonitorTracing = azd env get-value ENABLE_AZURE_MONITOR_TRACING 19 | $azureTracingGenAIContentRecordingEnabled = azd env get-value AZURE_TRACING_GEN_AI_CONTENT_RECORDING_ENABLED 20 | 21 | Add-Content -Path $envFilePath -Value "AZURE_EXISTING_AIPROJECT_RESOURCE_ID=$aiProjectResourceId" 22 | Add-Content -Path $envFilePath -Value "AZURE_EXISTING_AIPROJECT_ENDPOINT=$aiProjectEndpoint" 23 | Add-Content -Path $envFilePath -Value "AZURE_AI_CHAT_DEPLOYMENT_NAME=$azureAiChatDeploymentName" 24 | Add-Content -Path $envFilePath -Value "AZURE_TENANT_ID=$azureTenantId" 25 | Add-Content -Path $envFilePath -Value "AZURE_AI_EMBED_DEPLOYMENT_NAME=$azureAIEmbedDeploymentName" 26 | Add-Content -Path $envFilePath -Value "AZURE_AI_EMBED_DIMENSIONS=$azureAIEmbedDimensions" 27 | Add-Content -Path $envFilePath -Value "AZURE_AI_SEARCH_INDEX_NAME=$azureAISearchIndexName" 28 | Add-Content -Path $envFilePath -Value "AZURE_AI_SEARCH_ENDPOINT=$azureAISearchEndpoint" 29 | Add-Content -Path $envFilePath -Value "AZURE_TENANT_ID=$azureTenantId" 30 | Add-Content -Path $envFilePath -Value "ENABLE_AZURE_MONITOR_TRACING=$enableAzureMonitorTracing" 31 | Add-Content -Path $envFilePath -Value "AZURE_TRACING_GEN_AI_CONTENT_RECORDING_ENABLED=$azureTracingGenAIContentRecordingEnabled" 32 | 33 | Write-Host "🌐 Please visit web app URL:" 34 | Write-Host $serviceAPIUri -ForegroundColor Cyan -------------------------------------------------------------------------------- /scripts/write_env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Define the .env file path 4 | ENV_FILE_PATH="src/.env" 5 | 6 | # Clear the contents of the .env file 7 | > $ENV_FILE_PATH 8 | 9 | echo "AZURE_EXISTING_AIPROJECT_RESOURCE_ID=$(azd env get-value AZURE_EXISTING_AIPROJECT_RESOURCE_ID)" >> $ENV_FILE_PATH 10 | echo "AZURE_AI_CHAT_DEPLOYMENT_NAME=$(azd env get-value AZURE_AI_CHAT_DEPLOYMENT_NAME)" >> $ENV_FILE_PATH 11 | echo "AZURE_TENANT_ID=$(azd env get-value AZURE_TENANT_ID)" >> $ENV_FILE_PATH 12 | echo "AZURE_AI_EMBED_DEPLOYMENT_NAME=$(azd env get-value AZURE_AI_EMBED_DEPLOYMENT_NAME)" >> $ENV_FILE_PATH 13 | echo "AZURE_AI_EMBED_DIMENSIONS=$(azd env get-value AZURE_AI_EMBED_DIMENSIONS)" >> $ENV_FILE_PATH 14 | echo "AZURE_AI_SEARCH_INDEX_NAME=$(azd env get-value AZURE_AI_SEARCH_INDEX_NAME)" >> $ENV_FILE_PATH 15 | echo "AZURE_AI_SEARCH_ENDPOINT=$(azd env get-value AZURE_AI_SEARCH_ENDPOINT)" >> $ENV_FILE_PATH 16 | echo "AZURE_TENANT_ID=$(azd env get-value AZURE_TENANT_ID)" >> $ENV_FILE_PATH 17 | echo "AZURE_EXISTING_AIPROJECT_ENDPOINT=$(azd env get-value AZURE_EXISTING_AIPROJECT_ENDPOINT)" >> $ENV_FILE_PATH 18 | echo "ENABLE_AZURE_MONITOR_TRACING=$(azd env get-value ENABLE_AZURE_MONITOR_TRACING)" >> $ENV_FILE_PATH 19 | echo "AZURE_TRACING_GEN_AI_CONTENT_RECORDING_ENABLED=$(azd env get-value AZURE_TRACING_GEN_AI_CONTENT_RECORDING_ENABLED)" >> $ENV_FILE_PATH 20 | 21 | 22 | echo "🌐 Please visit web app URL:" 23 | echo -e "\033[0;36m$(azd env get-value SERVICE_API_URI)\033[0m" 24 | 25 | exit 0 -------------------------------------------------------------------------------- /src/.dockerignore: -------------------------------------------------------------------------------- 1 | .git* 2 | .venv/ 3 | **/*.pyc -------------------------------------------------------------------------------- /src/.env.sample: -------------------------------------------------------------------------------- 1 | 2 | # After running `azd up`, the following can be found in `.azure//.env` 3 | # Azure AI Search endpoint and index name can be found in Azure Portal. See https://learn.microsoft.com/en-us/azure/ai-services/agents/how-to/tools/azure-ai-search?tabs=azurecli%2Cpython&pivots=overview-azure-ai-search 4 | AZURE_AIPROJECT_CONNECTION_STRING="" # required. Example: "eastus2.api.azureml.ms;********-****-****-****-************;my_resource_group;ai-project-**********" 5 | AZURE_AI_CHAT_DEPLOYMENT_NAME="" # required. Example: "gpt-4o-mini" 6 | AZURE_AI_EMBED_DEPLOYMENT_NAME="" # required for index search. Example: "text-embedding-3-small" 7 | AZURE_AI_EMBED_DIMENSIONS=100 # required for index search. Example: 100 8 | AZURE_AI_SEARCH_ENDPOINT="" # required for index search. Example: "https://my-search-service.search.windows.net" 9 | AZURE_AI_SEARCH_INDEX_NAME="" # required for index search. Example: "index_sample" -------------------------------------------------------------------------------- /src/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM python:3.11 4 | 5 | WORKDIR /code 6 | 7 | COPY . . 8 | 9 | 10 | RUN pip install --no-cache-dir --upgrade -r requirements.txt 11 | 12 | # Install Node.js and pnpm with specific versions 13 | RUN apt-get update \ 14 | && apt-get install -y curl \ 15 | && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ 16 | && apt-get install -y nodejs \ 17 | && npm install -g pnpm@10.4.1 \ 18 | && node --version \ 19 | && pnpm --version 20 | 21 | # Build React frontend 22 | WORKDIR /code/frontend 23 | RUN pnpm install \ 24 | && pnpm build 25 | 26 | # Return to backend directory 27 | WORKDIR /code 28 | 29 | EXPOSE 50505 30 | 31 | CMD ["gunicorn", "api.main:create_app"] -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/get-started-with-ai-chat/aa5095af96749f2654cd9b07273b9c3f3c9aa1d6/src/__init__.py -------------------------------------------------------------------------------- /src/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/get-started-with-ai-chat/aa5095af96749f2654cd9b07273b9c3f3c9aa1d6/src/api/__init__.py -------------------------------------------------------------------------------- /src/api/main.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft. All rights reserved. 2 | # Licensed under the MIT license. 3 | # See LICENSE file in the project root for full license information. 4 | import contextlib 5 | import logging 6 | import os 7 | from typing import Union 8 | 9 | import fastapi 10 | from azure.ai.projects.aio import AIProjectClient 11 | from azure.identity import AzureDeveloperCliCredential, ManagedIdentityCredential 12 | from dotenv import load_dotenv 13 | from fastapi.staticfiles import StaticFiles 14 | 15 | from .search_index_manager import SearchIndexManager 16 | from .util import get_logger 17 | 18 | logger = None 19 | enable_trace = False 20 | 21 | @contextlib.asynccontextmanager 22 | async def lifespan(app: fastapi.FastAPI): 23 | azure_credential: Union[AzureDeveloperCliCredential, ManagedIdentityCredential] 24 | if not os.getenv("RUNNING_IN_PRODUCTION"): 25 | if tenant_id := os.getenv("AZURE_TENANT_ID"): 26 | logger.info("Using AzureDeveloperCliCredential with tenant_id %s", tenant_id) 27 | azure_credential = AzureDeveloperCliCredential(tenant_id=tenant_id) 28 | else: 29 | logger.info("Using AzureDeveloperCliCredential") 30 | azure_credential = AzureDeveloperCliCredential() 31 | else: 32 | # User-assigned identity was created and set in api.bicep 33 | user_identity_client_id = os.getenv("AZURE_CLIENT_ID") 34 | logger.info("Using ManagedIdentityCredential with client_id %s", user_identity_client_id) 35 | azure_credential = ManagedIdentityCredential(client_id=user_identity_client_id) 36 | 37 | project = AIProjectClient( 38 | credential=azure_credential, 39 | endpoint=os.environ["AZURE_EXISTING_AIPROJECT_ENDPOINT"], 40 | ) 41 | 42 | if enable_trace: 43 | application_insights_connection_string = "" 44 | try: 45 | application_insights_connection_string = await project.telemetry.get_connection_string() 46 | except Exception as e: 47 | e_string = str(e) 48 | logger.error("Failed to get Application Insights connection string, error: %s", e_string) 49 | if not application_insights_connection_string: 50 | logger.error("Application Insights was not enabled for this project.") 51 | logger.error("Enable it via the 'Tracing' tab in your AI Foundry project page.") 52 | exit() 53 | else: 54 | from azure.monitor.opentelemetry import configure_azure_monitor 55 | configure_azure_monitor(connection_string=application_insights_connection_string) 56 | 57 | chat = project.inference.get_chat_completions_client() 58 | embed = project.inference.get_embeddings_client() 59 | 60 | endpoint = os.environ.get('AZURE_AI_SEARCH_ENDPOINT') 61 | search_index_manager = None 62 | embed_dimensions = None 63 | if os.getenv('AZURE_AI_EMBED_DIMENSIONS'): 64 | embed_dimensions = int(os.getenv('AZURE_AI_EMBED_DIMENSIONS')) 65 | 66 | if endpoint and os.getenv('AZURE_AI_SEARCH_INDEX_NAME') and os.getenv('AZURE_AI_EMBED_DEPLOYMENT_NAME'): 67 | search_index_manager = SearchIndexManager( 68 | endpoint = endpoint, 69 | credential = azure_credential, 70 | index_name = os.getenv('AZURE_AI_SEARCH_INDEX_NAME'), 71 | dimensions = embed_dimensions, 72 | model = os.getenv('AZURE_AI_EMBED_DEPLOYMENT_NAME'), 73 | embeddings_client=embed 74 | ) 75 | # Create index and upload the documents only if index does not exist. 76 | logger.info(f"Creating index {os.getenv('AZURE_AI_SEARCH_INDEX_NAME')}.") 77 | await search_index_manager.ensure_index_created( 78 | vector_index_dimensions=embed_dimensions if embed_dimensions else 100) 79 | else: 80 | logger.info("The RAG search will not be used.") 81 | 82 | app.state.chat = chat 83 | app.state.search_index_manager = search_index_manager 84 | app.state.chat_model = os.environ["AZURE_AI_CHAT_DEPLOYMENT_NAME"] 85 | yield 86 | 87 | await project.close() 88 | await chat.close() 89 | if search_index_manager is not None: 90 | await search_index_manager.close() 91 | 92 | 93 | def create_app(): 94 | if not os.getenv("RUNNING_IN_PRODUCTION"): 95 | load_dotenv(override=True) 96 | 97 | global logger 98 | logger = get_logger( 99 | name="azureaiapp", 100 | log_level=logging.INFO, 101 | log_file_name = os.getenv("APP_LOG_FILE"), 102 | log_to_console=True 103 | ) 104 | 105 | enable_trace_string = os.getenv("ENABLE_AZURE_MONITOR_TRACING", "") 106 | global enable_trace 107 | enable_trace = False 108 | if enable_trace_string == "": 109 | enable_trace = False 110 | else: 111 | enable_trace = str(enable_trace_string).lower() == "true" 112 | if enable_trace: 113 | logger.info("Tracing is enabled.") 114 | try: 115 | from azure.monitor.opentelemetry import configure_azure_monitor 116 | except ModuleNotFoundError: 117 | logger.error("Required libraries for tracing not installed.") 118 | logger.error("Please make sure azure-monitor-opentelemetry is installed.") 119 | exit() 120 | else: 121 | logger.info("Tracing is not enabled") 122 | 123 | app = fastapi.FastAPI(lifespan=lifespan) 124 | app.mount("/static", StaticFiles(directory="api/static"), name="static") 125 | 126 | from . import routes # noqa 127 | 128 | app.include_router(routes.router) 129 | 130 | return app 131 | -------------------------------------------------------------------------------- /src/api/static/assets/template-images/Avatar_Default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/api/static/react/.vite/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "src/main.tsx": { 3 | "file": "assets/main-react-app.js", 4 | "name": "main", 5 | "src": "src/main.tsx", 6 | "isEntry": true 7 | }, 8 | "style.css": { 9 | "file": "assets/main-react-app.css", 10 | "src": "style.css" 11 | } 12 | } -------------------------------------------------------------------------------- /src/api/static/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, body { 6 | height: 100%; 7 | } 8 | 9 | #messages .toast-container { 10 | margin-bottom: 12px; 11 | } 12 | 13 | .background-user { 14 | background-color: #2372cc; 15 | } 16 | 17 | .background-assistant { 18 | background-color: #2c8310; 19 | } 20 | 21 | .error { 22 | color: #ff0000; 23 | } -------------------------------------------------------------------------------- /src/api/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Get Started with AI Agents 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/api/util.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft. All rights reserved. 2 | # Licensed under the MIT license. 3 | # See LICENSE file in the project root for full license information. 4 | from typing import Optional 5 | 6 | import logging 7 | import pydantic 8 | import sys 9 | 10 | 11 | def get_logger(name: str, 12 | log_level: int = logging.INFO, 13 | log_file_name: Optional[str] = None, 14 | log_to_console: bool=True) -> logging.Logger: 15 | """ 16 | Return the logger, capable to log into file and/or to console. 17 | 18 | :param name: the name of the logger. 19 | :param log_level: The logging verbosity level. 20 | :param log_file_name: The file to be sed to write logs if any. 21 | :param log_to_console: Boolean showing if we want to log into the console. 22 | :returns: The logger object. 23 | """ 24 | logger = logging.getLogger(name) 25 | logger.setLevel(log_level) 26 | 27 | if log_to_console: 28 | # Configure the stream handler (stdout) 29 | stream_handler = logging.StreamHandler(sys.stdout) 30 | stream_handler.setLevel(logging.INFO) 31 | stream_formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s") 32 | stream_handler.setFormatter(stream_formatter) 33 | logger.addHandler(stream_handler) 34 | 35 | if log_file_name: 36 | file_handler = logging.FileHandler(log_file_name) 37 | file_handler.setLevel(log_level) 38 | file_formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s") 39 | file_handler.setFormatter(file_formatter) 40 | logger.addHandler(file_handler) 41 | return logger 42 | 43 | 44 | class Message(pydantic.BaseModel): 45 | content: str 46 | role: str = "user" 47 | 48 | 49 | class ChatRequest(pydantic.BaseModel): 50 | messages: list[Message] 51 | -------------------------------------------------------------------------------- /src/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "1.0.0", 4 | "description": "React frontend for Azure AI Agents Demo", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "setup": "npm install -g pnpm@10.4.1 && pnpm install && pnpm build", 10 | "preview": "vite preview", 11 | "typecheck": "tsc --noEmit" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "packageManager": "pnpm@10.4.1", 17 | "dependencies": { 18 | "@fluentui-copilot/react-copilot": "0.23.3", 19 | "@fluentui-copilot/react-copilot-chat": "0.9.6", 20 | "@fluentui-copilot/react-feedback-buttons": "0.9.6", 21 | "@fluentui-copilot/react-provider": "0.9.4", 22 | "@fluentui-copilot/react-reference": "0.13.10", 23 | "@fluentui-copilot/react-sensitivity-label": "0.5.8", 24 | "@fluentui/react-components": "9.60.0", 25 | "@fluentui/react-icons": "2.0.279", 26 | "@fluentui/react-theme": "9.1.24", 27 | "@vitejs/plugin-react": "4.4.1", 28 | "clsx": "2.1.1", 29 | "copy-to-clipboard": "^3.3.3", 30 | "react": "19.1.0", 31 | "react-dom": "19.1.0", 32 | "react-markdown": "10.1.0", 33 | "react-syntax-highlighter": "^15.5.0", 34 | "rehype-katex": "^7.0.0", 35 | "rehype-raw": "^7.0.0", 36 | "rehype-sanitize": "^6.0.0", 37 | "rehype-stringify": "^10.0.0", 38 | "remark-breaks": "^4.0.0", 39 | "remark-gfm": "^4.0.0", 40 | "remark-math": "^6.0.0", 41 | "remark-parse": "^11.0.0", 42 | "remark-supersub": "^1.0.0", 43 | "vite": "6.3.3", 44 | "prismjs": "1.30.0" 45 | }, 46 | "devDependencies": { 47 | "@types/node": "22.14.1", 48 | "@types/react": "18.3.20", 49 | "@types/react-dom": "18.3.5", 50 | "@types/react-syntax-highlighter": "15.5.13", 51 | "typescript": "5.8.3" 52 | }, 53 | "resolutions": { 54 | "prismjs": "1.30.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/frontend/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import { AgentPreview } from "./agents/AgentPreview"; 2 | import { ThemeProvider } from "./core/theme/ThemeProvider"; 3 | 4 | const App: React.FC = () => { 5 | // State to store the agent details 6 | const agentDetails ={ 7 | id: "chatbot", 8 | object: "chatbot", 9 | created_at: Date.now(), 10 | name: "Chatbot", 11 | description: "This is a sample chatbot.", 12 | model: "default", 13 | metadata: { 14 | logo: "Avatar_Default.svg", 15 | }, 16 | }; 17 | 18 | return ( 19 | 20 |
21 | 25 |
26 |
27 | ); 28 | }; 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /src/frontend/src/components/agents/AgentIcon.module.css: -------------------------------------------------------------------------------- 1 | .iconContainer { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | } 6 | 7 | .icon { 8 | width: 32px; 9 | height: 32px; 10 | border-radius: 50%; 11 | object-fit: cover; 12 | } -------------------------------------------------------------------------------- /src/frontend/src/components/agents/AgentIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | 3 | import styles from "./AgentIcon.module.css"; 4 | 5 | export interface IAgentIconProps { 6 | /** 7 | * The name of the icon to display 8 | */ 9 | iconName?: string; 10 | /** 11 | * Alt text for the icon 12 | * This is used for accessibility and SEO purposes 13 | * and should describe the icon's purpose or meaning. 14 | * If the icon is purely decorative, a blank string can be used. 15 | */ 16 | alt: string; 17 | /** 18 | * Optional class name for the icon 19 | */ 20 | iconClassName?: string; 21 | } 22 | 23 | export function AgentIcon({ 24 | iconName = "Avatar_Default.svg", 25 | iconClassName, 26 | alt = "", 27 | }: IAgentIconProps): ReactNode { 28 | return ( 29 |
30 | {alt} 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/frontend/src/components/agents/AgentPreview.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | height: 100vh; 5 | width: 100%; 6 | background-color: var(--colorNeutralBackground1); 7 | } 8 | 9 | .topBar { 10 | display: flex; 11 | justify-content: space-between; 12 | align-items: center; 13 | padding: 8px 16px; 14 | border-bottom: 1px solid var(--colorNeutralStroke1); 15 | background-color: var(--colorNeutralBackground2); 16 | } 17 | 18 | .leftSection, .rightSection { 19 | display: flex; 20 | align-items: center; 21 | gap: 8px; 22 | } 23 | 24 | .agentIcon { 25 | width: 24px; 26 | height: 24px; 27 | border-radius: 4px; 28 | } 29 | 30 | .emptyStateAgentIcon { 31 | width: 64px; 32 | height: 64px; 33 | border-radius: 8px; 34 | } 35 | 36 | .agentName { 37 | font-weight: 600; 38 | } 39 | 40 | .content { 41 | display: flex; 42 | flex: 1; 43 | flex-direction: column; 44 | align-items: center; 45 | justify-content: center; 46 | overflow: hidden; 47 | 48 | box-sizing: border-box; 49 | width: 50%; 50 | margin: 5px auto; 51 | } 52 | 53 | .menuButtonContainer { 54 | position: relative; 55 | display: inline-block; 56 | } 57 | 58 | .emptyChatContainer { 59 | display: flex; 60 | flex-direction: column; 61 | gap: 16px; 62 | align-items: center; 63 | 64 | padding: 24px; 65 | 66 | text-align: center; 67 | } 68 | 69 | .menuButton { 70 | position: relative; 71 | } 72 | 73 | .menu { 74 | position: absolute; 75 | top: 100%; 76 | right: 0; 77 | background-color: var(--colorNeutralBackground1); 78 | border: 1px solid var(--colorNeutralStroke1); 79 | border-radius: 4px; 80 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); 81 | z-index: 1000; 82 | min-width: 200px; 83 | display: none; 84 | } 85 | 86 | .menuButtonContainer:hover .menu, 87 | .menuButton:focus-within .menu { 88 | display: block; 89 | } 90 | 91 | .menuItem { 92 | display: flex; 93 | align-items: center; 94 | padding: 8px 16px; 95 | cursor: pointer; 96 | gap: 8px; 97 | } 98 | 99 | .menuItem:hover { 100 | background-color: var(--colorNeutralBackground2); 101 | } 102 | 103 | .externalLink { 104 | text-decoration: none; 105 | color: inherit; 106 | display: flex; 107 | align-items: center; 108 | width: 100%; 109 | } -------------------------------------------------------------------------------- /src/frontend/src/components/agents/AgentPreviewChatBot.module.css: -------------------------------------------------------------------------------- 1 | .chatContainer { 2 | position: relative; 3 | 4 | overflow: hidden; 5 | display: flex; 6 | flex-direction: column; 7 | gap: 16px; 8 | 9 | width: 100%; 10 | height: 100%; 11 | 12 | > div:first-child { 13 | overflow-y: auto; 14 | display: flex; 15 | flex: 1; 16 | flex-direction: column; 17 | gap: 16px; 18 | } 19 | 20 | div[role='feed'] { 21 | overflow-y: auto; 22 | display: flex; 23 | flex: 1; 24 | flex-direction: column; 25 | 26 | padding: 8px 12px 0; 27 | } 28 | } 29 | 30 | /* When there are messages, make the chat fill the available space */ 31 | .hasMessages { 32 | height: 100%; 33 | } 34 | 35 | .emptyChatContainer { 36 | height: auto; 37 | } 38 | 39 | .userMessage { 40 | padding-top: 0; 41 | padding-bottom: 0; 42 | } 43 | 44 | .copilotChatContainer { 45 | transform: translateY(0); 46 | 47 | display: flex; 48 | flex-direction: column; 49 | flex: 1; 50 | 51 | border-radius: 8px; 52 | 53 | opacity: 1; 54 | background-color: var(--colorNeutralBackground3); 55 | 56 | transition: 57 | opacity 0.3s ease-in-out, 58 | transform 0.3s ease-in-out; 59 | animation: fade-in 0.3s ease-in-out; 60 | } 61 | 62 | @keyframes fade-in { 63 | from { 64 | transform: translateY(10px); 65 | opacity: 0; 66 | } 67 | 68 | to { 69 | transform: translateY(0); 70 | opacity: 1; 71 | } 72 | } 73 | 74 | /* Transition for empty state to messages state */ 75 | .emptyChatContainer + div div:empty + .inputContainer, 76 | .emptyChatContainer + .copilotChatContainer { 77 | transition: 78 | opacity 0.3s ease-in-out, 79 | transform 0.3s ease-in-out; 80 | } 81 | 82 | 83 | .inputContainer { 84 | /* TODO: File an issue with Fluent team to allow for a safer override of the input box styles, or figure out a better approach to customize the chat input box styles */ 85 | > div:first-child > div:first-child { 86 | padding: 12px 12px 6px; 87 | border: none; 88 | background-color: var(--colorNeutralBackground3); 89 | } 90 | } 91 | 92 | .emptyState { 93 | display: flex; 94 | flex-direction: column; 95 | align-items: center; 96 | justify-content: center; 97 | text-align: center; 98 | height: 100%; 99 | padding: 2rem; 100 | color: var(--colorNeutralForeground2); 101 | flex: 1; 102 | } 103 | 104 | .emptyState h2 { 105 | font-size: 1.25rem; 106 | font-weight: 600; 107 | color: var(--colorNeutralForeground1); 108 | margin-top: 1rem; 109 | margin-bottom: 0; 110 | } 111 | 112 | .emptyStateAgentIcon { 113 | display: flex; 114 | align-items: center; 115 | justify-content: center; 116 | width: 64px; 117 | height: 64px; 118 | border-radius: 50%; 119 | background-color: var(--colorNeutralBackground3); 120 | overflow: hidden; 121 | margin-bottom: 1rem; 122 | } 123 | 124 | .avatarImage { 125 | max-width: 100%; 126 | max-height: 100%; 127 | } 128 | 129 | .copilotChatMessage { 130 | display: flex; 131 | margin: 0 16px; 132 | padding: 4px 0; 133 | } 134 | 135 | /* Transition for empty state to messages state */ 136 | .emptyChatContainer .copilotChatContainer, 137 | .hasMessages .copilotChatContainer { 138 | transition: 139 | opacity 0.3s ease-in-out, 140 | transform 0.3s ease-in-out; 141 | } -------------------------------------------------------------------------------- /src/frontend/src/components/agents/AgentPreviewChatBot.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useMemo } from "react"; 2 | import { AssistantMessage } from "./AssistantMessage"; 3 | import { UserMessage } from "./UserMessage"; 4 | import { ChatInput } from "./chatbot/ChatInput"; 5 | import { AgentPreviewChatBotProps } from "./chatbot/types"; 6 | 7 | import styles from "./AgentPreviewChatBot.module.css"; 8 | import clsx from "clsx"; 9 | 10 | export function AgentPreviewChatBot({ 11 | agentName, 12 | agentLogo, 13 | chatContext, 14 | }: AgentPreviewChatBotProps): React.JSX.Element { 15 | const [currentUserMessage, setCurrentUserMessage] = useState< 16 | string | undefined 17 | >(); 18 | 19 | const messageListFromChatContext = useMemo( 20 | () => chatContext.messageList ?? [], 21 | [chatContext.messageList] 22 | ); 23 | 24 | const onEditMessage = (messageId: string) => { 25 | const selectedMessage = messageListFromChatContext.find( 26 | (message) => !message.isAnswer && message.id === messageId 27 | )?.content; 28 | setCurrentUserMessage(selectedMessage); 29 | }; 30 | 31 | const isEmpty = messageListFromChatContext.length === 0; 32 | 33 | return ( 34 |
40 | {!isEmpty ? ( 41 |
42 | {messageListFromChatContext.map((message, index, messageList) => 43 | message.isAnswer ? ( 44 | 55 | ) : ( 56 | 61 | ) 62 | )} 63 |
64 | ) : ( 65 | // Empty div needed for proper animation when transitioning to non-empty state 66 |
67 | )} 68 |
69 | 74 |
75 |
76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /src/frontend/src/components/agents/AssistantMessage.module.css: -------------------------------------------------------------------------------- 1 | .assistantMessageContainer { 2 | display: flex; 3 | flex-direction: column; 4 | padding: 16px; 5 | margin-bottom: 16px; 6 | background-color: var(--colorNeutralBackground1); 7 | border-radius: 8px; 8 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 9 | } 10 | 11 | .messageHeader { 12 | display: flex; 13 | justify-content: space-between; 14 | align-items: center; 15 | margin-bottom: 12px; 16 | } 17 | 18 | .avatarAndName { 19 | display: flex; 20 | align-items: center; 21 | } 22 | 23 | .avatar { 24 | width: 32px; 25 | height: 32px; 26 | margin-right: 8px; 27 | border-radius: 50%; 28 | overflow: hidden; 29 | background-color: var(--colorNeutralBackground3); 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | } 34 | 35 | .avatarImage { 36 | max-width: 100%; 37 | max-height: 100%; 38 | } 39 | 40 | .botName { 41 | font-weight: 600; 42 | color: var(--colorNeutralForeground1); 43 | } 44 | 45 | .actions { 46 | display: flex; 47 | } 48 | 49 | .messageContent { 50 | margin-bottom: 12px; 51 | } 52 | 53 | .messageFootnote { 54 | margin-top: 12px; 55 | padding-top: 12px; 56 | border-top: 1px solid var(--colorNeutralStroke2); 57 | font-size: 12px; 58 | } 59 | 60 | .references { 61 | margin-bottom: 8px; 62 | } 63 | 64 | .referenceList { 65 | display: flex; 66 | flex-wrap: wrap; 67 | gap: 8px; 68 | } 69 | 70 | .reference { 71 | background-color: var(--colorNeutralBackground3); 72 | padding: 4px 8px; 73 | border-radius: 4px; 74 | font-size: 12px; 75 | cursor: pointer; 76 | } 77 | 78 | .reference:hover { 79 | background-color: var(--colorNeutralBackground4); 80 | } 81 | 82 | .disclaimer { 83 | font-size: 12px; 84 | color: var(--colorNeutralForeground3); 85 | margin-top: 12px; 86 | font-style: italic; 87 | } -------------------------------------------------------------------------------- /src/frontend/src/components/agents/AssistantMessage.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Spinner } from "@fluentui/react-components"; 2 | import { bundleIcon, DeleteFilled, DeleteRegular } from "@fluentui/react-icons"; 3 | import { CopilotMessageV2 as CopilotMessage } from "@fluentui-copilot/react-copilot-chat"; 4 | import { 5 | ReferenceListV2 as ReferenceList, 6 | ReferenceOverflowButton, 7 | } from "@fluentui-copilot/react-reference"; 8 | import { Suspense } from "react"; 9 | 10 | import { Markdown } from "../core/Markdown"; 11 | import { UsageInfo } from "./UsageInfo"; 12 | import { IAssistantMessageProps } from "./chatbot/types"; 13 | 14 | import styles from "./AgentPreviewChatBot.module.css"; 15 | import { AgentIcon } from "./AgentIcon"; 16 | 17 | const DeleteIcon = bundleIcon(DeleteFilled, DeleteRegular); 18 | 19 | export function AssistantMessage({ 20 | message, 21 | agentLogo, 22 | loadingState, 23 | agentName, 24 | showUsageInfo, 25 | onDelete, 26 | }: IAssistantMessageProps): React.JSX.Element { 27 | const hasAnnotations = message.annotations && message.annotations.length > 0; 28 | const references = hasAnnotations 29 | ? message.annotations?.map((annotation, index) => ( 30 |
31 | {annotation.text || annotation.file_name} 32 |
33 | )) 34 | : []; 35 | 36 | return ( 37 | 42 | {onDelete && message.usageInfo && ( 43 | 34 |
35 |
36 | Usage Information 37 | 38 |
39 |
40 | Input 41 | {info.prompt_tokens} 42 |
43 |
44 | Output 45 | {info.completion_tokens} 46 |
47 |
48 |
49 | 50 | ); 51 | } -------------------------------------------------------------------------------- /src/frontend/src/components/agents/UserMessage.module.css: -------------------------------------------------------------------------------- 1 | .userMessageContainer { 2 | display: flex; 3 | flex-direction: column; 4 | padding: 16px; 5 | margin-bottom: 16px; 6 | background-color: var(--colorNeutralBackground2); 7 | border-radius: 8px; 8 | } 9 | 10 | .userMessageHeader { 11 | display: flex; 12 | justify-content: space-between; 13 | align-items: center; 14 | margin-bottom: 12px; 15 | } 16 | 17 | .userInfo { 18 | display: flex; 19 | align-items: center; 20 | gap: 8px; 21 | } 22 | 23 | .userAvatar { 24 | width: 32px; 25 | height: 32px; 26 | border-radius: 50%; 27 | background-color: var(--colorBrandBackground); 28 | color: var(--colorNeutralForegroundOnBrand); 29 | display: flex; 30 | align-items: center; 31 | justify-content: center; 32 | font-size: 12px; 33 | font-weight: 600; 34 | } 35 | 36 | .timestamp { 37 | font-size: 12px; 38 | color: var(--colorNeutralForeground3); 39 | } 40 | 41 | .messageActions { 42 | display: flex; 43 | } 44 | 45 | .messageContent { 46 | width: 100%; 47 | } 48 | 49 | .attachments { 50 | display: flex; 51 | flex-wrap: wrap; 52 | gap: 8px; 53 | margin-top: 12px; 54 | } 55 | 56 | .attachment { 57 | display: flex; 58 | align-items: center; 59 | background-color: var(--colorNeutralBackground3); 60 | padding: 6px 10px; 61 | border-radius: 4px; 62 | gap: 8px; 63 | } 64 | 65 | .attachmentName { 66 | font-size: 12px; 67 | color: var(--colorBrandForeground1); 68 | cursor: pointer; 69 | text-decoration: underline; 70 | } 71 | 72 | .removeButton { 73 | background: none; 74 | border: none; 75 | color: var(--colorNeutralForeground3); 76 | cursor: pointer; 77 | padding: 0; 78 | font-size: 12px; 79 | display: flex; 80 | align-items: center; 81 | justify-content: center; 82 | } 83 | 84 | .removeButton:hover { 85 | color: var(--colorNeutralForeground1); 86 | } -------------------------------------------------------------------------------- /src/frontend/src/components/agents/UserMessage.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner, ToolbarButton } from "@fluentui/react-components"; 2 | import { bundleIcon, EditFilled, EditRegular } from "@fluentui/react-icons"; 3 | import { UserMessageV2 as CopilotUserMessage } from "@fluentui-copilot/react-copilot-chat"; 4 | import { Suspense } from "react"; 5 | 6 | import { useFormatTimestamp } from "./hooks/useFormatTimestamp"; 7 | import { IUserMessageProps } from "./chatbot/types"; 8 | 9 | import { Markdown } from "../core/Markdown"; 10 | 11 | import styles from "./AgentPreviewChatBot.module.css"; 12 | 13 | const EditIcon = bundleIcon(EditFilled, EditRegular); 14 | 15 | export function UserMessage({ 16 | message, 17 | onEditMessage, 18 | }: IUserMessageProps): JSX.Element { 19 | const formatTimestamp = useFormatTimestamp(); 20 | 21 | return ( 22 | } 29 | onClick={() => { 30 | onEditMessage(message.id); 31 | }} 32 | /> 33 | } 34 | className={styles.userMessage} 35 | timestamp={ 36 | message.more?.time ? formatTimestamp(new Date(message.more.time)) : "" 37 | } 38 | > 39 | }> 40 | 41 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/frontend/src/components/agents/chatbot/ChatInput.module.css: -------------------------------------------------------------------------------- 1 | .chatInputContainer { 2 | background-color: var(--colorNeutralBackground3); 3 | border-top: 1px solid var(--colorNeutralStroke1); 4 | } 5 | 6 | /* Customize the Fluent ChatInput container */ 7 | .chatInputContainer > div { 8 | padding: 12px; 9 | border: none; 10 | background-color: var(--colorNeutralBackground3); 11 | } 12 | 13 | /* Custom textarea height for the Fluent ChatInput */ 14 | .chatInputContainer :global(.fui-ChatInput__textarea) { 15 | max-height: 150px; 16 | font-size: 14px; 17 | line-height: 20px; 18 | } 19 | 20 | /* Customize the send button in Fluent ChatInput */ 21 | .chatInputContainer :global(.fui-ChatInput__send-button) { 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | } -------------------------------------------------------------------------------- /src/frontend/src/components/agents/chatbot/ChatInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react"; 2 | import { 3 | ChatInput as ChatInputFluent, 4 | ImperativeControlPlugin, 5 | ImperativeControlPluginRef, 6 | } from "@fluentui-copilot/react-copilot"; 7 | import { ChatInputProps } from "./types"; 8 | 9 | export const ChatInput: React.FC = ({ 10 | onSubmit, 11 | isGenerating, 12 | currentUserMessage, 13 | }) => { 14 | const [inputText, setInputText] = useState(""); 15 | const controlRef = useRef(null); 16 | 17 | useEffect(() => { 18 | if (currentUserMessage !== undefined) { 19 | controlRef.current?.setInputText(currentUserMessage ?? ""); 20 | } 21 | }, [currentUserMessage]); 22 | const onMessageSend = (text: string): void => { 23 | if (text && text.trim() !== "") { 24 | onSubmit(text.trim()); 25 | setInputText(""); 26 | controlRef.current?.setInputText(""); 27 | } 28 | }; 29 | 30 | return ( 31 | ``} // needed per fluentui-copilot API 34 | data-testid="chat-input" 35 | disableSend={isGenerating} 36 | history={true} 37 | isSending={isGenerating} 38 | onChange={( 39 | _: React.ChangeEvent, 40 | d: { value: string } 41 | ) => { 42 | setInputText(d.value); 43 | }} 44 | onSubmit={() => { 45 | onMessageSend(inputText ?? ""); 46 | }} 47 | placeholderValue="Type your message here..." 48 | > 49 | 50 | 51 | ); 52 | }; 53 | 54 | export default ChatInput; 55 | -------------------------------------------------------------------------------- /src/frontend/src/components/agents/chatbot/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Common type definitions for chat components 3 | */ 4 | 5 | export interface IFileEntity { 6 | id: string; 7 | name: string; 8 | size: number; 9 | status?: 10 | | "pending" 11 | | "uploading" 12 | | "uploaded" 13 | | "error" 14 | | "deleting" 15 | | "processed"; 16 | type: string; 17 | progress?: boolean; 18 | supportFileType?: string; 19 | createdDate?: number; 20 | originalFile?: File; 21 | uploadedId?: string; 22 | base64Url?: string; 23 | url?: string; 24 | error?: string; 25 | isRemote?: boolean; 26 | } 27 | 28 | export interface IChatItem { 29 | id: string; 30 | role?: string; 31 | content: string; 32 | isAnswer?: boolean; 33 | annotations?: any[]; 34 | fileReferences?: Map; 35 | duration?: number; 36 | message_files?: IFileEntity[]; 37 | usageInfo?: { 38 | prompt_tokens: number; 39 | completion_tokens: number; 40 | total_tokens: number; 41 | }; 42 | more?: { 43 | time?: string; 44 | }; 45 | } 46 | 47 | export interface ChatInputProps { 48 | onSubmit: (message: string) => void; 49 | isGenerating: boolean; 50 | currentUserMessage?: string; 51 | } 52 | 53 | export interface IAssistantMessageProps { 54 | message: IChatItem; 55 | agentLogo?: string; 56 | agentName?: string; 57 | loadingState?: "loading" | "streaming" | "none"; 58 | showUsageInfo?: boolean; 59 | onDelete?: (messageId: string) => Promise; 60 | } 61 | 62 | export interface IUserMessageProps { 63 | message: IChatItem; 64 | onEditMessage: (messageId: string) => void; 65 | } 66 | 67 | export interface ChatContextType { 68 | messageList: IChatItem[]; 69 | isResponding: boolean; 70 | onSend: (message: string) => void; 71 | } 72 | 73 | export interface AgentPreviewChatBotProps { 74 | agentName?: string; 75 | agentLogo?: string; 76 | chatContext: ChatContextType; 77 | } 78 | -------------------------------------------------------------------------------- /src/frontend/src/components/agents/hooks/useFormatTimestamp.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | 3 | /** 4 | * Hook for formatting Date objects into readable date strings 5 | * @returns A function that formats Date objects 6 | */ 7 | export const useFormatTimestamp = (): ((date: Date | undefined) => string) => { 8 | return useCallback( 9 | (date: Date | undefined): string => { 10 | if (date === undefined) { 11 | return ''; 12 | } 13 | 14 | // Simple date formatting with time 15 | return new Intl.DateTimeFormat('en', { 16 | dateStyle: 'short', 17 | timeStyle: 'short' 18 | }).format(date); 19 | }, 20 | [], 21 | ); 22 | }; -------------------------------------------------------------------------------- /src/frontend/src/components/core/Markdown.module.css: -------------------------------------------------------------------------------- 1 | .markdown { 2 | font-size: 14px; 3 | line-height: 1.5; 4 | color: var(--colorNeutralForeground1); 5 | } 6 | 7 | .markdown p { 8 | margin-bottom: 16px; 9 | } 10 | 11 | .markdown h1, 12 | .markdown h2, 13 | .markdown h3, 14 | .markdown h4, 15 | .markdown h5, 16 | .markdown h6 { 17 | margin-top: 24px; 18 | margin-bottom: 16px; 19 | font-weight: 600; 20 | line-height: 1.25; 21 | } 22 | 23 | .markdown h1 { 24 | font-size: 2em; 25 | } 26 | 27 | .markdown h2 { 28 | font-size: 1.5em; 29 | } 30 | 31 | .markdown h3 { 32 | font-size: 1.25em; 33 | } 34 | 35 | .markdown ul, 36 | .markdown ol { 37 | padding-left: 2em; 38 | margin-bottom: 16px; 39 | } 40 | 41 | .markdown li { 42 | margin-bottom: 4px; 43 | } 44 | 45 | .markdown blockquote { 46 | padding: 0 1em; 47 | color: var(--colorNeutralForeground2); 48 | border-left: 4px solid var(--colorNeutralStroke2); 49 | margin: 0 0 16px; 50 | } 51 | 52 | .link { 53 | color: var(--colorBrandForeground1); 54 | text-decoration: none; 55 | } 56 | 57 | .link:hover { 58 | text-decoration: underline; 59 | } 60 | 61 | .inlineCode { 62 | padding: 0.2em 0.4em; 63 | margin: 0; 64 | font-size: 85%; 65 | background-color: rgba(0, 0, 0, 0.05); 66 | border-radius: 6px; 67 | font-family: monospace; 68 | } 69 | 70 | .codeBlock { 71 | margin: 16px 0; 72 | border-radius: 6px; 73 | overflow: hidden; 74 | border: 1px solid var(--colorNeutralStroke2); 75 | } 76 | 77 | .codeHeader { 78 | display: flex; 79 | justify-content: space-between; 80 | align-items: center; 81 | padding: 8px 16px; 82 | background-color: var(--colorNeutralBackground3); 83 | border-bottom: 1px solid var(--colorNeutralStroke2); 84 | } 85 | 86 | .alignRight { 87 | margin-left: auto; 88 | } 89 | 90 | .copyButton { 91 | display: flex; 92 | align-items: center; 93 | gap: 6px; 94 | background-color: transparent; 95 | border: none; 96 | cursor: pointer; 97 | } 98 | 99 | .copyButton:hover { 100 | background-color: var(--colorNeutralBackground4); 101 | } 102 | 103 | .markdown table { 104 | border-collapse: collapse; 105 | width: 100%; 106 | margin-bottom: 16px; 107 | } 108 | 109 | .markdown th, 110 | .markdown td { 111 | padding: 8px 12px; 112 | border: 1px solid var(--colorNeutralStroke2); 113 | } 114 | 115 | .markdown th { 116 | background-color: var(--colorNeutralBackground3); 117 | text-align: left; 118 | } 119 | 120 | .markdown tr:nth-child(even) { 121 | background-color: var(--colorNeutralBackground2); 122 | } -------------------------------------------------------------------------------- /src/frontend/src/components/core/MenuButton/MenuButton.module.css: -------------------------------------------------------------------------------- 1 | .secondary:not(:disabled, :active, :hover) { 2 | background-color: var(--colorNeutralBackground2); 3 | } 4 | -------------------------------------------------------------------------------- /src/frontend/src/components/core/MenuButton/MenuButton.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading -- This is a wrapper component */ 2 | import type { 3 | MenuButtonProps as FluentMenuButtonProps, 4 | MenuItemProps, 5 | MenuListProps, 6 | MenuPopoverProps, 7 | MenuProps, 8 | MenuTriggerProps, 9 | } from "@fluentui/react-components"; 10 | import type { Key, MouseEvent, MouseEventHandler, ReactNode } from "react"; 11 | 12 | import { 13 | MenuButton as FluentMenuButton, 14 | Menu, 15 | MenuItem, 16 | MenuList, 17 | MenuPopover, 18 | MenuTrigger, 19 | } from "@fluentui/react-components"; 20 | import clsx from "clsx"; 21 | import { forwardRef, useCallback } from "react"; 22 | 23 | import styles from "./MenuButton.module.css"; 24 | 25 | export interface IMenuItemConfig extends MenuItemProps { 26 | key: Key; 27 | } 28 | 29 | export interface IMenuButtonProps { 30 | menuButtonText: string; 31 | menuItems: IMenuItemConfig[]; 32 | menuProps?: MenuProps; 33 | menuButtonProps?: FluentMenuButtonProps; 34 | menuTriggerProps?: MenuTriggerProps; 35 | menuListProps?: MenuListProps; 36 | menuPopoverProps?: MenuPopoverProps; 37 | } 38 | 39 | export const MenuButton = forwardRef( 40 | ( 41 | { 42 | menuButtonText, 43 | menuItems, 44 | menuProps, 45 | menuButtonProps, 46 | menuTriggerProps, 47 | menuListProps, 48 | menuPopoverProps, 49 | }, 50 | ref 51 | ): ReactNode => { 52 | const handleMenuButtonClick = useCallback( 53 | (event: MouseEvent) => { 54 | if (menuButtonProps?.onClick) { 55 | if (event.currentTarget instanceof HTMLButtonElement) { 56 | (menuButtonProps.onClick as MouseEventHandler)( 57 | event as MouseEvent 58 | ); 59 | } else if (event.currentTarget instanceof HTMLAnchorElement) { 60 | (menuButtonProps.onClick as MouseEventHandler)( 61 | event as MouseEvent 62 | ); 63 | } 64 | } 65 | }, 66 | [menuButtonProps] 67 | ); 68 | 69 | const handleMenuItemClick = useCallback( 70 | ( 71 | event: MouseEvent, 72 | onClick: MouseEventHandler | undefined 73 | ) => { 74 | onClick?.(event); 75 | }, 76 | [] 77 | ); 78 | 79 | const { 80 | shape = "circular", 81 | appearance = "secondary", 82 | className, 83 | ...restMenuButtonProps 84 | } = menuButtonProps ?? {}; 85 | 86 | return ( 87 | 88 | 89 | 100 | {menuButtonText} 101 | 102 | 103 | 104 | 105 | {menuItems.map(({ key, children, onClick, ...itemProps }) => ( 106 | { 109 | handleMenuItemClick(event, onClick); 110 | }} 111 | {...itemProps} 112 | > 113 | {children} 114 | 115 | ))} 116 | 117 | 118 | 119 | ); 120 | } 121 | ); 122 | MenuButton.displayName = "MenuButton"; 123 | -------------------------------------------------------------------------------- /src/frontend/src/components/core/SettingsPanel.module.css: -------------------------------------------------------------------------------- 1 | .panel { 2 | width: 320px; 3 | } 4 | 5 | .content { 6 | display: flex; 7 | flex-direction: column; 8 | gap: 24px; 9 | } 10 | 11 | .settingsButton { 12 | margin: 8px; 13 | } -------------------------------------------------------------------------------- /src/frontend/src/components/core/SettingsPanel.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX } from "react"; 2 | import { 3 | Button, 4 | Drawer, 5 | DrawerBody, 6 | DrawerHeader, 7 | DrawerHeaderTitle, 8 | } from "@fluentui/react-components"; 9 | import { Dismiss24Regular } from "@fluentui/react-icons"; 10 | 11 | import styles from "./SettingsPanel.module.css"; 12 | import { ThemePicker } from "./theme/ThemePicker"; 13 | 14 | export interface ISettingsPanelProps { 15 | isOpen: boolean; 16 | onOpenChange: (isOpen: boolean) => void; 17 | } 18 | 19 | export function SettingsPanel({ 20 | isOpen = false, 21 | onOpenChange, 22 | }: ISettingsPanelProps): JSX.Element { 23 | return ( 24 | { 27 | onOpenChange(open); 28 | }} 29 | open={isOpen} 30 | position="end" 31 | > 32 | 33 | 36 |