diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e4e5060a9..17fafa640 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,7 +6,11 @@ "forwardPorts": [50505], "features": { "ghcr.io/azure/azure-dev/azd:latest": {}, - "ghcr.io/devcontainers/features/azure-cli:1": {}, + "ghcr.io/devcontainers/features/azure-cli:1": { + "installBicep": true, + "version": "latest", + "bicepVersion": "latest" + }, "ghcr.io/devcontainers/features/docker-in-docker:2": {}, "ghcr.io/jlaundry/devcontainer-features/mssql-odbc-driver:1": { "version": "18" diff --git a/.github/workflows/azd-template-validation.yml b/.github/workflows/azd-template-validation.yml new file mode 100644 index 000000000..2e2c752fb --- /dev/null +++ b/.github/workflows/azd-template-validation.yml @@ -0,0 +1,42 @@ +name: AZD Template Validation +on: + schedule: + - cron: '30 1 * * 4' # Every Thursday at 7:00 AM IST (1:30 AM UTC) + workflow_dispatch: + +permissions: + contents: read + id-token: write + pull-requests: write + +jobs: + template_validation_job: + runs-on: ubuntu-latest + name: azd template validation + environment: production + steps: + - uses: actions/checkout@v6 + + - name: Set timestamp + run: echo "HHMM=$(date -u +'%H%M')" >> $GITHUB_ENV + + - uses: microsoft/template-validation-action@v0.4.3 + with: + validateAzd: ${{ vars.TEMPLATE_VALIDATE_AZD }} + validateTests: ${{ vars.TEMPLATE_VALIDATE_TESTS }} + useDevContainer: ${{ vars.TEMPLATE_USE_DEV_CONTAINER }} + id: validation + env: + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + AZURE_ENV_NAME: azd-${{ vars.AZURE_ENV_NAME }}-${{ env.HHMM }} + AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} + AZURE_ENV_AI_SERVICE_LOCATION: ${{ vars.AZURE_ENV_AI_SERVICE_LOCATION || 'eastus2' }} + USE_CASE: ${{ vars.USE_CASE || 'telecom' }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} + + + - name: print result + run: cat ${{ steps.validation.outputs.resultFile }} \ No newline at end of file diff --git a/.github/workflows/azure-dev-validation.yml b/.github/workflows/azure-dev-validation.yml deleted file mode 100644 index 647506f33..000000000 --- a/.github/workflows/azure-dev-validation.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Azure Template Validation -on: - workflow_dispatch: - -permissions: - contents: read - actions: read - id-token: write - pull-requests: write - -jobs: - template_validation_job: - runs-on: ubuntu-latest - environment: production - name: Template validation - - steps: - # Step 1: Checkout the code from your repository - - name: Checkout code - uses: actions/checkout@v6 - - # Step 2: Validate the Azure template using microsoft/template-validation-action - - name: Validate Azure Template - uses: microsoft/template-validation-action@v0.4.4 - with: - validateAzd: true - useDevContainer: false - validateTests: false - id: validation - env: - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }} - AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }} - AZURE_ENV_OPENAI_LOCATION: ${{ vars.AZURE_ENV_OPENAI_LOCATION || 'eastus2' }} - AZURE_ENV_USE_CASE: ${{ vars.AZURE_ENV_USE_CASE || 'telecom' }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} - - # Step 3: Print the result of the validation - - name: Print result - run: cat ${{ steps.validation.outputs.resultFile }} \ No newline at end of file diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index 006307aa4..31480f6a6 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -1,49 +1,58 @@ -name: Deploy to Azure +name: Azure Dev Deploy on: workflow_dispatch: - # push: - # branches: - # - main -# Set up permissions for deploying with secretless Azure federated credentials -# https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux#set-up-azure-login-with-openid-connect-authentication permissions: - id-token: write contents: read + id-token: write jobs: - build: + deploy: runs-on: ubuntu-latest environment: production env: - AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} - AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} - AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} - AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} - AZURE_ENV_OPENAI_LOCATION: ${{ vars.AZURE_ENV_OPENAI_LOCATION || 'eastus2' }} - AZURE_ENV_USE_CASE: ${{ vars.AZURE_ENV_USE_CASE || 'telecom' }} + AZURE_ENV_AI_SERVICE_LOCATION: ${{ vars.AZURE_ENV_AI_SERVICE_LOCATION || 'eastus2' }} + USE_CASE: ${{ vars.USE_CASE || 'telecom' }} AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} steps: - - name: Checkout - uses: actions/checkout@v6 + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set timestamp and env name + run: | + HHMM=$(date -u +'%H%M') + echo "AZURE_ENV_NAME=azd-${{ vars.AZURE_ENV_NAME }}-${HHMM}" >> $GITHUB_ENV - name: Install azd - uses: Azure/setup-azd@v2.0.0 + uses: Azure/setup-azd@v2 + + - name: Login to Azure + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Login to AZD + shell: bash + run: | + azd auth login \ + --client-id "$AZURE_CLIENT_ID" \ + --federated-credential-provider "github" \ + --tenant-id "$AZURE_TENANT_ID" - - name: Log in with Azure (Federated Credentials) + - name: Provision and Deploy + shell: bash run: | - azd auth login ` - --client-id "$Env:AZURE_CLIENT_ID" ` - --federated-credential-provider "github" ` - --tenant-id "$Env:AZURE_TENANT_ID" - shell: pwsh - - - name: Provision Infrastructure - run: azd provision --no-prompt - env: - AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ secrets.AZD_INITIAL_ENVIRONMENT_CONFIG }} - - - name: Deploy Application - run: azd deploy --no-prompt + if ! azd env select "$AZURE_ENV_NAME"; then + azd env new "$AZURE_ENV_NAME" --subscription "$AZURE_SUBSCRIPTION_ID" --location "$AZURE_LOCATION" --no-prompt + fi + azd config set defaults.subscription "$AZURE_SUBSCRIPTION_ID" + azd env set AZURE_ENV_AI_SERVICE_LOCATION="$AZURE_ENV_AI_SERVICE_LOCATION" + azd env set USE_CASE="$USE_CASE" + azd up --no-prompt \ No newline at end of file diff --git a/.github/workflows/bicep_deploy.yml b/.github/workflows/bicep_deploy.yml index e8b4816ff..da477600e 100644 --- a/.github/workflows/bicep_deploy.yml +++ b/.github/workflows/bicep_deploy.yml @@ -3,6 +3,11 @@ on: push: branches: - ckm-v2 + paths: + - 'infra/**/*.bicep' + - 'infra/**/*.json' + - 'infra/scripts/**' + - '.github/workflows/bicep_deploy.yml' permissions: contents: read diff --git a/.github/workflows/deploy-KMGeneric.yml b/.github/workflows/deploy-KMGeneric.yml index d21dcb6c4..a870194df 100644 --- a/.github/workflows/deploy-KMGeneric.yml +++ b/.github/workflows/deploy-KMGeneric.yml @@ -130,7 +130,7 @@ jobs: az deployment group create \ --resource-group ${{ env.RESOURCE_GROUP_NAME }} \ --template-file infra/main.bicep \ - --parameters solutionName=${{env.SOLUTION_PREFIX}} location="${{ env.AZURE_LOCATION }}" contentUnderstandingLocation="swedencentral" secondaryLocation="${{ env.AZURE_LOCATION }}" gptDeploymentCapacity=150 aiServiceLocation="${{ env.AZURE_LOCATION }}" createdBy="Pipeline" tags="{'Purpose':'Deploying and Cleaning Up Resources for Validation','CreatedDate':'$current_date'}" + --parameters solutionName=${{env.SOLUTION_PREFIX}} location="${{ env.AZURE_LOCATION }}" secondaryLocation="${{ env.AZURE_LOCATION }}" gptDeploymentCapacity=150 aiServiceLocation="${{ env.AZURE_LOCATION }}" createdBy="Pipeline" tags="{'Purpose':'Deploying and Cleaning Up Resources for Validation','CreatedDate':'$current_date'}" diff --git a/.github/workflows/deploy-orchestrator.yml b/.github/workflows/deploy-orchestrator.yml index 18dbf54f3..3d00e9630 100644 --- a/.github/workflows/deploy-orchestrator.yml +++ b/.github/workflows/deploy-orchestrator.yml @@ -42,12 +42,12 @@ on: required: false default: 'GoldenPath-Testing' type: string - azure_env_log_analytics_workspace_id: + azure_env_existing_log_analytics_workspace_rid: description: 'Log Analytics Workspace ID (Optional)' required: false default: '' type: string - azure_existing_ai_project_resource_id: + azure_existing_aiproject_resource_id: description: 'AI Project Resource ID (Optional)' required: false default: '' @@ -57,7 +57,7 @@ on: required: false default: '' type: string - azure_env_use_case: + use_case: description: 'Azure Environment Use Case (telecom or IT_helpdesk)' required: false default: 'telecom' @@ -91,9 +91,9 @@ jobs: exp: ${{ inputs.exp }} build_docker_image: ${{ inputs.build_docker_image }} existing_webapp_url: ${{ inputs.existing_webapp_url }} - azure_env_log_analytics_workspace_id: ${{ inputs.azure_env_log_analytics_workspace_id }} - azure_existing_ai_project_resource_id: ${{ inputs.azure_existing_ai_project_resource_id }} - azure_env_use_case: ${{ inputs.azure_env_use_case }} + azure_env_existing_log_analytics_workspace_rid: ${{ inputs.azure_env_existing_log_analytics_workspace_rid }} + azure_existing_aiproject_resource_id: ${{ inputs.azure_existing_aiproject_resource_id }} + use_case: ${{ inputs.use_case }} docker_image_tag: ${{ needs.docker-build.outputs.IMAGE_TAG }} run_e2e_tests: ${{ inputs.run_e2e_tests }} cleanup_resources: ${{ inputs.cleanup_resources }} @@ -107,7 +107,7 @@ jobs: KMGENERIC_URL: ${{ needs.deploy.outputs.WEB_APP_URL || inputs.existing_webapp_url }} KMGENERIC_URL_API: ${{ needs.deploy.outputs.API_APP_URL || inputs.existing_webapp_url }} TEST_SUITE: ${{ inputs.trigger_type == 'workflow_dispatch' && inputs.run_e2e_tests || 'GoldenPath-Testing' }} - AZURE_ENV_USE_CASE: ${{ inputs.azure_env_use_case }} + USE_CASE: ${{ inputs.use_case }} secrets: inherit cleanup-deployment: @@ -121,7 +121,7 @@ jobs: existing_webapp_url: ${{ inputs.existing_webapp_url }} resource_group_name: ${{ needs.deploy.outputs.RESOURCE_GROUP_NAME }} azure_location: ${{ needs.deploy.outputs.AZURE_LOCATION }} - azure_env_openai_location: ${{ needs.deploy.outputs.AZURE_ENV_OPENAI_LOCATION }} + azure_env_ai_service_location: ${{ needs.deploy.outputs.AZURE_ENV_AI_SERVICE_LOCATION }} env_name: ${{ needs.deploy.outputs.ENV_NAME }} image_tag: ${{ needs.deploy.outputs.IMAGE_TAG }} secrets: inherit diff --git a/.github/workflows/deploy-v2.yml b/.github/workflows/deploy-v2.yml index 624465792..33aa0adec 100644 --- a/.github/workflows/deploy-v2.yml +++ b/.github/workflows/deploy-v2.yml @@ -39,7 +39,6 @@ on: - 'australiaeast' - 'eastus' - 'eastus2' - - 'francecentral' - 'japaneast' - 'swedencentral' - 'uksouth' @@ -70,7 +69,7 @@ on: required: false default: false type: boolean - AZURE_ENV_USE_CASE: + USE_CASE: description: 'Specify Use case to deploy' type: 'choice' options: @@ -87,12 +86,12 @@ on: - 'GoldenPath-Testing' - 'Smoke-Testing' - 'None' - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: + AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: description: 'Log Analytics Workspace ID (Optional)' required: false default: '' type: string - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: + AZURE_EXISTING_AIPROJECT_RESOURCE_ID: description: 'AI Project Resource ID (Optional)' required: false default: '' @@ -102,6 +101,8 @@ on: required: false default: '' type: string + schedule: + - cron: '0 9,21 * * *' # Runs at 9:00 AM and 9:00 PM GMT permissions: contents: read @@ -120,10 +121,10 @@ jobs: build_docker_image: ${{ steps.validate.outputs.build_docker_image }} cleanup_resources: ${{ steps.validate.outputs.cleanup_resources }} run_e2e_tests: ${{ steps.validate.outputs.run_e2e_tests }} - azure_env_log_analytics_workspace_id: ${{ steps.validate.outputs.azure_env_log_analytics_workspace_id }} - azure_existing_ai_project_resource_id: ${{ steps.validate.outputs.azure_existing_ai_project_resource_id }} + azure_env_existing_log_analytics_workspace_rid: ${{ steps.validate.outputs.azure_env_existing_log_analytics_workspace_rid }} + azure_existing_aiproject_resource_id: ${{ steps.validate.outputs.azure_existing_aiproject_resource_id }} existing_webapp_url: ${{ steps.validate.outputs.existing_webapp_url }} - azure_env_use_case: ${{ steps.validate.outputs.azure_env_use_case }} + use_case: ${{ steps.validate.outputs.use_case }} runner_os: ${{ steps.validate.outputs.runner_os }} steps: @@ -138,10 +139,10 @@ jobs: INPUT_BUILD_DOCKER_IMAGE: ${{ github.event.inputs.build_docker_image }} INPUT_CLEANUP_RESOURCES: ${{ github.event.inputs.cleanup_resources }} INPUT_RUN_E2E_TESTS: ${{ github.event.inputs.run_e2e_tests }} - INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ github.event.inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} - INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ github.event.inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} + INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: ${{ github.event.inputs.AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID }} + INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID: ${{ github.event.inputs.AZURE_EXISTING_AIPROJECT_RESOURCE_ID }} INPUT_EXISTING_WEBAPP_URL: ${{ github.event.inputs.existing_webapp_url }} - INPUT_AZURE_ENV_USE_CASE: ${{ github.event.inputs.AZURE_ENV_USE_CASE }} + INPUT_USE_CASE: ${{ github.event.inputs.USE_CASE }} INPUT_RUNNER_OS: ${{ github.event.inputs.runner_os }} run: | @@ -232,32 +233,32 @@ jobs: echo "✅ run_e2e_tests: '$TEST_OPTION' is valid" fi - # Validate AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID (optional, Azure Resource ID format) - if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" ]]; then - if [[ ! "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then - echo "❌ ERROR: AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID is invalid. Must be a valid Azure Resource ID format:" + # Validate AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID (optional, Azure Resource ID format) + if [[ -n "$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID" ]]; then + if [[ ! "$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then + echo "❌ ERROR: AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID is invalid. Must be a valid Azure Resource ID format:" echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}" - echo " Got: '$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID'" + echo " Got: '$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID'" VALIDATION_FAILED=true else - echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: Valid Resource ID format" + echo "✅ AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: Valid Resource ID format" fi else - echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: Not provided (optional)" + echo "✅ AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: Not provided (optional)" fi - # Validate AZURE_EXISTING_AI_PROJECT_RESOURCE_ID (optional, Azure Resource ID format) - if [[ -n "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" ]]; then - if [[ ! "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; then - echo "❌ ERROR: AZURE_EXISTING_AI_PROJECT_RESOURCE_ID is invalid. Must be a valid Azure Resource ID format:" + # Validate AZURE_EXISTING_AIPROJECT_RESOURCE_ID (optional, Azure Resource ID format) + if [[ -n "$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID" ]]; then + if [[ ! "$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; then + echo "❌ ERROR: AZURE_EXISTING_AIPROJECT_RESOURCE_ID is invalid. Must be a valid Azure Resource ID format:" echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.CognitiveServices/accounts/{accountName}/projects/{projectName}" - echo " Got: '$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID'" + echo " Got: '$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID'" VALIDATION_FAILED=true else - echo "✅ AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: Valid Resource ID format" + echo "✅ AZURE_EXISTING_AIPROJECT_RESOURCE_ID: Valid Resource ID format" fi else - echo "✅ AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: Not provided (optional)" + echo "✅ AZURE_EXISTING_AIPROJECT_RESOURCE_ID: Not provided (optional)" fi # Validate existing_webapp_url (optional, must start with https) @@ -272,13 +273,13 @@ jobs: echo "✅ existing_webapp_url: Not provided (will perform deployment)" fi - # Validate AZURE_ENV_USE_CASE (specific allowed values) - USE_CASE="${INPUT_AZURE_ENV_USE_CASE:-telecom}" + # Validate USE_CASE (specific allowed values) + USE_CASE="${INPUT_USE_CASE:-telecom}" if [[ "$USE_CASE" != "telecom" && "$USE_CASE" != "IT_helpdesk" ]]; then - echo "❌ ERROR: AZURE_ENV_USE_CASE must be one of: telecom, IT_helpdesk, got: '$USE_CASE'" + echo "❌ ERROR: USE_CASE must be one of: telecom, IT_helpdesk, got: '$USE_CASE'" VALIDATION_FAILED=true else - echo "✅ AZURE_ENV_USE_CASE: '$USE_CASE' is valid" + echo "✅ USE_CASE: '$USE_CASE' is valid" fi # Fail workflow if any validation failed @@ -300,10 +301,10 @@ jobs: echo "build_docker_image=$BUILD_DOCKER" >> $GITHUB_OUTPUT echo "cleanup_resources=$CLEANUP_RESOURCES" >> $GITHUB_OUTPUT echo "run_e2e_tests=$TEST_OPTION" >> $GITHUB_OUTPUT - echo "azure_env_log_analytics_workspace_id=$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" >> $GITHUB_OUTPUT - echo "azure_existing_ai_project_resource_id=$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" >> $GITHUB_OUTPUT + echo "azure_env_existing_log_analytics_workspace_rid=$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID" >> $GITHUB_OUTPUT + echo "azure_existing_aiproject_resource_id=$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID" >> $GITHUB_OUTPUT echo "existing_webapp_url=$INPUT_EXISTING_WEBAPP_URL" >> $GITHUB_OUTPUT - echo "azure_env_use_case=$USE_CASE" >> $GITHUB_OUTPUT + echo "use_case=$USE_CASE" >> $GITHUB_OUTPUT echo "runner_os=$RUNNER_OS" >> $GITHUB_OUTPUT @@ -320,10 +321,10 @@ jobs: build_docker_image: ${{ needs.validate-inputs.outputs.build_docker_image == 'true' }} cleanup_resources: ${{ needs.validate-inputs.outputs.cleanup_resources == 'true' }} run_e2e_tests: ${{ needs.validate-inputs.outputs.run_e2e_tests || 'GoldenPath-Testing' }} - azure_env_log_analytics_workspace_id: ${{ needs.validate-inputs.outputs.azure_env_log_analytics_workspace_id || '' }} - azure_existing_ai_project_resource_id: ${{ needs.validate-inputs.outputs.azure_existing_ai_project_resource_id || '' }} + azure_env_existing_log_analytics_workspace_rid: ${{ needs.validate-inputs.outputs.azure_env_existing_log_analytics_workspace_rid || '' }} + azure_existing_aiproject_resource_id: ${{ needs.validate-inputs.outputs.azure_existing_aiproject_resource_id || '' }} existing_webapp_url: ${{ needs.validate-inputs.outputs.existing_webapp_url || '' }} - azure_env_use_case: ${{ needs.validate-inputs.outputs.azure_env_use_case || 'telecom' }} + use_case: ${{ needs.validate-inputs.outputs.use_case || 'telecom' }} trigger_type: ${{ github.event_name }} secrets: inherit \ No newline at end of file diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 1cb2676d5..15f4367f5 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -6,6 +6,14 @@ on: - main - dev - demo + paths: + - 'src/App/**' + - 'src/api/**' + - 'src/**/*.Dockerfile' + - 'src/gunicorn.conf.py' + - 'src/start.sh' + - 'src/start.cmd' + - '.github/workflows/docker-build.yml' pull_request: types: - opened @@ -16,6 +24,14 @@ on: - main - dev - demo + paths: + - 'src/App/**' + - 'src/api/**' + - 'src/**/*.Dockerfile' + - 'src/gunicorn.conf.py' + - 'src/start.sh' + - 'src/start.cmd' + - '.github/workflows/docker-build.yml' workflow_dispatch: permissions: diff --git a/.github/workflows/job-azure-deploy.yml b/.github/workflows/job-azure-deploy.yml index 694af7d8a..3bcd2b046 100644 --- a/.github/workflows/job-azure-deploy.yml +++ b/.github/workflows/job-azure-deploy.yml @@ -51,17 +51,17 @@ on: required: false default: '' type: string - azure_env_log_analytics_workspace_id: + azure_env_existing_log_analytics_workspace_rid: description: 'Log Analytics Workspace ID (Optional)' required: false default: '' type: string - azure_existing_ai_project_resource_id: + azure_existing_aiproject_resource_id: description: 'AI Project Resource ID (Optional)' required: false default: '' type: string - azure_env_use_case: + use_case: description: 'Azure Environment Use Case (telecom or IT_helpdesk)' required: false default: 'telecom' @@ -87,9 +87,9 @@ on: AZURE_LOCATION: description: "Azure Location" value: ${{ jobs.azure-setup.outputs.AZURE_LOCATION }} - AZURE_ENV_OPENAI_LOCATION: - description: "Azure OpenAI Location" - value: ${{ jobs.azure-setup.outputs.AZURE_ENV_OPENAI_LOCATION }} + AZURE_ENV_AI_SERVICE_LOCATION: + description: "Azure AI Service Location" + value: ${{ jobs.azure-setup.outputs.AZURE_ENV_AI_SERVICE_LOCATION }} IMAGE_TAG: description: "Docker Image Tag Used" value: ${{ jobs.azure-setup.outputs.IMAGE_TAG }} @@ -106,6 +106,7 @@ env: CLEANUP_RESOURCES: ${{ inputs.trigger_type != 'workflow_dispatch' || inputs.cleanup_resources }} RUN_E2E_TESTS: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.run_e2e_tests || 'GoldenPath-Testing') || 'GoldenPath-Testing' }} BUILD_DOCKER_IMAGE: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.build_docker_image || false) || false }} + RG_TAGS: ${{ vars.RG_TAGS }} jobs: azure-setup: @@ -117,7 +118,7 @@ jobs: RESOURCE_GROUP_NAME: ${{ steps.check_create_rg.outputs.RESOURCE_GROUP_NAME }} ENV_NAME: ${{ steps.generate_env_name.outputs.ENV_NAME }} AZURE_LOCATION: ${{ steps.set_region.outputs.AZURE_LOCATION }} - AZURE_ENV_OPENAI_LOCATION: ${{ steps.set_region.outputs.AZURE_ENV_OPENAI_LOCATION }} + AZURE_ENV_AI_SERVICE_LOCATION: ${{ steps.set_region.outputs.AZURE_ENV_AI_SERVICE_LOCATION }} IMAGE_TAG: ${{ steps.determine_image_tag.outputs.IMAGE_TAG }} QUOTA_FAILED: ${{ steps.quota_failure_output.outputs.QUOTA_FAILED }} EXP_ENABLED: ${{ steps.configure_exp.outputs.EXP_ENABLED }} @@ -136,8 +137,8 @@ jobs: INPUT_EXP: ${{ inputs.exp }} INPUT_CLEANUP_RESOURCES: ${{ inputs.cleanup_resources }} INPUT_RUN_E2E_TESTS: ${{ inputs.run_e2e_tests }} - INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.azure_env_log_analytics_workspace_id }} - INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.azure_existing_ai_project_resource_id }} + INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.azure_env_existing_log_analytics_workspace_rid }} + INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID: ${{ inputs.azure_existing_aiproject_resource_id }} INPUT_EXISTING_WEBAPP_URL: ${{ inputs.existing_webapp_url }} INPUT_DOCKER_IMAGE_TAG: ${{ inputs.docker_image_tag }} run: | @@ -233,27 +234,27 @@ jobs: fi fi - # Validate AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID (Azure Resource ID format) - if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" ]]; then - if [[ ! "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then - echo "❌ ERROR: AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID is invalid. Must be a valid Azure Resource ID format:" + # Validate AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID (Azure Resource ID format) + if [[ -n "$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID" ]]; then + if [[ ! "$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then + echo "❌ ERROR: AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID is invalid. Must be a valid Azure Resource ID format:" echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}" - echo " Got: '$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID'" + echo " Got: '$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID'" VALIDATION_FAILED=true else - echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: Valid Resource ID format" + echo "✅ AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: Valid Resource ID format" fi fi - # Validate AZURE_EXISTING_AI_PROJECT_RESOURCE_ID (Azure Resource ID format) - if [[ -n "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" ]]; then - if [[ ! "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; then - echo "❌ ERROR: AZURE_EXISTING_AI_PROJECT_RESOURCE_ID is invalid. Must be a valid Azure Resource ID format:" + # Validate AZURE_EXISTING_AIPROJECT_RESOURCE_ID (Azure Resource ID format) + if [[ -n "$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID" ]]; then + if [[ ! "$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; then + echo "❌ ERROR: AZURE_EXISTING_AIPROJECT_RESOURCE_ID is invalid. Must be a valid Azure Resource ID format:" echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.CognitiveServices/accounts/{accountName}/projects/{projectName}" - echo " Got: '$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID'" + echo " Got: '$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID'" VALIDATION_FAILED=true else - echo "✅ AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: Valid Resource ID format" + echo "✅ AZURE_EXISTING_AIPROJECT_RESOURCE_ID: Valid Resource ID format" fi fi @@ -297,8 +298,8 @@ jobs: shell: bash env: INPUT_EXP: ${{ inputs.EXP }} - INPUT_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} - INPUT_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} + INPUT_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.azure_env_existing_log_analytics_workspace_rid }} + INPUT_AI_PROJECT_RESOURCE_ID: ${{ inputs.azure_existing_aiproject_resource_id }} run: | echo "🔍 Validating EXP configuration..." @@ -307,11 +308,11 @@ jobs: if [[ "$INPUT_EXP" == "true" ]]; then EXP_ENABLED="true" echo "✅ EXP explicitly enabled by user input" - elif [[ -n "$INPUT_LOG_ANALYTICS_WORKSPACE_ID" ]] || [[ -n "$INPUT_AI_PROJECT_RESOURCE_ID" ]]; then + elif [[ -n "$INPUT_LOG_ANALYTICS_WORKSPACE_RID" ]] || [[ -n "$INPUT_AI_PROJECT_RESOURCE_ID" ]]; then echo "🔧 AUTO-ENABLING EXP: EXP parameter values were provided but EXP was not explicitly enabled." echo "" echo "You provided values for:" - [[ -n "$INPUT_LOG_ANALYTICS_WORKSPACE_ID" ]] && echo " - Azure Log Analytics Workspace ID: '$INPUT_LOG_ANALYTICS_WORKSPACE_ID'" + [[ -n "$INPUT_LOG_ANALYTICS_WORKSPACE_RID" ]] && echo " - Azure Log Analytics Workspace ID: '$INPUT_LOG_ANALYTICS_WORKSPACE_RID'" [[ -n "$INPUT_AI_PROJECT_RESOURCE_ID" ]] && echo " - Azure AI Project Resource ID: '$INPUT_AI_PROJECT_RESOURCE_ID'" echo "" echo "✅ Automatically enabling EXP to use these values." @@ -374,8 +375,8 @@ jobs: INPUT_AZURE_LOCATION: ${{ inputs.azure_location }} run: | echo "Selected Region from Quota Check: $VALID_REGION" - echo "AZURE_ENV_OPENAI_LOCATION=$VALID_REGION" >> $GITHUB_ENV - echo "AZURE_ENV_OPENAI_LOCATION=$VALID_REGION" >> $GITHUB_OUTPUT + echo "AZURE_ENV_AI_SERVICE_LOCATION=$VALID_REGION" >> $GITHUB_ENV + echo "AZURE_ENV_AI_SERVICE_LOCATION=$VALID_REGION" >> $GITHUB_OUTPUT if [[ "$INPUT_TRIGGER_TYPE" == "workflow_dispatch" && -n "$INPUT_AZURE_LOCATION" ]]; then USER_SELECTED_LOCATION="$INPUT_AZURE_LOCATION" @@ -416,7 +417,7 @@ jobs: rg_exists=$(az group exists --name $RESOURCE_GROUP_NAME) if [ "$rg_exists" = "false" ]; then echo "📦 Resource group does not exist. Creating new resource group '$RESOURCE_GROUP_NAME' in location '$AZURE_LOCATION'..." - az group create --name $RESOURCE_GROUP_NAME --location $AZURE_LOCATION || { echo "❌ Error creating resource group"; exit 1; } + az group create --name $RESOURCE_GROUP_NAME --location $AZURE_LOCATION --tags ${{ env.RG_TAGS }} || { echo "❌ Error creating resource group"; exit 1; } echo "✅ Resource group '$RESOURCE_GROUP_NAME' created successfully." else echo "✅ Resource group '$RESOURCE_GROUP_NAME' already exists. Deploying to existing resource group." @@ -504,7 +505,7 @@ jobs: EXP_DISPLAY: ${{ steps.configure_exp.outputs.EXP_ENABLED == 'true' && '✅ Yes' || '❌ No' }} CLEANUP_DISPLAY: ${{ env.CLEANUP_RESOURCES == 'true' && '✅ Yes' || '❌ No' }} BUILD_DOCKER_DISPLAY: ${{ env.BUILD_DOCKER_IMAGE == 'true' && '✅ Yes' || '❌ No' }} - AZURE_ENV_USE_CASE: ${{ inputs.azure_env_use_case }} + USE_CASE: ${{ inputs.use_case }} run: | echo "## 📋 Workflow Configuration Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY @@ -516,7 +517,7 @@ jobs: echo "| **Run E2E Tests** | \`${{ env.RUN_E2E_TESTS }}\` |" >> $GITHUB_STEP_SUMMARY echo "| **Cleanup Resources** | $CLEANUP_DISPLAY |" >> $GITHUB_STEP_SUMMARY echo "| **Build Docker Image** | $BUILD_DOCKER_DISPLAY |" >> $GITHUB_STEP_SUMMARY - echo "| **Use Case** | \`$AZURE_ENV_USE_CASE\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Use Case** | \`$USE_CASE\` |" >> $GITHUB_STEP_SUMMARY if [[ "$INPUT_TRIGGER_TYPE" == "workflow_dispatch" && -n "$INPUT_AZURE_LOCATION" ]]; then echo "| **Azure Location** | \`$INPUT_AZURE_LOCATION\` (User Selected) |" >> $GITHUB_STEP_SUMMARY @@ -543,16 +544,16 @@ jobs: uses: ./.github/workflows/job-deploy-linux.yml with: ENV_NAME: ${{ needs.azure-setup.outputs.ENV_NAME }} - AZURE_ENV_OPENAI_LOCATION: ${{ needs.azure-setup.outputs.AZURE_ENV_OPENAI_LOCATION }} + AZURE_ENV_AI_SERVICE_LOCATION: ${{ needs.azure-setup.outputs.AZURE_ENV_AI_SERVICE_LOCATION }} AZURE_LOCATION: ${{ needs.azure-setup.outputs.AZURE_LOCATION }} RESOURCE_GROUP_NAME: ${{ needs.azure-setup.outputs.RESOURCE_GROUP_NAME }} IMAGE_TAG: ${{ needs.azure-setup.outputs.IMAGE_TAG }} BUILD_DOCKER_IMAGE: ${{ github.event.inputs.build_docker_image || 'false' }} EXP: ${{ needs.azure-setup.outputs.EXP_ENABLED }} WAF_ENABLED: ${{ inputs.waf_enabled == true && 'true' || 'false' }} - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.azure_env_log_analytics_workspace_id }} - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.azure_existing_ai_project_resource_id }} - AZURE_ENV_USE_CASE: ${{ inputs.azure_env_use_case }} + AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.azure_env_existing_log_analytics_workspace_rid }} + AZURE_EXISTING_AIPROJECT_RESOURCE_ID: ${{ inputs.azure_existing_aiproject_resource_id }} + USE_CASE: ${{ inputs.use_case }} secrets: inherit deploy-windows: @@ -562,14 +563,14 @@ jobs: uses: ./.github/workflows/job-deploy-windows.yml with: ENV_NAME: ${{ needs.azure-setup.outputs.ENV_NAME }} - AZURE_ENV_OPENAI_LOCATION: ${{ needs.azure-setup.outputs.AZURE_ENV_OPENAI_LOCATION }} + AZURE_ENV_AI_SERVICE_LOCATION: ${{ needs.azure-setup.outputs.AZURE_ENV_AI_SERVICE_LOCATION }} AZURE_LOCATION: ${{ needs.azure-setup.outputs.AZURE_LOCATION }} RESOURCE_GROUP_NAME: ${{ needs.azure-setup.outputs.RESOURCE_GROUP_NAME }} IMAGE_TAG: ${{ needs.azure-setup.outputs.IMAGE_TAG }} BUILD_DOCKER_IMAGE: ${{ github.event.inputs.build_docker_image || 'false' }} EXP: ${{ needs.azure-setup.outputs.EXP_ENABLED }} WAF_ENABLED: ${{ inputs.waf_enabled == true && 'true' || 'false' }} - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.azure_env_log_analytics_workspace_id }} - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.azure_existing_ai_project_resource_id }} - AZURE_ENV_USE_CASE: ${{ inputs.azure_env_use_case }} + AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.azure_env_existing_log_analytics_workspace_rid }} + AZURE_EXISTING_AIPROJECT_RESOURCE_ID: ${{ inputs.azure_existing_aiproject_resource_id }} + USE_CASE: ${{ inputs.use_case }} secrets: inherit \ No newline at end of file diff --git a/.github/workflows/job-cleanup-resources.yml b/.github/workflows/job-cleanup-resources.yml index 4608ce880..83a60a576 100644 --- a/.github/workflows/job-cleanup-resources.yml +++ b/.github/workflows/job-cleanup-resources.yml @@ -29,8 +29,8 @@ on: description: 'Azure Location' required: true type: string - azure_env_openai_location: - description: 'Azure OpenAI Location' + azure_env_ai_service_location: + description: 'Azure AI Service Location' required: true type: string env_name: @@ -50,7 +50,7 @@ jobs: env: RESOURCE_GROUP_NAME: ${{ inputs.resource_group_name }} AZURE_LOCATION: ${{ inputs.azure_location }} - AZURE_ENV_OPENAI_LOCATION: ${{ inputs.azure_env_openai_location }} + AZURE_ENV_AI_SERVICE_LOCATION: ${{ inputs.azure_env_ai_service_location }} ENV_NAME: ${{ inputs.env_name }} IMAGE_TAG: ${{ inputs.image_tag }} steps: diff --git a/.github/workflows/job-deploy-linux.yml b/.github/workflows/job-deploy-linux.yml index 3a70e45db..343e40820 100644 --- a/.github/workflows/job-deploy-linux.yml +++ b/.github/workflows/job-deploy-linux.yml @@ -6,7 +6,7 @@ on: ENV_NAME: required: true type: string - AZURE_ENV_OPENAI_LOCATION: + AZURE_ENV_AI_SERVICE_LOCATION: required: true type: string AZURE_LOCATION: @@ -28,13 +28,13 @@ on: required: false type: string default: 'false' - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: + AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: required: false type: string - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: + AZURE_EXISTING_AIPROJECT_RESOURCE_ID: required: false type: string - AZURE_ENV_USE_CASE: + USE_CASE: required: false type: string default: 'telecom' @@ -61,16 +61,16 @@ jobs: shell: bash env: INPUT_ENV_NAME: ${{ inputs.ENV_NAME }} - INPUT_AZURE_ENV_OPENAI_LOCATION: ${{ inputs.AZURE_ENV_OPENAI_LOCATION }} + INPUT_AZURE_ENV_AI_SERVICE_LOCATION: ${{ inputs.AZURE_ENV_AI_SERVICE_LOCATION }} INPUT_AZURE_LOCATION: ${{ inputs.AZURE_LOCATION }} INPUT_RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }} INPUT_IMAGE_TAG: ${{ inputs.IMAGE_TAG }} INPUT_BUILD_DOCKER_IMAGE: ${{ inputs.BUILD_DOCKER_IMAGE }} INPUT_EXP: ${{ inputs.EXP }} INPUT_WAF_ENABLED: ${{ inputs.WAF_ENABLED }} - INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} - INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} - INPUT_AZURE_ENV_USE_CASE: ${{ inputs.AZURE_ENV_USE_CASE }} + INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID }} + INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AIPROJECT_RESOURCE_ID }} + INPUT_USE_CASE: ${{ inputs.USE_CASE }} run: | echo "🔍 Validating workflow input parameters..." VALIDATION_FAILED=false @@ -86,15 +86,15 @@ jobs: echo "✅ ENV_NAME: '$INPUT_ENV_NAME' is valid" fi - # Validate AZURE_ENV_OPENAI_LOCATION (required, Azure region format) - if [[ -z "$INPUT_AZURE_ENV_OPENAI_LOCATION" ]]; then - echo "❌ ERROR: AZURE_ENV_OPENAI_LOCATION is required but not provided" + # Validate AZURE_ENV_AI_SERVICE_LOCATION (required, Azure region format) + if [[ -z "$INPUT_AZURE_ENV_AI_SERVICE_LOCATION" ]]; then + echo "❌ ERROR: AZURE_ENV_AI_SERVICE_LOCATION is required but not provided" VALIDATION_FAILED=true - elif [[ ! "$INPUT_AZURE_ENV_OPENAI_LOCATION" =~ ^[a-z0-9]+$ ]]; then - echo "❌ ERROR: AZURE_ENV_OPENAI_LOCATION '$INPUT_AZURE_ENV_OPENAI_LOCATION' is invalid. Must contain only lowercase letters and numbers" + elif [[ ! "$INPUT_AZURE_ENV_AI_SERVICE_LOCATION" =~ ^[a-z0-9]+$ ]]; then + echo "❌ ERROR: AZURE_ENV_AI_SERVICE_LOCATION '$INPUT_AZURE_ENV_AI_SERVICE_LOCATION' is invalid. Must contain only lowercase letters and numbers" VALIDATION_FAILED=true else - echo "✅ AZURE_ENV_OPENAI_LOCATION: '$INPUT_AZURE_ENV_OPENAI_LOCATION' is valid" + echo "✅ AZURE_ENV_AI_SERVICE_LOCATION: '$INPUT_AZURE_ENV_AI_SERVICE_LOCATION' is valid" fi # Validate AZURE_LOCATION (required, Azure region format) @@ -157,37 +157,37 @@ jobs: echo "✅ WAF_ENABLED: '$INPUT_WAF_ENABLED' is valid" fi - # Validate AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID (optional, if provided must be valid Resource ID) - if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" ]]; then - if [[ ! "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then - echo "❌ ERROR: AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID is invalid. Must be a valid Azure Resource ID format:" + # Validate AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID (optional, if provided must be valid Resource ID) + if [[ -n "$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID" ]]; then + if [[ ! "$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then + echo "❌ ERROR: AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID is invalid. Must be a valid Azure Resource ID format:" echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}" - echo " Got: '$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID'" + echo " Got: '$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID'" VALIDATION_FAILED=true else - echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: Valid Resource ID format" + echo "✅ AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: Valid Resource ID format" fi fi - # Validate AZURE_EXISTING_AI_PROJECT_RESOURCE_ID (optional, if provided must be valid Resource ID) - if [[ -n "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" ]]; then - if [[ ! "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; then - echo "❌ ERROR: AZURE_EXISTING_AI_PROJECT_RESOURCE_ID is invalid. Must be a valid Azure Resource ID format:" + # Validate AZURE_EXISTING_AIPROJECT_RESOURCE_ID (optional, Azure Resource ID format) + if [[ -n "$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID" ]]; then + if [[ ! "$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; then + echo "❌ ERROR: AZURE_EXISTING_AIPROJECT_RESOURCE_ID is invalid. Must be a valid Azure Resource ID format:" echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.CognitiveServices/accounts/{accountName}/projects/{projectName}" - echo " Got: '$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID'" + echo " Got: '$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID'" VALIDATION_FAILED=true else - echo "✅ AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: Valid Resource ID format" + echo "✅ AZURE_EXISTING_AIPROJECT_RESOURCE_ID: Valid Resource ID format" fi fi - # Validate AZURE_ENV_USE_CASE (optional, must be 'telecom' or 'IT_helpdesk') - USE_CASE="${INPUT_AZURE_ENV_USE_CASE:-telecom}" + # Validate USE_CASE (optional, must be 'telecom' or 'IT_helpdesk') + USE_CASE="${INPUT_USE_CASE:-telecom}" if [[ "$USE_CASE" != "telecom" && "$USE_CASE" != "IT_helpdesk" ]]; then - echo "❌ ERROR: AZURE_ENV_USE_CASE must be one of: telecom, IT_helpdesk, got: '$USE_CASE'" + echo "❌ ERROR: USE_CASE must be one of: telecom, IT_helpdesk, got: '$USE_CASE'" VALIDATION_FAILED=true else - echo "✅ AZURE_ENV_USE_CASE: '$USE_CASE' is valid" + echo "✅ USE_CASE: '$USE_CASE' is valid" fi # Fail workflow if any validation failed @@ -234,15 +234,15 @@ jobs: shell: bash env: ENV_NAME: ${{ inputs.ENV_NAME }} - AZURE_ENV_OPENAI_LOCATION: ${{ inputs.AZURE_ENV_OPENAI_LOCATION }} + AZURE_ENV_AI_SERVICE_LOCATION: ${{ inputs.AZURE_ENV_AI_SERVICE_LOCATION }} AZURE_LOCATION: ${{ inputs.AZURE_LOCATION }} RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }} IMAGE_TAG: ${{ inputs.IMAGE_TAG }} BUILD_DOCKER_IMAGE: ${{ inputs.BUILD_DOCKER_IMAGE }} EXP: ${{ inputs.EXP }} - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} - AZURE_ENV_USE_CASE: ${{ inputs.AZURE_ENV_USE_CASE }} + AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID }} + AZURE_EXISTING_AIPROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AIPROJECT_RESOURCE_ID }} + USE_CASE: ${{ inputs.USE_CASE }} run: | set -e echo "Starting azd deployment..." @@ -261,11 +261,11 @@ jobs: # Set additional parameters azd env set AZURE_SUBSCRIPTION_ID="${{ secrets.AZURE_SUBSCRIPTION_ID }}" - azd env set AZURE_ENV_OPENAI_LOCATION="$AZURE_ENV_OPENAI_LOCATION" + azd env set AZURE_ENV_AI_SERVICE_LOCATION="$AZURE_ENV_AI_SERVICE_LOCATION" azd env set AZURE_LOCATION="$AZURE_LOCATION" azd env set AZURE_RESOURCE_GROUP="$RESOURCE_GROUP_NAME" - azd env set AZURE_ENV_IMAGETAG="$IMAGE_TAG" - azd env set AZURE_ENV_USE_CASE="$AZURE_ENV_USE_CASE" + azd env set AZURE_ENV_IMAGE_TAG="$IMAGE_TAG" + azd env set USE_CASE="$USE_CASE" if [[ "$BUILD_DOCKER_IMAGE" == "true" ]]; then ACR_NAME=$(echo "${{ secrets.ACR_TEST_LOGIN_SERVER }}") @@ -278,22 +278,22 @@ jobs: if [[ "$EXP" == "true" ]]; then echo "✅ EXP ENABLED - Setting EXP parameters..." - if [[ -n "$AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" ]]; then - EXP_LOG_ANALYTICS_ID="$AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" + if [[ -n "$AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID" ]]; then + EXP_LOG_ANALYTICS_ID="$AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID" else - EXP_LOG_ANALYTICS_ID="${{ secrets.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" + EXP_LOG_ANALYTICS_ID="${{ secrets.AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID }}" fi - if [[ -n "$AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" ]]; then - EXP_AI_PROJECT_ID="$AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" + if [[ -n "$AZURE_EXISTING_AIPROJECT_RESOURCE_ID" ]]; then + EXP_AI_PROJECT_ID="$AZURE_EXISTING_AIPROJECT_RESOURCE_ID" else - EXP_AI_PROJECT_ID="${{ secrets.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" + EXP_AI_PROJECT_ID="${{ secrets.AZURE_EXISTING_AIPROJECT_RESOURCE_ID }}" fi - echo "AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: $EXP_LOG_ANALYTICS_ID" - echo "AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: $EXP_AI_PROJECT_ID" - azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID="$EXP_LOG_ANALYTICS_ID" - azd env set AZURE_EXISTING_AI_PROJECT_RESOURCE_ID="$EXP_AI_PROJECT_ID" + echo "AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: $EXP_LOG_ANALYTICS_ID" + echo "AZURE_EXISTING_AIPROJECT_RESOURCE_ID: $EXP_AI_PROJECT_ID" + azd env set AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID="$EXP_LOG_ANALYTICS_ID" + azd env set AZURE_EXISTING_AIPROJECT_RESOURCE_ID="$EXP_AI_PROJECT_ID" else echo "❌ EXP DISABLED - Skipping EXP parameters" fi @@ -406,9 +406,9 @@ jobs: WAF_ENABLED: ${{ inputs.WAF_ENABLED }} EXP: ${{ inputs.EXP }} AZURE_LOCATION: ${{ inputs.AZURE_LOCATION }} - AZURE_ENV_OPENAI_LOCATION: ${{ inputs.AZURE_ENV_OPENAI_LOCATION }} + AZURE_ENV_AI_SERVICE_LOCATION: ${{ inputs.AZURE_ENV_AI_SERVICE_LOCATION }} IMAGE_TAG: ${{ inputs.IMAGE_TAG }} - AZURE_ENV_USE_CASE: ${{ inputs.AZURE_ENV_USE_CASE }} + USE_CASE: ${{ inputs.USE_CASE }} run: | echo "## 🚀 Deploy Job Summary (Linux)" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY @@ -418,9 +418,9 @@ jobs: echo "| **Resource Group** | \`$RESOURCE_GROUP_NAME\` |" >> $GITHUB_STEP_SUMMARY echo "| **Configuration Type** | \`${{ inputs.WAF_ENABLED == 'true' && inputs.EXP == 'true' && 'WAF + EXP' || inputs.WAF_ENABLED == 'true' && inputs.EXP != 'true' && 'WAF + Non-EXP' || inputs.WAF_ENABLED != 'true' && inputs.EXP == 'true' && 'Non-WAF + EXP' || 'Non-WAF + Non-EXP' }}\` |" >> $GITHUB_STEP_SUMMARY echo "| **Azure Region (Infrastructure)** | \`$AZURE_LOCATION\` |" >> $GITHUB_STEP_SUMMARY - echo "| **Azure OpenAI Region** | \`$AZURE_ENV_OPENAI_LOCATION\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Azure AI Service Region** | \`$AZURE_ENV_AI_SERVICE_LOCATION\` |" >> $GITHUB_STEP_SUMMARY echo "| **Docker Image Tag** | \`$IMAGE_TAG\` |" >> $GITHUB_STEP_SUMMARY - echo "| **Use Case** | \`$AZURE_ENV_USE_CASE\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Use Case** | \`$USE_CASE\` |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY if [[ "${{ job.status }}" == "success" ]]; then echo "### ✅ Deployment Details" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index e4f26c820..f406e4922 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -6,7 +6,7 @@ on: ENV_NAME: required: true type: string - AZURE_ENV_OPENAI_LOCATION: + AZURE_ENV_AI_SERVICE_LOCATION: required: true type: string AZURE_LOCATION: @@ -28,13 +28,13 @@ on: required: false type: string default: 'false' - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: + AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: required: false type: string - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: + AZURE_EXISTING_AIPROJECT_RESOURCE_ID: required: false type: string - AZURE_ENV_USE_CASE: + USE_CASE: required: false type: string default: 'telecom' @@ -64,16 +64,16 @@ jobs: shell: bash env: INPUT_ENV_NAME: ${{ inputs.ENV_NAME }} - INPUT_AZURE_ENV_OPENAI_LOCATION: ${{ inputs.AZURE_ENV_OPENAI_LOCATION }} + INPUT_AZURE_ENV_AI_SERVICE_LOCATION: ${{ inputs.AZURE_ENV_AI_SERVICE_LOCATION }} INPUT_AZURE_LOCATION: ${{ inputs.AZURE_LOCATION }} INPUT_RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }} INPUT_IMAGE_TAG: ${{ inputs.IMAGE_TAG }} INPUT_BUILD_DOCKER_IMAGE: ${{ inputs.BUILD_DOCKER_IMAGE }} INPUT_EXP: ${{ inputs.EXP }} INPUT_WAF_ENABLED: ${{ inputs.WAF_ENABLED }} - INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} - INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} - INPUT_AZURE_ENV_USE_CASE: ${{ inputs.AZURE_ENV_USE_CASE }} + INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID }} + INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AIPROJECT_RESOURCE_ID }} + INPUT_USE_CASE: ${{ inputs.USE_CASE }} run: | echo "🔍 Validating workflow input parameters..." VALIDATION_FAILED=false @@ -89,15 +89,15 @@ jobs: echo "✅ ENV_NAME: '$INPUT_ENV_NAME' is valid" fi - # Validate AZURE_ENV_OPENAI_LOCATION (required, Azure region format) - if [[ -z "$INPUT_AZURE_ENV_OPENAI_LOCATION" ]]; then - echo "❌ ERROR: AZURE_ENV_OPENAI_LOCATION is required but not provided" + # Validate AZURE_ENV_AI_SERVICE_LOCATION (required, Azure region format) + if [[ -z "$INPUT_AZURE_ENV_AI_SERVICE_LOCATION" ]]; then + echo "❌ ERROR: AZURE_ENV_AI_SERVICE_LOCATION is required but not provided" VALIDATION_FAILED=true - elif [[ ! "$INPUT_AZURE_ENV_OPENAI_LOCATION" =~ ^[a-z0-9]+$ ]]; then - echo "❌ ERROR: AZURE_ENV_OPENAI_LOCATION '$INPUT_AZURE_ENV_OPENAI_LOCATION' is invalid. Must contain only lowercase letters and numbers" + elif [[ ! "$INPUT_AZURE_ENV_AI_SERVICE_LOCATION" =~ ^[a-z0-9]+$ ]]; then + echo "❌ ERROR: AZURE_ENV_AI_SERVICE_LOCATION '$INPUT_AZURE_ENV_AI_SERVICE_LOCATION' is invalid. Must contain only lowercase letters and numbers" VALIDATION_FAILED=true else - echo "✅ AZURE_ENV_OPENAI_LOCATION: '$INPUT_AZURE_ENV_OPENAI_LOCATION' is valid" + echo "✅ AZURE_ENV_AI_SERVICE_LOCATION: '$INPUT_AZURE_ENV_AI_SERVICE_LOCATION' is valid" fi # Validate AZURE_LOCATION (required, Azure region format) @@ -160,37 +160,37 @@ jobs: echo "✅ WAF_ENABLED: '$INPUT_WAF_ENABLED' is valid" fi - # Validate AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID (optional, if provided must be valid Resource ID) - if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" ]]; then - if [[ ! "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then - echo "❌ ERROR: AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID is invalid. Must be a valid Azure Resource ID format:" + # Validate AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID (optional, if provided must be valid Resource ID) + if [[ -n "$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID" ]]; then + if [[ ! "$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then + echo "❌ ERROR: AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID is invalid. Must be a valid Azure Resource ID format:" echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}" - echo " Got: '$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID'" + echo " Got: '$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID'" VALIDATION_FAILED=true else - echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: Valid Resource ID format" + echo "✅ AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: Valid Resource ID format" fi fi - # Validate AZURE_EXISTING_AI_PROJECT_RESOURCE_ID (optional, if provided must be valid Resource ID) - if [[ -n "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" ]]; then - if [[ ! "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; then - echo "❌ ERROR: AZURE_EXISTING_AI_PROJECT_RESOURCE_ID is invalid. Must be a valid Azure Resource ID format:" + # Validate AZURE_EXISTING_AIPROJECT_RESOURCE_ID (optional, Azure Resource ID format) + if [[ -n "$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID" ]]; then + if [[ ! "$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; then + echo "❌ ERROR: AZURE_EXISTING_AIPROJECT_RESOURCE_ID is invalid. Must be a valid Azure Resource ID format:" echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.CognitiveServices/accounts/{accountName}/projects/{projectName}" - echo " Got: '$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID'" + echo " Got: '$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID'" VALIDATION_FAILED=true else - echo "✅ AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: Valid Resource ID format" + echo "✅ AZURE_EXISTING_AIPROJECT_RESOURCE_ID: Valid Resource ID format" fi fi - # Validate AZURE_ENV_USE_CASE (optional, must be 'telecom' or 'IT_helpdesk') - USE_CASE="${INPUT_AZURE_ENV_USE_CASE:-telecom}" + # Validate USE_CASE (optional, must be 'telecom' or 'IT_helpdesk') + USE_CASE="${INPUT_USE_CASE:-telecom}" if [[ "$USE_CASE" != "telecom" && "$USE_CASE" != "IT_helpdesk" ]]; then - echo "❌ ERROR: AZURE_ENV_USE_CASE must be one of: telecom, IT_helpdesk, got: '$USE_CASE'" + echo "❌ ERROR: USE_CASE must be one of: telecom, IT_helpdesk, got: '$USE_CASE'" VALIDATION_FAILED=true else - echo "✅ AZURE_ENV_USE_CASE: '$USE_CASE' is valid" + echo "✅ USE_CASE: '$USE_CASE' is valid" fi # Fail workflow if any validation failed @@ -237,15 +237,15 @@ jobs: shell: pwsh env: INPUT_ENV_NAME: ${{ inputs.ENV_NAME }} - INPUT_AZURE_ENV_OPENAI_LOCATION: ${{ inputs.AZURE_ENV_OPENAI_LOCATION }} + INPUT_AZURE_ENV_AI_SERVICE_LOCATION: ${{ inputs.AZURE_ENV_AI_SERVICE_LOCATION }} INPUT_AZURE_LOCATION: ${{ inputs.AZURE_LOCATION }} INPUT_RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }} INPUT_IMAGE_TAG: ${{ inputs.IMAGE_TAG }} INPUT_BUILD_DOCKER_IMAGE: ${{ inputs.BUILD_DOCKER_IMAGE }} INPUT_EXP: ${{ inputs.EXP }} - INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} - INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} - INPUT_AZURE_ENV_USE_CASE: ${{ inputs.AZURE_ENV_USE_CASE }} + INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID }} + INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AIPROJECT_RESOURCE_ID }} + INPUT_USE_CASE: ${{ inputs.USE_CASE }} run: | $ErrorActionPreference = "Stop" Write-Host "Starting azd deployment..." @@ -261,11 +261,11 @@ jobs: # Set additional parameters azd env set AZURE_SUBSCRIPTION_ID="${{ secrets.AZURE_SUBSCRIPTION_ID }}" - azd env set AZURE_ENV_OPENAI_LOCATION="$env:INPUT_AZURE_ENV_OPENAI_LOCATION" + azd env set AZURE_ENV_AI_SERVICE_LOCATION="$env:INPUT_AZURE_ENV_AI_SERVICE_LOCATION" azd env set AZURE_LOCATION="$env:INPUT_AZURE_LOCATION" azd env set AZURE_RESOURCE_GROUP="$env:INPUT_RESOURCE_GROUP_NAME" - azd env set AZURE_ENV_IMAGETAG="$env:INPUT_IMAGE_TAG" - azd env set AZURE_ENV_USE_CASE="$env:INPUT_AZURE_ENV_USE_CASE" + azd env set AZURE_ENV_IMAGE_TAG="$env:INPUT_IMAGE_TAG" + azd env set USE_CASE="$env:INPUT_USE_CASE" # Set ACR name only when building Docker image if ($env:INPUT_BUILD_DOCKER_IMAGE -eq "true") { @@ -281,22 +281,22 @@ jobs: Write-Host "✅ EXP ENABLED - Setting EXP parameters..." # Set EXP variables dynamically - if ($env:INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID -ne "") { - $EXP_LOG_ANALYTICS_ID = $env:INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID + if ($env:INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID -ne "") { + $EXP_LOG_ANALYTICS_ID = $env:INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID } else { - $EXP_LOG_ANALYTICS_ID = "${{ secrets.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" + $EXP_LOG_ANALYTICS_ID = "${{ secrets.AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID }}" } - if ($env:INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID -ne "") { - $EXP_AI_PROJECT_ID = $env:INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID + if ($env:INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID -ne "") { + $EXP_AI_PROJECT_ID = $env:INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID } else { - $EXP_AI_PROJECT_ID = "${{ secrets.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" + $EXP_AI_PROJECT_ID = "${{ secrets.AZURE_EXISTING_AIPROJECT_RESOURCE_ID }}" } - Write-Host "AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: $EXP_LOG_ANALYTICS_ID" - Write-Host "AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: $EXP_AI_PROJECT_ID" - azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID="$EXP_LOG_ANALYTICS_ID" - azd env set AZURE_EXISTING_AI_PROJECT_RESOURCE_ID="$EXP_AI_PROJECT_ID" + Write-Host "AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: $EXP_LOG_ANALYTICS_ID" + Write-Host "AZURE_EXISTING_AIPROJECT_RESOURCE_ID: $EXP_AI_PROJECT_ID" + azd env set AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID="$EXP_LOG_ANALYTICS_ID" + azd env set AZURE_EXISTING_AIPROJECT_RESOURCE_ID="$EXP_AI_PROJECT_ID" } else { Write-Host "❌ EXP DISABLED - Skipping EXP parameters" } @@ -439,9 +439,9 @@ jobs: INPUT_EXP: ${{ inputs.EXP }} INPUT_RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }} INPUT_AZURE_LOCATION: ${{ inputs.AZURE_LOCATION }} - INPUT_AZURE_ENV_OPENAI_LOCATION: ${{ inputs.AZURE_ENV_OPENAI_LOCATION }} + INPUT_AZURE_ENV_AI_SERVICE_LOCATION: ${{ inputs.AZURE_ENV_AI_SERVICE_LOCATION }} INPUT_IMAGE_TAG: ${{ inputs.IMAGE_TAG }} - INPUT_AZURE_ENV_USE_CASE: ${{ inputs.AZURE_ENV_USE_CASE }} + INPUT_USE_CASE: ${{ inputs.USE_CASE }} run: | echo "## 🚀 Deploy Job Summary (Windows)" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY @@ -463,9 +463,9 @@ jobs: echo "| **Configuration Type** | \`$CONFIG_TYPE\` |" >> $GITHUB_STEP_SUMMARY echo "| **Resource Group** | \`$INPUT_RESOURCE_GROUP_NAME\` |" >> $GITHUB_STEP_SUMMARY echo "| **Azure Region (Infrastructure)** | \`$INPUT_AZURE_LOCATION\` |" >> $GITHUB_STEP_SUMMARY - echo "| **Azure OpenAI Region** | \`$INPUT_AZURE_ENV_OPENAI_LOCATION\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Azure AI Service Region** | \`$INPUT_AZURE_ENV_AI_SERVICE_LOCATION\` |" >> $GITHUB_STEP_SUMMARY echo "| **Docker Image Tag** | \`$INPUT_IMAGE_TAG\` |" >> $GITHUB_STEP_SUMMARY - echo "| **Use Case** | \`$INPUT_AZURE_ENV_USE_CASE\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Use Case** | \`$INPUT_USE_CASE\` |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY if [ "${{ job.status }}" == "success" ]; then echo "### ✅ Deployment Details" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/job-test-automation.yml b/.github/workflows/job-test-automation.yml index 3a0b0aafd..9db1db98d 100644 --- a/.github/workflows/job-test-automation.yml +++ b/.github/workflows/job-test-automation.yml @@ -16,7 +16,7 @@ on: type: string default: "GoldenPath-Testing" description: "Test suite to run: 'Smoke-Testing', 'GoldenPath-Testing' " - AZURE_ENV_USE_CASE: + USE_CASE: required: false type: string default: "telecom" @@ -38,7 +38,7 @@ env: api_url: ${{ inputs.KMGENERIC_URL_API}} accelerator_name: "KMGeneric" test_suite: ${{ inputs.TEST_SUITE }} - azure_env_use_case: ${{ inputs.AZURE_ENV_USE_CASE }} + use_case: ${{ inputs.USE_CASE }} jobs: test: @@ -100,15 +100,15 @@ jobs: - name: Validate Use Case and Test Suite run: | - echo "Validating use case: '${{ env.azure_env_use_case }}'" + echo "Validating use case: '${{ env.use_case }}'" echo "Validating test suite: '${{ env.test_suite }}'" # Validate use case - if [ -z "${{ env.azure_env_use_case }}" ]; then - echo "ERROR: AZURE_ENV_USE_CASE is empty or not provided" + if [ -z "${{ env.use_case }}" ]; then + echo "ERROR: USE_CASE is empty or not provided" exit 1 - elif [ "${{ env.azure_env_use_case }}" != "telecom" ] && [ "${{ env.azure_env_use_case }}" != "IT_helpdesk" ]; then - echo "ERROR: Invalid AZURE_ENV_USE_CASE '${{ env.azure_env_use_case }}'. Must be 'telecom' or 'IT_helpdesk'" + elif [ "${{ env.use_case }}" != "telecom" ] && [ "${{ env.use_case }}" != "IT_helpdesk" ]; then + echo "ERROR: Invalid USE_CASE '${{ env.use_case }}'. Must be 'telecom' or 'IT_helpdesk'" exit 1 fi @@ -121,18 +121,18 @@ jobs: exit 1 fi - echo "✅ Use case '${{ env.azure_env_use_case }}' and test suite '${{ env.test_suite }}' are valid" + echo "✅ Use case '${{ env.use_case }}' and test suite '${{ env.test_suite }}' are valid" - name: Run tests(1) id: test1 run: | - if [ "${{ env.azure_env_use_case }}" == "telecom" ]; then + if [ "${{ env.use_case }}" == "telecom" ]; then if [ "${{ env.test_suite }}" == "GoldenPath-Testing" ]; then xvfb-run pytest tests/test_telecom_gp_tc.py --headed --html=report/report.html --self-contained-html elif [ "${{ env.test_suite }}" == "Smoke-Testing" ]; then xvfb-run pytest tests/test_telecom_gp_tc.py tests/test_telecom_smoke_tc.py --headed --html=report/report.html --self-contained-html fi - elif [ "${{ env.azure_env_use_case }}" == "IT_helpdesk" ]; then + elif [ "${{ env.use_case }}" == "IT_helpdesk" ]; then if [ "${{ env.test_suite }}" == "GoldenPath-Testing" ]; then xvfb-run pytest tests/test_ithelpdesk_gp_tc.py --headed --html=report/report.html --self-contained-html elif [ "${{ env.test_suite }}" == "Smoke-Testing" ]; then @@ -151,13 +151,13 @@ jobs: if: ${{ steps.test1.outcome == 'failure' }} id: test2 run: | - if [ "${{ env.azure_env_use_case }}" == "telecom" ]; then + if [ "${{ env.use_case }}" == "telecom" ]; then if [ "${{ env.test_suite }}" == "GoldenPath-Testing" ]; then xvfb-run pytest tests/test_telecom_gp_tc.py --headed --html=report/report.html --self-contained-html elif [ "${{ env.test_suite }}" == "Smoke-Testing" ]; then xvfb-run pytest tests/test_telecom_gp_tc.py tests/test_telecom_smoke_tc.py --headed --html=report/report.html --self-contained-html fi - elif [ "${{ env.azure_env_use_case }}" == "IT_helpdesk" ]; then + elif [ "${{ env.use_case }}" == "IT_helpdesk" ]; then if [ "${{ env.test_suite }}" == "GoldenPath-Testing" ]; then xvfb-run pytest tests/test_ithelpdesk_gp_tc.py --headed --html=report/report.html --self-contained-html elif [ "${{ env.test_suite }}" == "Smoke-Testing" ]; then @@ -176,13 +176,13 @@ jobs: if: ${{ steps.test2.outcome == 'failure' }} id: test3 run: | - if [ "${{ env.azure_env_use_case }}" == "telecom" ]; then + if [ "${{ env.use_case }}" == "telecom" ]; then if [ "${{ env.test_suite }}" == "GoldenPath-Testing" ]; then xvfb-run pytest tests/test_telecom_gp_tc.py --headed --html=report/report.html --self-contained-html elif [ "${{ env.test_suite }}" == "Smoke-Testing" ]; then xvfb-run pytest tests/test_telecom_gp_tc.py tests/test_telecom_smoke_tc.py --headed --html=report/report.html --self-contained-html fi - elif [ "${{ env.azure_env_use_case }}" == "IT_helpdesk" ]; then + elif [ "${{ env.use_case }}" == "IT_helpdesk" ]; then if [ "${{ env.test_suite }}" == "GoldenPath-Testing" ]; then xvfb-run pytest tests/test_ithelpdesk_gp_tc.py --headed --html=report/report.html --self-contained-html elif [ "${{ env.test_suite }}" == "Smoke-Testing" ]; then diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f9699f197..a45f09f72 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,6 +31,7 @@ on: permissions: contents: read actions: read + pull-requests: write jobs: # frontend_tests: @@ -106,7 +107,19 @@ jobs: - name: Run Backend Tests with Coverage if: env.skip_backend_tests == 'false' run: | - pytest --cov=. --cov-report=term-missing --cov-report=xml ./src/tests/api + pytest --cov=. --cov-report=term-missing --cov-report=xml --junitxml=pytest.xml ./src/tests/api + + - name: Pytest Coverage Comment + if: | + always() && + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.fork == false && + env.skip_backend_tests == 'false' + uses: MishaKav/pytest-coverage-comment@26f986d2599c288bb62f623d29c2da98609e9cd4 # v1.6.0 + with: + pytest-xml-coverage-path: coverage.xml + junitxml-path: pytest.xml + report-only-changed-files: true - name: Skip Backend Tests if: env.skip_backend_tests == 'true' diff --git a/.github/workflows/validate-bicep-params.yml b/.github/workflows/validate-bicep-params.yml new file mode 100644 index 000000000..23482b308 --- /dev/null +++ b/.github/workflows/validate-bicep-params.yml @@ -0,0 +1,116 @@ +name: Validate Bicep Parameters + +permissions: + contents: read + +on: + schedule: + - cron: '30 6 * * 3' # Wednesday 12:00 PM IST (6:30 AM UTC) + pull_request: + branches: + - main + - dev + paths: + - 'infra/**/*.bicep' + - 'infra/**/*.parameters.json' + - 'infra/scripts/validate_bicep_params.py' + workflow_dispatch: + +env: + accelerator_name: "CKM" + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Validate infra/ parameters + id: validate_infra + continue-on-error: true + env: + ACCELERATOR_NAME: ${{ env.accelerator_name }} + run: | + set +e + RUN_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + python infra/scripts/validate_bicep_params.py --dir infra --strict --no-color \ + --json-output infra_results.json \ + --html-output email_body.html \ + --accelerator-name "${ACCELERATOR_NAME}" \ + --run-url "${RUN_URL}" 2>&1 | tee infra_output.txt + EXIT_CODE=${PIPESTATUS[0]} + set -e + echo "## Infra Param Validation" >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + cat infra_output.txt >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + exit $EXIT_CODE + + - name: Set overall result + id: result + run: | + if [[ "${{ steps.validate_infra.outcome }}" == "failure" ]]; then + echo "status=failure" >> "$GITHUB_OUTPUT" + else + echo "status=success" >> "$GITHUB_OUTPUT" + fi + + - name: Upload validation results + if: always() + uses: actions/upload-artifact@v4 + with: + name: bicep-validation-results + path: | + infra_results.json + email_body.html + retention-days: 30 + + - name: Send schedule notification on failure + if: github.event_name == 'schedule' && steps.result.outputs.status == 'failure' + env: + LOGICAPP_URL: ${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }} + ACCELERATOR_NAME: ${{ env.accelerator_name }} + run: | + if [ -f email_body.html ]; then + EMAIL_BODY=$(cat email_body.html) + else + EMAIL_BODY="

Bicep parameter validation failed but no detailed report was generated.

" + fi + + jq -n \ + --arg name "${ACCELERATOR_NAME}" \ + --arg body "$EMAIL_BODY" \ + '{subject: ("Bicep Parameter Validation Report - " + $name + " - Issues Detected"), body: $body}' \ + | curl -X POST "${LOGICAPP_URL}" \ + -H "Content-Type: application/json" \ + -d @- || echo "Failed to send notification" + + - name: Send schedule notification on success + if: github.event_name == 'schedule' && steps.result.outputs.status == 'success' + env: + LOGICAPP_URL: ${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }} + ACCELERATOR_NAME: ${{ env.accelerator_name }} + run: | + if [ -f email_body.html ]; then + EMAIL_BODY=$(cat email_body.html) + else + EMAIL_BODY="

Bicep parameter validation passed. No detailed report was generated.

" + fi + + jq -n \ + --arg name "${ACCELERATOR_NAME}" \ + --arg body "$EMAIL_BODY" \ + '{subject: ("Bicep Parameter Validation Report - " + $name + " - Passed"), body: $body}' \ + | curl -X POST "${LOGICAPP_URL}" \ + -H "Content-Type: application/json" \ + -d @- || echo "Failed to send notification" + + - name: Fail if errors found + if: steps.result.outputs.status == 'failure' + run: exit 1 diff --git a/README.md b/README.md index 3fe5c5970..014571548 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,6 @@ Quick deploy ### How to install or deploy Follow the quick deploy steps on the deployment guide to deploy this solution to your own Azure subscription. - [Click here to launch the deployment guide](./documents/DeploymentGuide.md)

@@ -78,6 +77,8 @@ Follow the quick deploy steps on the deployment guide to deploy this solution
+> **Note**: Some tenants may have additional security restrictions that run periodically and could impact the application (e.g., blocking public network access). If you experience issues or the application stops working, check if these restrictions are the cause. In such cases, consider deploying the WAF-supported version to ensure compliance. To configure, [Click here](./documents/DeploymentGuide.md#31-choose-deployment-type-optional). + > ⚠️ **Important: Check Azure OpenAI Quota Availability**
To ensure sufficient quota is available in your subscription, please follow [quota check instructions guide](./documents/QuotaCheck.md) before you deploy the solution. @@ -112,10 +113,8 @@ either by deleting the resource group in the Portal or running `azd down`. | [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry) | Used to orchestrate and build AI workflows that combine Azure AI services. | Free Tier | [Pricing](https://azure.microsoft.com/pricing/details/ai-studio/) | | [Foundry IQ](https://learn.microsoft.com/en-us/azure/search/search-what-is-azure-search) | Powers vector-based semantic search for retrieving indexed conversation data. | Standard S1; costs scale with document count and replica/partition settings. | [Pricing](https://azure.microsoft.com/pricing/details/search/) | | [Azure Storage Account](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview) | Stores transcripts, intermediate outputs, and application assets. | Standard LRS; usage-based cost by storage/operations. | [Pricing](https://azure.microsoft.com/pricing/details/storage/blobs/) | - | [Azure AI Services (OpenAI)](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/overview) | Enables language understanding, summarization, entity extraction, and chat capabilities using GPT models. | S0 Tier; pricing depends on token volume and model used (e.g., GPT-4o-mini). | [Pricing](https://azure.microsoft.com/pricing/details/cognitive-services/) | -| [Azure Container Apps](https://learn.microsoft.com/en-us/azure/container-apps/overview) | Hosts microservices and APIs powering the front-end and backend orchestration. | Consumption plan with 0.5 vCPU, 1GiB memory; includes a free usage tier. | [Pricing](https://azure.microsoft.com/pricing/details/container-apps/) | -| [Azure Container Registry](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-intro) | Stores and serves container images used by Azure Container Apps. | Basic Tier; fixed daily cost per registry. | [Pricing](https://azure.microsoft.com/pricing/details/container-registry/) | +| [Azure App Service](https://learn.microsoft.com/en-us/azure/app-service/overview) | Hosts the front-end web application and backend API as Linux web apps. | Basic B3 (non-WAF) or Premium P1v3 (WAF); fixed cost based on plan tier. | [Pricing](https://azure.microsoft.com/pricing/details/app-service/linux/) | | [Azure Monitor / Log Analytics](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/log-analytics-overview) | Collects and analyzes telemetry and logs from services and containers. | Pay-as-you-go; charges based on data ingestion volume. | [Pricing](https://azure.microsoft.com/pricing/details/monitor/) | | [Azure SQL Database](https://learn.microsoft.com/en-us/azure/azure-sql/database/sql-database-paas-overview) | Stores structured data including insights, metadata, and indexed results. | General Purpose Tier; can be provisioned or serverless. Fixed cost if provisioned. | [Pricing](https://azure.microsoft.com/pricing/details/azure-sql-database/single/) | | [Azure Cosmos DB](https://learn.microsoft.com/en-us/azure/cosmos-db/introduction) | Used for fast, globally distributed NoSQL data storage for chat history and vector metadata. | Autoscale or provisioned throughput; fixed minimum cost if provisioned. | [Pricing](https://azure.microsoft.com/en-us/pricing/details/cosmos-db/autoscale-provisioned/) | @@ -188,7 +187,7 @@ To maintain strong security practices, it is recommended that GitHub repositorie Additional security considerations include: - Enabling [Microsoft Defender for Cloud](https://learn.microsoft.com/en-us/azure/defender-for-cloud) to monitor and secure Azure resources. -- Using [Virtual Networks](https://learn.microsoft.com/en-us/azure/container-apps/networking?tabs=workload-profiles-env%2Cazure-cli) or [firewall rules](https://learn.microsoft.com/en-us/azure/container-apps/waf-app-gateway) to protect Azure Container Apps from unauthorized access. +- Using [Virtual Networks](https://learn.microsoft.com/en-us/azure/app-service/overview-vnet-integration) or [access restrictions](https://learn.microsoft.com/en-us/azure/app-service/app-service-ip-restrictions) to protect Azure App Service from unauthorized access.
diff --git a/azure.yaml b/azure.yaml index 10cce74b7..04c409462 100644 --- a/azure.yaml +++ b/azure.yaml @@ -7,7 +7,7 @@ environment: name: conversation-knowledge-mining requiredVersions: - azd: ">= 1.18.0" + azd: ">= 1.18.0 != 1.23.9" metadata: template: conversation-knowledge-mining@1.0 diff --git a/azure_custom.yaml b/azure_custom.yaml index a1f28cf04..586b4273a 100644 --- a/azure_custom.yaml +++ b/azure_custom.yaml @@ -1,7 +1,7 @@ name: conversation-knowledge-mining requiredVersions: - azd: ">= 1.18.0" + azd: ">= 1.18.0 != 1.23.9" metadata: template: conversation-knowledge-mining@1.0 diff --git a/docs/workshop/docs/workshop/Challenge-3-and-4/knowledge_mining_api.ipynb b/docs/workshop/docs/workshop/Challenge-3-and-4/knowledge_mining_api.ipynb index aa399eb82..af883d33f 100644 --- a/docs/workshop/docs/workshop/Challenge-3-and-4/knowledge_mining_api.ipynb +++ b/docs/workshop/docs/workshop/Challenge-3-and-4/knowledge_mining_api.ipynb @@ -270,7 +270,7 @@ " },\n", " \"embedding_dependency\": {\n", " \"type\": \"deployment_name\",\n", - " \"deployment_name\": \"text-embedding-ada-002\"\n", + " \"deployment_name\": \"text-embedding-3-small\"\n", " },\n", "\n", " }\n", @@ -394,3 +394,4 @@ "nbformat": 4, "nbformat_minor": 5 } + diff --git a/docs/workshop/docs/workshop/Challenge-5/python/utility.py b/docs/workshop/docs/workshop/Challenge-5/python/utility.py index 39de0dbe1..6ea009e93 100644 --- a/docs/workshop/docs/workshop/Challenge-5/python/utility.py +++ b/docs/workshop/docs/workshop/Challenge-5/python/utility.py @@ -242,7 +242,7 @@ def schema_to_tool(schema: Any): return json.loads( assistant_message.tool_calls[0].function.arguments, strict=False ) - except: + except Exception: return assistant_message.tool_calls[0].function.arguments def get_structured_output_answer( @@ -348,7 +348,6 @@ def generate_scenes( scene_generation_prompt = Template(SCENE_GENERATION_PROMPT).substitute( descriptions=next_segment_content ) - scence_response = VideoSceneResponse(scenes=[]) scence_response = openai_assistant.get_structured_output_answer( "", scene_generation_prompt, VideoSceneResponse ) @@ -433,7 +432,6 @@ def generate_chapters( chapter_generation_prompt = Template(CHAPTER_GENERATION_PROMPT).substitute( descriptions=scene_descriptions ) - chapter_response = VideoChapterResponse(chapters=[]) chapter_response = openai_assistant.get_structured_output_answer( "", chapter_generation_prompt, VideoChapterResponse ) @@ -460,7 +458,6 @@ def aggregate_tags( tags_dedup = set(map(lambda x: re.sub(r'^ ', '', x), tags)) tag_dedup_prompt = Template(DEDUP_PROMPT).substitute(tag_list=tags_dedup) - tag_response = VideoTagResponse(tags=[]) tag_response = openai_assistant.get_structured_output_answer( "", tag_dedup_prompt, VideoTagResponse ) diff --git a/docs/workshop/docs/workshop/support-docs/AzureGPTQuotaSettings.md b/docs/workshop/docs/workshop/support-docs/AzureGPTQuotaSettings.md index 8aaa2d348..503f833ef 100644 --- a/docs/workshop/docs/workshop/support-docs/AzureGPTQuotaSettings.md +++ b/docs/workshop/docs/workshop/support-docs/AzureGPTQuotaSettings.md @@ -12,7 +12,7 @@ - Click on the `Quota` tab. - In the `GlobalStandard` dropdown: - - Select the desired model (e.g., **GPT-4**, **GPT-4o**, **GPT-4o Mini**, or **text-embedding-ada-002**). + - Select the desired model (e.g., **GPT-4**, **GPT-4o**, **GPT-4o Mini**, or **text-embedding-3-small**). - Choose the **region** where your deployment is hosted. - You can: **Request more quota**, or **Delete unused deployments** to free up capacity. diff --git a/docs/workshop/docs/workshop/support-docs/quota_check.md b/docs/workshop/docs/workshop/support-docs/quota_check.md index 9ea7ea6fc..a9a443474 100644 --- a/docs/workshop/docs/workshop/support-docs/quota_check.md +++ b/docs/workshop/docs/workshop/support-docs/quota_check.md @@ -16,7 +16,7 @@ Use one of the following scripts based on your needs: ```sh curl -L -o quota_check_params.sh "https://raw.githubusercontent.com/microsoft/Conversation-Knowledge-Mining-Solution-Accelerator/main/infra/scripts/quota_check_params.sh" chmod +x quota_check_params.sh - ./quota_check_params.sh [] (e.g., gpt-4o-mini:30,text-embedding-ada-002:20 eastus) + ./quota_check_params.sh [] (e.g., gpt-4o-mini:30,text-embedding-3-small:20 eastus) ``` ## **If using VS Code or Codespaces** @@ -26,7 +26,7 @@ Use one of the following scripts based on your needs: **To check quota for a specific model and capacity:** ```sh - ./quota_check_params.sh [] (e.g., gpt-4o-mini:30,text-embedding-ada-002:20 eastus) + ./quota_check_params.sh [] (e.g., gpt-4o-mini:30,text-embedding-3-small:20 eastus) ``` 2. If you see the error `_bash: az: command not found_`, install Azure CLI: @@ -38,5 +38,5 @@ Use one of the following scripts based on your needs: 3. Rerun the script after installing Azure CLI. **Parameters** - - ``: The name and required capacity for each model, in the format model_name:capacity (**e.g., gpt-4o-mini:30,text-embedding-ada-002:20**). + - ``: The name and required capacity for each model, in the format model_name:capacity (**e.g., gpt-4o-mini:30,text-embedding-3-small:20**). - `[] (optional)`: The Azure region to check first. If not provided, all supported regions will be checked (**e.g., eastus**). diff --git a/documents/AVMPostDeploymentGuide.md b/documents/AVMPostDeploymentGuide.md index 805db9130..d34691d66 100644 --- a/documents/AVMPostDeploymentGuide.md +++ b/documents/AVMPostDeploymentGuide.md @@ -35,6 +35,7 @@ Ensure the following tools are installed on your machine: |------|---------|---------------| | PowerShell | v7.0+ | [Install PowerShell](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell?view=powershell-7.5) | | Azure Developer CLI (azd) | v1.18.0+ | [Install azd](https://aka.ms/install-azd) | +| Bicep CLI | v0.33.0+ | [Install Bicep](https://learn.microsoft.com/azure/azure-resource-manager/bicep/install) | | Python | 3.9+ | [Download Python](https://www.python.org/downloads/) | | Docker Desktop | Latest | [Download Docker](https://www.docker.com/products/docker-desktop/) | | Git | Latest | [Download Git](https://git-scm.com/downloads) | @@ -211,7 +212,7 @@ bash ./infra/scripts/process_custom_data.sh \ \ \ \ - \ + \ \ ``` diff --git a/documents/AzureGPTQuotaSettings.md b/documents/AzureGPTQuotaSettings.md index 693791bb8..a4134372b 100644 --- a/documents/AzureGPTQuotaSettings.md +++ b/documents/AzureGPTQuotaSettings.md @@ -5,6 +5,6 @@ 3. **Go to** the `Management Center` from the bottom-left navigation menu. 4. Select `Quota` - Click on the `GlobalStandard` dropdown. - - Select the required **GPT model** (`GPT-4, GPT-4o, GPT-4o Mini`) or **Embeddings model** (`text-embedding-ada-002`). + - Select the required **GPT model** (`GPT-4, GPT-4o, GPT-4o Mini`) or **Embeddings model** (`text-embedding-3-small`). - Choose the **region** where the deployment is hosted. 5. Request More Quota or delete any unused model deployments as needed. diff --git a/documents/CustomizeData.md b/documents/CustomizeData.md index eb6fbf9a6..7be19b224 100644 --- a/documents/CustomizeData.md +++ b/documents/CustomizeData.md @@ -29,7 +29,7 @@ If you would like to update the solution to leverage your own data please follow \ \ \ - \ + \ \ ``` diff --git a/documents/CustomizingAzdParameters.md b/documents/CustomizingAzdParameters.md index 51055490e..d4b456387 100644 --- a/documents/CustomizingAzdParameters.md +++ b/documents/CustomizingAzdParameters.md @@ -11,19 +11,18 @@ By default this template will use the environment name as the prefix to prevent | ----------------------------------------- | ------- | ------------------------ | -------------------------------------------------------------------------- | | `AZURE_LOCATION` | string | ` ` | Sets the Azure region for resource deployment. | | `AZURE_ENV_NAME` | string | `env_name` | Sets the environment name prefix for all Azure resources. | -| `AZURE_CONTENT_UNDERSTANDING_LOCATION` | string | `swedencentral` | Specifies the region for content understanding resources. | -| `AZURE_SECONDARY_LOCATION` | string | `eastus2` | Specifies a secondary Azure region. | -| `AZURE_OPENAI_MODEL_DEPLOYMENT_TYPE` | string | `GlobalStandard` | Defines the model deployment type (allowed: `Standard`, `GlobalStandard`). | -| `AZURE_OPENAI_DEPLOYMENT_MODEL` | string | `gpt-4o-mini` | Specifies the GPT model name (e.g., `gpt-4`, `gpt-4o-mini`). | -| `AZURE_ENV_MODEL_VERSION` | string | `2024-07-18` | Sets the Azure model version (allowed: `2024-08-06`, etc.). | -| `AZURE_OPENAI_API_VERSION` | string | `2025-01-01-preview` | Specifies the API version for Azure OpenAI. | -| `AZURE_OPENAI_DEPLOYMENT_MODEL_CAPACITY` | integer | `30` | Sets the GPT model capacity. | -| `AZURE_OPENAI_EMBEDDING_MODEL` | string | `text-embedding-ada-002` | Sets the name of the embedding model to use. | -| `AZURE_ENV_IMAGETAG` | string | `latest_afv2` | Sets the image tag (`latest_afv2`, `dev`, `hotfix`, etc.). | -| `AZURE_OPENAI_EMBEDDING_MODEL_CAPACITY` | integer | `80` | Sets the capacity for the embedding model deployment. | -| `AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID` | string | Guide to get your [Existing Workspace ID](/documents/re-use-log-analytics.md) | Reuses an existing Log Analytics Workspace instead of creating a new one. | +| `AZURE_ENV_AI_SERVICE_LOCATION` | string | `eastus2` | Specifies the Azure AI service location. | +| `AZURE_ENV_SECONDARY_LOCATION` | string | `eastus2` | Specifies a secondary Azure region. | +| `AZURE_ENV_MODEL_DEPLOYMENT_TYPE` | string | `GlobalStandard` | Defines the model deployment type (allowed: `Standard`, `GlobalStandard`). **Note:** The `azd` location-picker filters regions using the `usageName` metadata on `aiServiceLocation` in `infra/main.bicep` (currently `OpenAI.GlobalStandard.gpt-4o-mini,150`). If you set this to `Standard`, also edit that metadata to `OpenAI.Standard.gpt-4o-mini,150` so the picker shows the correct subset of regions, since `gpt-4o-mini` Standard (regional) availability differs from Global Standard. | +| `AZURE_ENV_GPT_MODEL_NAME` | string | `gpt-4o-mini` | Specifies the GPT model name (e.g., `gpt-4o-mini`, `gpt-4.1`, etc.). | +| `AZURE_ENV_GPT_MODEL_VERSION` | string | `2024-07-18` | Sets the Azure model version (e.g., `2024-07-18`, etc.). | +| `AZURE_ENV_GPT_MODEL_CAPACITY` | integer | `30` | Sets the GPT model capacity. | +| `AZURE_ENV_EMBEDDING_MODEL_NAME` | string | `text-embedding-3-small` | Sets the name of the embedding model to use. | +| `AZURE_ENV_IMAGE_TAG` | string | `latest_afv2` | Sets the image tag (`latest_afv2`, `dev`, `hotfix`, etc.). | +| `AZURE_ENV_EMBEDDING_DEPLOYMENT_CAPACITY` | integer | `80` | Sets the capacity for the embedding model deployment. | +| `AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID` | string | Guide to get your [Existing Workspace ID](/documents/re-use-log-analytics.md) | Reuses an existing Log Analytics Workspace instead of creating a new one. | | `USE_LOCAL_BUILD` | string | `false` | Indicates whether to use a local container build for deployment. | -| `AZURE_EXISTING_AI_PROJECT_RESOURCE_ID` | string | `` | Reuses an existing AIFoundry and AIFoundryProject instead of creating a new one. | +| `AZURE_EXISTING_AIPROJECT_RESOURCE_ID` | string | `` | Reuses an existing AIFoundry and AIFoundryProject instead of creating a new one. | | `AZURE_ENV_VM_ADMIN_USERNAME` | string | `take(newGuid(), 20)` | The administrator username for the virtual machine. | | `AZURE_ENV_VM_ADMIN_PASSWORD` | string | `newGuid()` | The administrator password for the virtual machine. | | `AZURE_ENV_VM_SIZE` | string | `Standard_D2s_v5` | The size/SKU of the Jumpbox Virtual Machine (e.g., `Standard_D2s_v5`, `Standard_DS2_v2`). | diff --git a/documents/DeploymentGuide.md b/documents/DeploymentGuide.md index f30012830..7bb0a03ef 100644 --- a/documents/DeploymentGuide.md +++ b/documents/DeploymentGuide.md @@ -6,6 +6,8 @@ This guide walks you through deploying the Conversation Knowledge Mining Solutio 🆘 **Need Help?** If you encounter any issues during deployment, check our [Troubleshooting Guide](./TroubleShootingSteps.md) for solutions to common problems. +> **Note**: Some tenants may have additional security restrictions that run periodically and could impact the application (e.g., blocking public network access). If you experience issues or the application stops working, check if these restrictions are the cause. In such cases, consider deploying the WAF-supported version to ensure compliance. To configure, [Click here](#31-choose-deployment-type-optional). + ## Step 1: Prerequisites & Setup ### 1.1 Azure Account Requirements @@ -16,7 +18,7 @@ Ensure you have access to an [Azure subscription](https://azure.microsoft.com/fr |------------------------------|-----------|-------------| | **Contributor** | Subscription level | Create and manage Azure resources | | **User Access Administrator** | Subscription level | Manage user access and role assignments | -| **Role Based Access Control** | Subscription/Resource Group level | Configure RBAC permissions | +| **Role Based Access Control Admin** | Subscription/Resource Group level | Configure RBAC permissions | | **Application Developer** | Tenant | Create app registrations for authentication | **🔍 How to Check Your Permissions:** @@ -51,7 +53,7 @@ Ensure you have access to an [Azure subscription](https://azure.microsoft.com/fr - [Foundry IQ](https://learn.microsoft.com/en-us/azure/search/search-what-is-azure-search) - [Azure SQL Database](https://learn.microsoft.com/en-us/azure/azure-sql/database/sql-database-paas-overview) - [Azure Cosmos DB](https://learn.microsoft.com/en-us/azure/cosmos-db/introduction) -- [Azure Container Apps](https://learn.microsoft.com/en-us/azure/container-apps/) +- [Azure App Service](https://learn.microsoft.com/en-us/azure/app-service/overview) - [Azure Container Registry](https://learn.microsoft.com/en-us/azure/container-registry/) - [Embedding Deployment Capacity](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models#embedding-models) - [Azure Semantic Search](./AzureSemanticSearchRegion.md) @@ -160,6 +162,7 @@ Select one of the following options to deploy the Conversational Knowledge Minin **Required Tools:** - [PowerShell 7.0+](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell) - [Azure Developer CLI (azd) 1.18.0+](https://aka.ms/install-azd) +- [Bicep CLI 0.33.0+](https://learn.microsoft.com/azure/azure-resource-manager/bicep/install) - [Python 3.9+](https://www.python.org/downloads/) - [Docker Desktop](https://www.docker.com/products/docker-desktop/) - [Git](https://git-scm.com/downloads) @@ -392,7 +395,7 @@ bash ./infra/scripts/process_sample_data.sh \ \ \ \ - \ + \ \ ``` @@ -402,9 +405,9 @@ bash ./infra/scripts/process_sample_data.sh \ - **Storage Parameters:** Storage account name and container name - **SQL Parameters:** SQL server name, database name, backend user managed identity client ID and display name - **Search Parameters:** AI Search service name and endpoint -- **AI Foundry Parameters:** AI Foundry resource ID and Content Understanding Foundry resource ID +- **AI Foundry Parameters:** AI Foundry resource ID - **OpenAI Parameters:** OpenAI endpoint, embedding model name, and deployment model name -- **Content Understanding Parameters:** CU endpoint, AI agent endpoint, CU API version +- **Content Understanding Parameters:** CU endpoint, CU API version, AI agent endpoint - **Use Case:** Either `telecom` or `IT_helpdesk` - **Solution Parameters:** Solution deployment name diff --git a/documents/Images/AddDetails.png b/documents/Images/AddDetails.png index f36b596f2..f5946c6db 100644 Binary files a/documents/Images/AddDetails.png and b/documents/Images/AddDetails.png differ diff --git a/documents/Images/AddPlatform.png b/documents/Images/AddPlatform.png index 6c74919b4..2424f2a8f 100644 Binary files a/documents/Images/AddPlatform.png and b/documents/Images/AddPlatform.png differ diff --git a/documents/Images/ReadMe/solution-architecture.png b/documents/Images/ReadMe/solution-architecture.png index ce14d8476..ef2dc116c 100644 Binary files a/documents/Images/ReadMe/solution-architecture.png and b/documents/Images/ReadMe/solution-architecture.png differ diff --git a/documents/Images/Web.png b/documents/Images/Web.png index 35f846453..d997cbd3a 100644 Binary files a/documents/Images/Web.png and b/documents/Images/Web.png differ diff --git a/documents/LocalDevelopmentSetup.md b/documents/LocalDevelopmentSetup.md index 344021d12..e2f2480c1 100644 --- a/documents/LocalDevelopmentSetup.md +++ b/documents/LocalDevelopmentSetup.md @@ -293,6 +293,46 @@ Create `.vscode/settings.json` and copy the following JSON: --- +### Running with Automated Script + +For convenience, you can use the provided startup scripts that handle environment setup and start both backend and frontend services automatically. This is the quickest way to get up and running locally. + +> **Note**: You must complete **Step 1 (Prerequisites)** and **Step 2 (Development Tools Setup)** before using the automated scripts. + +#### Windows (Command Prompt or PowerShell): + +```cmd +cd src +.\start.cmd +``` + +#### macOS/Linux/WSL: + +```bash +cd src +chmod +x start.sh +./start.sh +``` + +### What the Scripts Do + +The startup scripts automatically handle: +- Environment variable configuration +- Azure authentication +- Azure RBAC role assignments (Cosmos DB, SQL Server, AI Foundry, AI Search) +- Python virtual environment setup +- Backend dependency installation +- Frontend dependency installation +- Starting both backend and frontend servers + +> **Note**: The script includes a 30-second wait for the backend to initialize before starting the frontend. If you see connection errors initially, wait a moment and reload the page. + +--- + +## Running Backend and Frontend Manually + +If you prefer more control over the setup process, follow the steps below to configure and run each service individually. + ## Step 3: Azure Authentication Setup Before configuring services, authenticate with Azure: @@ -385,13 +425,17 @@ az role assignment create \ ``` #### Cosmos DB Access - ```bash +# Get your principal ID +PRINCIPAL_ID=$(az ad signed-in-user show --query id -o tsv) + # Assign Cosmos DB Built-in Data Contributor role -az role assignment create \ - --role "Cosmos DB Built-in Data Contributor" \ - --assignee $PRINCIPAL_ID \ - --scope "/subscriptions//resourceGroups//providers/Microsoft.DocumentDB/databaseAccounts/" +az cosmosdb sql role assignment create \ + --resource-group \ + --account-name \ + --role-definition-name "Cosmos DB Built-in Data Contributor" \ + --principal-id $PRINCIPAL_ID \ + --scope /subscriptions//resourceGroups//providers/Microsoft.DocumentDB/databaseAccounts/ ``` #### Azure Storage Access @@ -528,7 +572,7 @@ AZURE_AI_PROJECT_CONN_STRING= AZURE_AI_AGENT_API_VERSION=2024-11-01-preview AZURE_AI_PROJECT_NAME= AZURE_AI_FOUNDRY_NAME= -AZURE_EXISTING_AI_PROJECT_RESOURCE_ID= +AZURE_EXISTING_AIPROJECT_RESOURCE_ID= AZURE_AI_AGENT_ENDPOINT= AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME= @@ -544,14 +588,6 @@ AZURE_AI_SEARCH_INDEX=call_transcripts_index AZURE_AI_SEARCH_CONNECTION_NAME= AZURE_AI_SEARCH_NAME= -# Azure OpenAI Configuration -AZURE_OPENAI_DEPLOYMENT_MODEL= -AZURE_OPENAI_ENDPOINT= -AZURE_OPENAI_MODEL_DEPLOYMENT_TYPE= -AZURE_OPENAI_EMBEDDING_MODEL= -AZURE_OPENAI_API_VERSION=2024-08-01-preview -AZURE_OPENAI_RESOURCE= - # Cosmos DB Configuration AZURE_COSMOSDB_ACCOUNT= AZURE_COSMOSDB_CONVERSATIONS_CONTAINER=conversations diff --git a/documents/QuotaCheck.md b/documents/QuotaCheck.md index 1769e36e8..5cfa305cc 100644 --- a/documents/QuotaCheck.md +++ b/documents/QuotaCheck.md @@ -14,11 +14,11 @@ az login --use-device-code ### 📌 Default Models & Capacities: ``` -gpt-4o:150, gpt-4o-mini:150, gpt-4:150, text-embedding-ada-002:80 +gpt-4o:150, gpt-4o-mini:150, gpt-4:150, text-embedding-3-small:80 ``` ### 📌 Default Regions: ``` -eastus, uksouth, eastus2, northcentralus, swedencentral, westus, westus2, southcentralus, canadacentral +eastus, eastus2, australiaeast, uksouth, swedencentral, westus, westus3, japaneast, southcentralus, westeurope ``` ### Usage Scenarios: - No parameters passed → Default models and capacities will be checked in default regions. @@ -40,7 +40,7 @@ eastus, uksouth, eastus2, northcentralus, swedencentral, westus, westus2, southc ``` ✔️ Check specific model(s) in default regions: ``` - ./quota_check_params.sh --models gpt-4o:150,text-embedding-ada-002:80 + ./quota_check_params.sh --models gpt-4o:150,text-embedding-3-small:80 ``` ✔️ Check default models in specific region(s): ``` @@ -52,7 +52,7 @@ eastus, uksouth, eastus2, northcentralus, swedencentral, westus, westus2, southc ``` ✔️ All parameters combined: ``` - ./quota_check_params.sh --models gpt-4:150,text-embedding-ada-002:80 --regions eastus,westus --verbose + ./quota_check_params.sh --models gpt-4:150,text-embedding-3-small:80 --regions eastus,westus --verbose ``` ### **Sample Output** diff --git a/documents/TroubleShootingSteps.md b/documents/TroubleShootingSteps.md index 77395d970..61edb4013 100644 --- a/documents/TroubleShootingSteps.md +++ b/documents/TroubleShootingSteps.md @@ -60,7 +60,6 @@ Use these as quick reference guides to unblock your deployments. | **InternalSubscriptionIsOverQuotaForSku/
ManagedEnvironmentProvisioningError** | Subscription quota exceeded for the requested SKU | Quotas are applied per resource group, subscriptions, accounts, and other scopes. For example, your subscription might be configured to limit the number of vCPUs for a region. If you attempt to deploy a virtual machine with more vCPUs than the permitted amount, you receive an error that the quota was exceeded.

For PowerShell, use the `Get-AzVMUsage` cmdlet to find virtual machine quotas:
`Get-AzVMUsage -Location "West US"`

Based on available quota you can deploy application otherwise, you can request for more quota | | **ServiceQuotaExceeded** | Free tier service quota limit reached for Azure AI Search | This error occurs when you attempt to deploy an Azure AI Search service but have already reached the **free tier quota limit** for your subscription. Each Azure subscription is limited to **one free tier Search service**.

**Example error message:**
`ServiceQuotaExceeded: Operation would exceed 'free' tier service quota. You are using 1 out of 1 'free' tier service quota.`

**Common causes:**
  • Already have a free tier Azure AI Search service in the subscription
  • Previous deployment created a free tier Search service that wasn't deleted
  • Attempting to deploy multiple environments with free tier Search services

**Resolution:**
  • **Option 1: Delete existing free tier Search service:**
    `az search service list --query "[?sku.name=='free']" -o table`
    `az search service delete --name --resource-group --yes`
  • **Option 2: Upgrade to a paid SKU:**
    Modify your Bicep/ARM template to use `basic`, `standard`, or higher SKU instead of `free`
  • **Option 3: Use existing Search service:**
    Reference the existing free tier Search service in your deployment instead of creating a new one
  • **Request quota increase:**
    Submit a support request with issue type 'Service and subscription limits (quota)' and quota type 'Search' via [Azure Quota Request](https://aka.ms/AddQuotaSubscription)

**Reference:**
  • [Azure AI Search service limits](https://learn.microsoft.com/en-us/azure/search/search-limits-quotas-capacity)
  • [Azure AI Search pricing tiers](https://learn.microsoft.com/en-us/azure/search/search-sku-tier)
| | **InsufficientQuota** | Not enough quota available in subscription |
  • Check if you have sufficient quota available in your subscription before deployment
  • To verify, refer to the [quota_check](../documents/QuotaCheck.md) file for details
| -| **MaxNumberOfRegionalEnvironmentsInSubExceeded** | Maximum Container App Environments limit reached for region |This error occurs when you attempt to create more **Azure Container App Environments** than the regional quota limit allows for your subscription. Each Azure region has a specific limit on the number of Container App Environments that can be created per subscription.

**Common Causes:**
  • Deploying to regions with low quota limits (e.g., Sweden Central allows only 1 environment)
  • Multiple deployments without cleaning up previous environments
  • Exceeding the standard limit of 15 environments in most major regions

**Resolution:**
  • **Delete unused environments** in the target region, OR
  • **Deploy to a different region** with available capacity, OR
  • **Request quota increase** via [Azure Support](https://go.microsoft.com/fwlink/?linkid=2208872)

**Reference:**
  • [Azure Container Apps quotas](https://learn.microsoft.com/en-us/azure/container-apps/quotas)
  • [Azure subscription and service limits](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits)
| | **SkuNotAvailable** | Requested SKU not available in selected location or zone | This error occurs when the resource SKU you've selected (such as VM size) isn't available for the target location or availability zone.

**In this deployment**, the jumpbox VM defaults to `Standard_D2s_v5`. While this size is available in most regions, certain regions or zones may not support it.

**Resolution:**
  • **Check SKU availability** for your target region:
    `az vm list-skus --location --size Standard_D2s --output table`
  • **Override the VM size** if the default isn't available in your region:
    `azd env set AZURE_ENV_VM_SIZE Standard_D2s_v4`
  • **Recommended alternatives** (all support accelerated networking + Premium SSD):
    - `Standard_D2s_v4` — previous gen, identical pricing
    - `Standard_D2as_v5` — AMD-based, similar pricing
    - `Standard_D2s_v3` — older gen, widely available
  • **Avoid A-series VMs** (e.g., `Standard_A2m_v2`) — they do not support accelerated networking or Premium SSD, which are required by this deployment

**Reference:**
  • [Resolve errors for SKU not available](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/error-sku-not-available)
  • [Azure VM sizes - Dsv5 series](https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/general-purpose/dsv5-series)
| | **Conflict - No available instances to satisfy this request** | Azure App Service has insufficient capacity in the region | This error occurs when Azure App Service doesn't have enough available compute instances in the selected region to provision or scale your app.

**Common Causes:**
  • High demand in the selected region (e.g., East US, West Europe)
  • Specific SKUs experiencing capacity constraints (Free, Shared, or certain Premium tiers)
  • Multiple rapid deployments in the same region

**Resolution:**
  • **Wait and Retry** (15-30 minutes): `azd up`
  • **Deploy to a New Resource Group** (Recommended for urgent cases):
    ```
    azd down --force --purge
    azd up
    ```
  • **Try a Different Region:**
    Update region in `main.bicep` or `azure.yaml` to a less congested region (e.g., `westus2`, `centralus`, `northeurope`)
  • **Use a Different SKU/Tier:**
    If using Free/Shared tier, upgrade to Basic or Standard
    Check SKU availability: `az appservice list-locations --sku `

**Reference:** [Azure App Service Plans](https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans) | @@ -147,7 +146,6 @@ Use these as quick reference guides to unblock your deployments. | **AccountProvisioningStateInvalid** | Resource used before provisioning completed |
  • The AccountProvisioningStateInvalid error occurs when you try to use resources while they are still in the Accepted provisioning state
  • This means the deployment has not yet fully completed
  • To avoid this error, wait until the provisioning state changes to Succeeded
  • Only use the resources once the deployment is fully completed
| | **BadRequest - DatabaseAccount is in a failed provisioning state because the previous attempt to create it was not successful** | Database account failed to provision previously |
  • This error occurs when a user attempts to redeploy a resource that previously failed to provision
  • To resolve the issue, delete the failed deployment first, then start a new deployment
  • For guidance on deleting a resource from a Resource Group, refer to the following link: [Delete an Azure Cosmos DB account](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/manage-with-powershell#delete-account:~:text=%3A%24enableMultiMaster-,Delete%20an%20Azure%20Cosmos%20DB%20account,-This%20command%20deletes)
| | **ServiceDeleting** | Cannot provision service because deletion is still in progress | This error occurs when you attempt to create an Azure Search service with the same name as one that is currently being deleted. Azure Search services have a **soft-delete period** during which the service name remains reserved.

**Common causes:**
  • Deleting a Search service and immediately trying to recreate it with the same name
  • Rapid redeployments using the same service name in Bicep/ARM templates
  • The deletion operation is asynchronous and takes several minutes to complete

**Resolution:**
  • **Wait for deletion to complete** (10-15 minutes) before redeploying
  • **Use a different service name** - append timestamp or unique identifier to the name
  • **Implement retry logic** with exponential backoff as suggested in the error message
  • **Check deletion status** before recreating:
    `az search service show --name --resource-group `
  • For Bicep deployments, ensure your naming strategy includes unique suffixes to avoid conflicts
  • For more details, refer to [Azure Search service limits](https://learn.microsoft.com/en-us/azure/search/search-limits-quotas-capacity)
| -| **FailedIdentityOperation / ManagedEnvironmentScheduledForDelete** | Identity operation failed due to pending delete or resource conflict | This error occurs when you attempt to create or update an Azure Container Apps Managed Environment while it has a **pending delete operation** or the resource already exists in a conflicting state.

**Example error messages:**
`FailedIdentityOperation: Identity operation for resource failed with error 'Failed to perform resource identity operation. Status: 'Conflict'. Response: 'Request specified that resource is new, but resource already exists. This may be due to a pending delete operation, try again later.'`

`ManagedEnvironmentScheduledForDelete: The environment 'cae-xxx' is under deletion. Please retry the creation with new name or wait for the deletion completed.`

**Common causes:**
  • Deleting a Container Apps Environment and immediately trying to recreate it with the same name
  • Rapid redeployments using `azd up` without waiting for previous cleanup
  • Resource group deletion in progress while attempting to redeploy
  • Previous deployment failed or was canceled, leaving resources in an inconsistent state
  • Concurrent deployments targeting the same resources

**Resolution:**
  • **Wait for deletion to complete** (5-15 minutes) before redeploying:
    `az containerapp env show --name --resource-group --query "properties.provisioningState"`
  • **Check environment status:** If status is `ScheduledForDelete` or `Deleting`, wait for it to complete
  • **Use a new environment name:** Create a new environment with a different name or use a new resource group:
    `azd env new `
    `azd up`
  • **Force delete and wait:** If the environment is stuck, try force deletion:
    `az containerapp env delete --name --resource-group --yes`
    Wait for deletion to complete before redeploying
  • **Delete associated Container Apps first:** If the environment has apps, delete them before the environment:
    `az containerapp list --environment --resource-group -o table`
    `az containerapp delete --name --resource-group --yes`
  • **Use unique naming:** Implement timestamp or unique suffix in your naming strategy to avoid conflicts

**Reference:**
  • [Azure Container Apps troubleshooting](https://learn.microsoft.com/en-us/azure/container-apps/troubleshooting)
  • [Manage Container Apps environments](https://learn.microsoft.com/en-us/azure/container-apps/environment)
| | **BadRequest - Parent account does not provision correctly** | Parent AI Services/Cognitive Services account failed to provision | This error occurs when a **child resource** (such as an AI project, model deployment, or other dependent resource) attempts to be created on a **parent Cognitive Services/AI Services account** that has **failed to provision** or is in an incomplete state.

**Example error message:**
`Parent account does not provision correctly, please retry creating the account.`

**Common causes:**
  • Parent AI Services account provisioning failed due to quota, region, or configuration issues
  • Using `restore: true` flag when no soft-deleted resource exists to restore
  • Network or transient errors during parent account creation
  • Invalid configuration on the parent account (e.g., invalid SKU, unsupported region)
  • Previous deployment of the parent account was interrupted or canceled

**Resolution:**
  • **Check parent account status:**
    `az cognitiveservices account show --name --resource-group --query "properties.provisioningState"`
  • **Delete failed parent account and redeploy:**
    `az cognitiveservices account delete --name --resource-group `
    Then run: `azd up`
  • **If using restore flag incorrectly:** Ensure `restore: false` in your Bicep template unless you specifically need to restore a soft-deleted resource
  • **Check for soft-deleted resources:**
    `az cognitiveservices account list-deleted`
  • **Purge soft-deleted resources if needed:**
    `az cognitiveservices account purge --name --resource-group --location `
  • **Verify quota and region availability:** Ensure you have sufficient quota and the service is available in your selected region

**Reference:**
  • [Manage Cognitive Services accounts](https://learn.microsoft.com/en-us/azure/ai-services/manage-resources)
  • [Recover deleted Cognitive Services resources](https://learn.microsoft.com/en-us/azure/ai-services/recover-purge-resources)
| --------------------------------- @@ -157,9 +155,8 @@ Use these as quick reference guides to unblock your deployments. |-------------|-------------|------------------| | **DeploymentModelNotSupported/
ServiceModelDeprecated/
InvalidResourceProperties** | Model not supported or deprecated in selected region |
  • The updated model may not be supported in the selected region. Please verify its availability in the [Azure AI Foundry models](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/concepts/models?tabs=global-standard%2Cstandard-chat-completions) document
| | **FlagMustBeSetForRestore/
NameUnavailable/
CustomDomainInUse** | Soft-deleted resource requires restore flag or purge | This error occurs when you try to deploy a Cognitive Services resource that was **soft-deleted** earlier. Azure requires you to explicitly set the **`restore` flag** to `true` if you want to recover the soft-deleted resource. If you don't want to restore the resource, you must **purge the deleted resource** first before redeploying.

**Example causes:**
  • Trying to redeploy a Cognitive Services account with the same name as a previously deleted one
  • The deleted resource still exists in a **soft-delete retention state**

**How to fix:**
  • If you want to restore → add `"restore": true` in your template properties
  • If you want a fresh deployment → purge the resource using:
    `az cognitiveservices account purge --name --resource-group --location `
  • For more details, refer to [Soft delete and resource restore](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/delete-resource-group?tabs=azure-powershell)
| -| **ContainerAppOperationError** | Container image build or deployment issue |
  • The error is likely due to an improperly built container image. For resolution steps, refer to the [Azure Container Registry (ACR) – Build & Push Guide](./ACRBuildAndPushGuide.md)
| | **LinkedAuthorizationFailed** | Service principal lacks permission to use a linked resource required for deployment | This error occurs when a service principal doesn't have permission to perform an action on a linked resource that is required for the operation (e.g., cluster creation).

**Common causes:**
  • The service principal has permission on the primary resource but lacks permission on the linked scope
  • Missing role assignment for operations like `Microsoft.Network/ddosProtectionPlans/join/action`

**Resolution:**
  • Identify the **service principal**, **resource**, and **operation** from the error message
  • Grant the service principal the required permissions on the linked resource
  • Use [Assign Azure roles using the Azure portal](https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal) to add the role assignment
  • For more details, refer to [LinkedAuthorizationFailed error](https://learn.microsoft.com/en-us/troubleshoot/azure/azure-kubernetes/error-codes/linkedauthorizationfailed-error)
| -| **ContainerOperationFailure** | Container image or storage resource does not exist | This error occurs when an operation fails because the **specified container resource does not exist**. This can happen with Azure Container Registry images or Azure Storage blob containers.

**Example error message:**
`ContainerOperationFailure: The specified resource does not exist. RequestId:xxxxx Time:xxxxx`

**Common causes:**
  • **Invalid container image tag:** The specified image tag does not exist in the container registry
  • **Non-existent container registry:** The container registry endpoint is incorrect or inaccessible
  • **Missing blob container:** The storage blob container referenced by the application does not exist
  • **Incorrect storage account URL:** The storage account endpoint is misconfigured
  • **Permission issues:** The managed identity lacks permissions to access the container registry or storage account

**Resolution:**
  • **Verify container image exists:**
    `az acr repository show-tags --name --repository `
  • **Check image tag in deployment:** Ensure the `imageTag` parameter matches an existing tag in the registry
  • **Verify storage containers exist:**
    `az storage container list --account-name --auth-mode login`
  • **Check role assignments:** Ensure the Container App's managed identity has `AcrPull` role on the container registry and `Storage Blob Data Contributor` role on the storage account
  • **Rebuild and push container images:** If images are missing, follow the [ACR Build & Push Guide](./ACRBuildAndPushGuide.md)
  • **Verify storage account URL:** Ensure `APP_STORAGE_BLOB_URL` and `APP_STORAGE_QUEUE_URL` in App Configuration point to the correct storage account

**Reference:**
  • [Azure Container Registry troubleshooting](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-troubleshoot-login)
  • [Azure Storage troubleshooting](https://learn.microsoft.com/en-us/azure/storage/common/storage-troubleshoot-common-errors)
| +| **ContainerOperationFailure** | Container image or storage resource does not exist | This error occurs when an operation fails because the **specified container resource does not exist**. This can happen with Azure Container Registry images or Azure Storage blob containers.

**Example error message:**
`ContainerOperationFailure: The specified resource does not exist. RequestId:xxxxx Time:xxxxx`

**Common causes:**
  • **Invalid container image tag:** The specified image tag does not exist in the container registry
  • **Non-existent container registry:** The container registry endpoint is incorrect or inaccessible
  • **Missing blob container:** The storage blob container referenced by the application does not exist
  • **Incorrect storage account URL:** The storage account endpoint is misconfigured
  • **Permission issues:** The managed identity lacks permissions to access the container registry or storage account

**Resolution:**
  • **Verify container image exists:**
    `az acr repository show-tags --name --repository `
  • **Check image tag in deployment:** Ensure the `imageTag` parameter matches an existing tag in the registry
  • **Verify storage containers exist:**
    `az storage container list --account-name --auth-mode login`
  • **Check role assignments:** Ensure the App Service's managed identity has `AcrPull` role on the container registry and `Storage Blob Data Contributor` role on the storage account
  • **Rebuild and push container images:** If images are missing, follow the [ACR Build & Push Guide](./ACRBuildAndPushGuide.md)
  • **Verify storage account URL:** Ensure `APP_STORAGE_BLOB_URL` and `APP_STORAGE_QUEUE_URL` in App Configuration point to the correct storage account

**Reference:**
  • [Azure Container Registry troubleshooting](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-troubleshoot-login)
  • [Azure Storage troubleshooting](https://learn.microsoft.com/en-us/azure/storage/common/storage-troubleshoot-common-errors)
| --------------------------------- diff --git a/documents/create_new_app_registration.md b/documents/create_new_app_registration.md index f6747eb58..1bd55bf8b 100644 --- a/documents/create_new_app_registration.md +++ b/documents/create_new_app_registration.md @@ -20,7 +20,7 @@ ![Redirect URL](Images/AddRedirectURL.png) -6. Click on `+ Add a platform`. +6. Click on `+ Add redirect URI`. ![+ Add platform](Images/AddPlatform.png) diff --git a/documents/re-use-foundry-project.md b/documents/re-use-foundry-project.md index 785f29178..a85fb91a0 100644 --- a/documents/re-use-foundry-project.md +++ b/documents/re-use-foundry-project.md @@ -2,6 +2,13 @@ # Reusing an Existing Microsoft Foundry Project To configure your environment to use an existing Microsoft Foundry Project, follow these steps: + +> **⚠️ Region requirement** +> +> The existing Foundry project must reside in a region that supports **both** the GPT model deployed by this accelerator (default `gpt-4o-mini` with `GlobalStandard` deployment type) **and** Azure AI Content Understanding (GA).
+> Supported regions: `australiaeast`, `eastus`, `eastus2`, `japaneast`, `southcentralus`, `swedencentral`, `uksouth`, `westeurope`, `westus`, `westus3`.
+> If the existing project is in a different region, deployment will fail or the application will not work correctly. + --- ### 1. Go to Azure Portal Go to https://portal.azure.com @@ -36,7 +43,7 @@ In the left-hand menu of the project blade: ### 6. Set the Foundry Project Resource ID in Your Environment Run the following command in your terminal ```bash -azd env set AZURE_EXISTING_AI_PROJECT_RESOURCE_ID '' +azd env set AZURE_EXISTING_AIPROJECT_RESOURCE_ID '' ``` Replace `` with the value obtained from Step 5. diff --git a/documents/re-use-log-analytics.md b/documents/re-use-log-analytics.md index be1a42a0d..043435f6b 100644 --- a/documents/re-use-log-analytics.md +++ b/documents/re-use-log-analytics.md @@ -23,7 +23,7 @@ Copy Resource ID that is your Workspace ID ### 4. Set the Workspace ID in Your Environment Run the following command in your terminal ```bash -azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID '' +azd env set AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID '' ``` Replace `` with the value obtained from Step 3. diff --git a/infra/abbreviations.json b/infra/abbreviations.json deleted file mode 100644 index 0371b1753..000000000 --- a/infra/abbreviations.json +++ /dev/null @@ -1,229 +0,0 @@ -{ - "ai": { - "aiSearch": "srch-", - "aiServices": "aisa-", - "aiVideoIndexer": "avi-", - "machineLearningWorkspace": "mlw-", - "openAIService": "oai-", - "botService": "bot-", - "computerVision": "cv-", - "contentModerator": "cm-", - "contentSafety": "cs-", - "customVisionPrediction": "cstv-", - "customVisionTraining": "cstvt-", - "documentIntelligence": "di-", - "faceApi": "face-", - "healthInsights": "hi-", - "immersiveReader": "ir-", - "languageService": "lang-", - "speechService": "spch-", - "translator": "trsl-", - "aiHub": "aih-", - "aiHubProject": "aihp-", - "aiFoundry": "aif-", - "aiFoundryProject": "aifp-" - }, - "analytics": { - "analysisServicesServer": "as", - "databricksWorkspace": "dbw-", - "dataExplorerCluster": "dec", - "dataExplorerClusterDatabase": "dedb", - "dataFactory": "adf-", - "digitalTwin": "dt-", - "streamAnalytics": "asa-", - "synapseAnalyticsPrivateLinkHub": "synplh-", - "synapseAnalyticsSQLDedicatedPool": "syndp", - "synapseAnalyticsSparkPool": "synsp", - "synapseAnalyticsWorkspaces": "synw", - "dataLakeStoreAccount": "dls", - "dataLakeAnalyticsAccount": "dla", - "eventHubsNamespace": "evhns-", - "eventHub": "evh-", - "eventGridDomain": "evgd-", - "eventGridSubscriptions": "evgs-", - "eventGridTopic": "evgt-", - "eventGridSystemTopic": "egst-", - "hdInsightHadoopCluster": "hadoop-", - "hdInsightHBaseCluster": "hbase-", - "hdInsightKafkaCluster": "kafka-", - "hdInsightSparkCluster": "spark-", - "hdInsightStormCluster": "storm-", - "hdInsightMLServicesCluster": "mls-", - "iotHub": "iot-", - "provisioningServices": "provs-", - "provisioningServicesCertificate": "pcert-", - "powerBIEmbedded": "pbi-", - "timeSeriesInsightsEnvironment": "tsi-" - }, - "compute": { - "appServiceEnvironment": "ase-", - "appServicePlan": "asp-", - "loadTesting": "lt-", - "availabilitySet": "avail-", - "arcEnabledServer": "arcs-", - "arcEnabledKubernetesCluster": "arck", - "batchAccounts": "ba-", - "cloudService": "cld-", - "communicationServices": "acs-", - "diskEncryptionSet": "des", - "functionApp": "func-", - "gallery": "gal", - "hostingEnvironment": "host-", - "imageTemplate": "it-", - "managedDiskOS": "osdisk", - "managedDiskData": "disk", - "notificationHubs": "ntf-", - "notificationHubsNamespace": "ntfns-", - "proximityPlacementGroup": "ppg-", - "restorePointCollection": "rpc-", - "snapshot": "snap-", - "staticWebApp": "stapp-", - "virtualMachine": "vm", - "virtualMachineScaleSet": "vmss-", - "virtualMachineMaintenanceConfiguration": "mc-", - "virtualMachineStorageAccount": "stvm", - "webApp": "app-" - }, - "containers": { - "aksCluster": "aks-", - "aksSystemNodePool": "npsystem-", - "aksUserNodePool": "np-", - "containerApp": "ca-", - "containerAppsEnvironment": "cae-", - "containerRegistry": "cr", - "containerInstance": "ci", - "serviceFabricCluster": "sf-", - "serviceFabricManagedCluster": "sfmc-" - }, - "databases": { - "cosmosDBDatabase": "cosmos-", - "cosmosDBApacheCassandra": "coscas-", - "cosmosDBMongoDB": "cosmon-", - "cosmosDBNoSQL": "cosno-", - "cosmosDBTable": "costab-", - "cosmosDBGremlin": "cosgrm-", - "cosmosDBPostgreSQL": "cospos-", - "cacheForRedis": "redis-", - "sqlDatabaseServer": "sql-", - "sqlDatabase": "sqldb-", - "sqlElasticJobAgent": "sqlja-", - "sqlElasticPool": "sqlep-", - "mariaDBServer": "maria-", - "mariaDBDatabase": "mariadb-", - "mySQLDatabase": "mysql-", - "postgreSQLDatabase": "psql-", - "sqlServerStretchDatabase": "sqlstrdb-", - "sqlManagedInstance": "sqlmi-" - }, - "developerTools": { - "appConfigurationStore": "appcs-", - "mapsAccount": "map-", - "signalR": "sigr", - "webPubSub": "wps-" - }, - "devOps": { - "managedGrafana": "amg-" - }, - "integration": { - "apiManagementService": "apim-", - "integrationAccount": "ia-", - "logicApp": "logic-", - "serviceBusNamespace": "sbns-", - "serviceBusQueue": "sbq-", - "serviceBusTopic": "sbt-", - "serviceBusTopicSubscription": "sbts-" - }, - "managementGovernance": { - "automationAccount": "aa-", - "applicationInsights": "appi-", - "monitorActionGroup": "ag-", - "monitorDataCollectionRules": "dcr-", - "monitorAlertProcessingRule": "apr-", - "blueprint": "bp-", - "blueprintAssignment": "bpa-", - "dataCollectionEndpoint": "dce-", - "logAnalyticsWorkspace": "log-", - "logAnalyticsQueryPacks": "pack-", - "managementGroup": "mg-", - "purviewInstance": "pview-", - "resourceGroup": "rg-", - "templateSpecsName": "ts-" - }, - "migration": { - "migrateProject": "migr-", - "databaseMigrationService": "dms-", - "recoveryServicesVault": "rsv-" - }, - "networking": { - "applicationGateway": "agw-", - "applicationSecurityGroup": "asg-", - "cdnProfile": "cdnp-", - "cdnEndpoint": "cdne-", - "connections": "con-", - "dnsForwardingRuleset": "dnsfrs-", - "dnsPrivateResolver": "dnspr-", - "dnsPrivateResolverInboundEndpoint": "in-", - "dnsPrivateResolverOutboundEndpoint": "out-", - "firewall": "afw-", - "firewallPolicy": "afwp-", - "expressRouteCircuit": "erc-", - "expressRouteGateway": "ergw-", - "frontDoorProfile": "afd-", - "frontDoorEndpoint": "fde-", - "frontDoorFirewallPolicy": "fdfp-", - "ipGroups": "ipg-", - "loadBalancerInternal": "lbi-", - "loadBalancerExternal": "lbe-", - "loadBalancerRule": "rule-", - "localNetworkGateway": "lgw-", - "natGateway": "ng-", - "networkInterface": "nic-", - "networkSecurityGroup": "nsg-", - "networkSecurityGroupSecurityRules": "nsgsr-", - "networkWatcher": "nw-", - "privateLink": "pl-", - "privateEndpoint": "pep-", - "publicIPAddress": "pip-", - "publicIPAddressPrefix": "ippre-", - "routeFilter": "rf-", - "routeServer": "rtserv-", - "routeTable": "rt-", - "serviceEndpointPolicy": "se-", - "trafficManagerProfile": "traf-", - "userDefinedRoute": "udr-", - "virtualNetwork": "vnet-", - "virtualNetworkGateway": "vgw-", - "virtualNetworkManager": "vnm-", - "virtualNetworkPeering": "peer-", - "virtualNetworkSubnet": "snet-", - "virtualWAN": "vwan-", - "virtualWANHub": "vhub-" - }, - "security": { - "bastion": "bas-", - "keyVault": "kv-", - "keyVaultManagedHSM": "kvmhsm-", - "managedIdentity": "id-", - "sshKey": "sshkey-", - "vpnGateway": "vpng-", - "vpnConnection": "vcn-", - "vpnSite": "vst-", - "webApplicationFirewallPolicy": "waf", - "webApplicationFirewallPolicyRuleGroup": "wafrg" - }, - "storage": { - "storSimple": "ssimp", - "backupVault": "bvault-", - "backupVaultPolicy": "bkpol-", - "fileShare": "share-", - "storageAccount": "st", - "storageSyncService": "sss-" - }, - "virtualDesktop": { - "labServicesPlan": "lp-", - "virtualDesktopHostPool": "vdpool-", - "virtualDesktopApplicationGroup": "vdag-", - "virtualDesktopWorkspace": "vdws-", - "virtualDesktopScalingPlan": "vdscaling-" - } -} \ No newline at end of file diff --git a/infra/data/ckm-analyzer_config_audio.json b/infra/data/ckm_analyzer_config_audio.json similarity index 83% rename from infra/data/ckm-analyzer_config_audio.json rename to infra/data/ckm_analyzer_config_audio.json index ef87affa9..42d2c3c26 100644 --- a/infra/data/ckm-analyzer_config_audio.json +++ b/infra/data/ckm_analyzer_config_audio.json @@ -1,19 +1,11 @@ { - "analyzerId": "ckm-analyzer", - "name": "ckm-analyzer", - "scenario": "conversation", + "baseAnalyzerId": "prebuilt-callCenter", "description": "Conversation process", - "tags": { - "projectId": "", - "templateId": "postCallAnalytics-2024-12-01" - }, "config": { "returnDetails": false, "locales": ["en-US"] }, "fieldSchema": { - "name": "CallCenterConversationAnalysis", - "descriptions": "Content, Summary, sentiment, and more analyses from a call center conversation", "fields": { "content": { "type": "string", diff --git a/infra/data/ckm-analyzer_config_text.json b/infra/data/ckm_analyzer_config_json.json similarity index 83% rename from infra/data/ckm-analyzer_config_text.json rename to infra/data/ckm_analyzer_config_json.json index 81ffa0c98..9afba49c5 100644 --- a/infra/data/ckm-analyzer_config_text.json +++ b/infra/data/ckm_analyzer_config_json.json @@ -1,18 +1,10 @@ { - "analyzerId": "ckm-analyzer-text", - "name": "ckm-analyzer-text", - "scenario": "text", + "baseAnalyzerId": "prebuilt-document", "description": "Conversation analytics", - "tags": { - "projectId": "", - "templateId": "postCallAnalytics-2024-12-01" - }, "config": { "returnDetails": true }, "fieldSchema": { - "name": "CallCenterConversationAnalysis", - "descriptions": "Content, Summary, sentiment, and more analyses from a call center conversation", "fields": { "content": { "type": "string", diff --git a/infra/main.bicep b/infra/main.bicep index 3401bd101..850a11396 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -24,10 +24,11 @@ param location string 'australiaeast' 'eastus' 'eastus2' - 'francecentral' 'japaneast' + 'southcentralus' 'swedencentral' 'uksouth' + 'westeurope' 'westus' 'westus3' ]) @@ -36,7 +37,7 @@ param location string type: 'location' usageName: [ 'OpenAI.GlobalStandard.gpt-4o-mini,150' - 'OpenAI.GlobalStandard.text-embedding-ada-002,80' + 'OpenAI.GlobalStandard.text-embedding-3-small,80' ] } }) @@ -51,16 +52,6 @@ param aiServiceLocation string ]) param usecase string -@minLength(1) -@description('Optional. Location for the Content Understanding service deployment.') -@allowed(['swedencentral', 'australiaeast']) -@metadata({ - azd: { - type: 'location' - } -}) -param contentUnderstandingLocation string = 'swedencentral' - @minLength(1) @description('Optional. Secondary location for databases creation (example: eastus2).') param secondaryLocation string = 'eastus2' @@ -79,14 +70,11 @@ param gptModelName string = 'gpt-4o-mini' @description('Optional. Version of the GPT model to deploy.') param gptModelVersion string = '2024-07-18' -@description('Optional. Version of the Azure OpenAI API.') -param azureOpenAIApiVersion string = '2025-01-01-preview' - @description('Optional. Version of AI Agent API.') param azureAiAgentApiVersion string = '2025-05-01' @description('Optional. Version of Content Understanding API.') -param azureContentUnderstandingApiVersion string = '2024-12-01-preview' +param azureContentUnderstandingApiVersion string = '2025-11-01' // You can increase this, but capacity is limited per model/region, so you will get errors if you go over // https://learn.microsoft.com/en-us/azure/ai-services/openai/quotas-limits @@ -97,9 +85,9 @@ param gptDeploymentCapacity int = 150 @minLength(1) @description('Optional. Name of the Text Embedding model to deploy.') @allowed([ - 'text-embedding-ada-002' + 'text-embedding-3-small' ]) -param embeddingModel string = 'text-embedding-ada-002' +param embeddingModel string = 'text-embedding-3-small' @minValue(10) @description('Optional. Capacity of the Embedding Model deployment.') @@ -212,6 +200,16 @@ var useExistingLogAnalytics = !empty(existingLogAnalyticsWorkspaceId) var logAnalyticsWorkspaceResourceId = useExistingLogAnalytics ? existingLogAnalyticsWorkspaceId : logAnalyticsWorkspace!.outputs.resourceId + +var existingLawSubscription = useExistingLogAnalytics ? split(existingLogAnalyticsWorkspaceId, '/')[2] : '' +var existingLawResourceGroup = useExistingLogAnalytics ? split(existingLogAnalyticsWorkspaceId, '/')[4] : '' +var existingLawName = useExistingLogAnalytics ? split(existingLogAnalyticsWorkspaceId, '/')[8] : '' + +resource existingLogAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2025-07-01' existing = if (useExistingLogAnalytics) { + name: existingLawName + scope: resourceGroup(existingLawSubscription, existingLawResourceGroup) +} + var existingTags = resourceGroup().tags ?? {} // ========== Resource Group Tag ========== // @@ -377,6 +375,125 @@ module bastionHost 'br/public:avm/res/network/bastion-host:0.8.2' = if (enablePr } } + +var dataCollectionRulesResourceName = 'dcr-${solutionSuffix}' +var dataCollectionRulesLocation = useExistingLogAnalytics + ? existingLogAnalyticsWorkspace!.location + : logAnalyticsWorkspace!.outputs.location +module windowsVmDataCollectionRules 'br/public:avm/res/insights/data-collection-rule:0.11.0' = if (enablePrivateNetworking && enableMonitoring) { + name: take('avm.res.insights.data-collection-rule.${dataCollectionRulesResourceName}', 64) + params: { + name: dataCollectionRulesResourceName + tags: tags + enableTelemetry: enableTelemetry + location: dataCollectionRulesLocation + dataCollectionRuleProperties: { + kind: 'Windows' + dataSources: { + performanceCounters: [ + { + streams: [ + 'Microsoft-Perf' + ] + samplingFrequencyInSeconds: 60 + counterSpecifiers: [ + '\\Processor Information(_Total)\\% Processor Time' + '\\Processor Information(_Total)\\% Privileged Time' + '\\Processor Information(_Total)\\% User Time' + '\\Processor Information(_Total)\\Processor Frequency' + '\\System\\Processes' + '\\Process(_Total)\\Thread Count' + '\\Process(_Total)\\Handle Count' + '\\System\\System Up Time' + '\\System\\Context Switches/sec' + '\\System\\Processor Queue Length' + '\\Memory\\% Committed Bytes In Use' + '\\Memory\\Available Bytes' + '\\Memory\\Committed Bytes' + '\\Memory\\Cache Bytes' + '\\Memory\\Pool Paged Bytes' + '\\Memory\\Pool Nonpaged Bytes' + '\\Memory\\Pages/sec' + '\\Memory\\Page Faults/sec' + '\\Process(_Total)\\Working Set' + '\\Process(_Total)\\Working Set - Private' + '\\LogicalDisk(_Total)\\% Disk Time' + '\\LogicalDisk(_Total)\\% Disk Read Time' + '\\LogicalDisk(_Total)\\% Disk Write Time' + '\\LogicalDisk(_Total)\\% Idle Time' + '\\LogicalDisk(_Total)\\Disk Bytes/sec' + '\\LogicalDisk(_Total)\\Disk Read Bytes/sec' + '\\LogicalDisk(_Total)\\Disk Write Bytes/sec' + '\\LogicalDisk(_Total)\\Disk Transfers/sec' + '\\LogicalDisk(_Total)\\Disk Reads/sec' + '\\LogicalDisk(_Total)\\Disk Writes/sec' + '\\LogicalDisk(_Total)\\Avg. Disk sec/Transfer' + '\\LogicalDisk(_Total)\\Avg. Disk sec/Read' + '\\LogicalDisk(_Total)\\Avg. Disk sec/Write' + '\\LogicalDisk(_Total)\\Avg. Disk Queue Length' + '\\LogicalDisk(_Total)\\Avg. Disk Read Queue Length' + '\\LogicalDisk(_Total)\\Avg. Disk Write Queue Length' + '\\LogicalDisk(_Total)\\% Free Space' + '\\LogicalDisk(_Total)\\Free Megabytes' + '\\Network Interface(*)\\Bytes Total/sec' + '\\Network Interface(*)\\Bytes Sent/sec' + '\\Network Interface(*)\\Bytes Received/sec' + '\\Network Interface(*)\\Packets/sec' + '\\Network Interface(*)\\Packets Sent/sec' + '\\Network Interface(*)\\Packets Received/sec' + '\\Network Interface(*)\\Packets Outbound Errors' + '\\Network Interface(*)\\Packets Received Errors' + ] + name: 'perfCounterDataSource60' + } + ] + windowsEventLogs: [ + { + name: 'SecurityAuditEvents' + streams: [ + 'Microsoft-Event' + ] + xPathQueries: [ + 'Security!*[System[(band(Keywords,13510798882111488)) and (EventID != 4624)]]' + ] + } + ] + } + destinations: { + logAnalytics: [ + { + workspaceResourceId: logAnalyticsWorkspaceResourceId + name: 'la--1264800308' + } + ] + } + dataFlows: [ + { + streams: [ + 'Microsoft-Perf' + ] + destinations: [ + 'la--1264800308' + ] + transformKql: 'source' + outputStream: 'Microsoft-Perf' + } + { + streams: [ + 'Microsoft-Event' + ] + destinations: [ + 'la--1264800308' + ] + transformKql: 'source' + outputStream: 'Microsoft-Event' + } + ] + } + } +} + + // Jumpbox Virtual Machine var jumpboxVmName = take('vm-jumpbox-${solutionSuffix}', 15) module jumpboxVM 'br/public:avm/res/compute/virtual-machine:0.21.0' = if (enablePrivateNetworking) { @@ -433,6 +550,18 @@ module jumpboxVM 'br/public:avm/res/compute/virtual-machine:0.21.0' = if (enable } ] enableTelemetry: enableTelemetry + extensionMonitoringAgentConfig: enableMonitoring + ? { + dataCollectionRuleAssociations: [ + { + dataCollectionRuleResourceId: windowsVmDataCollectionRules!.outputs.resourceId + name: 'send-${logAnalyticsWorkspaceResourceName}' + } + ] + enabled: true + tags: tags + } + : null } } @@ -448,6 +577,7 @@ var privateDnsZones = [ 'privatelink.documents.azure.com' 'privatelink${environment().suffixes.sqlServerHostname}' 'privatelink.search.windows.net' + 'privatelink.azurewebsites.net' ] // DNS Zone Index Constants @@ -462,6 +592,7 @@ var dnsZoneIndex = { cosmosDB: 7 sqlServer: 8 search: 9 + webApp: 10 } // =================================================== @@ -519,7 +650,6 @@ module backendUserAssignedIdentity 'br/public:avm/res/managed-identity/user-assi // ========== AI Foundry: AI Services ========== // // WAF best practices for Open AI: https://learn.microsoft.com/en-us/azure/well-architected/service-guides/azure-openai -var existingOpenAIEndpoint = !empty(existingAiFoundryAiProjectResourceId) ? format('https://{0}.openai.azure.com/', split(existingAiFoundryAiProjectResourceId, '/')[8]) : '' var existingProjEndpoint = !empty(existingAiFoundryAiProjectResourceId) ? format('https://{0}.services.ai.azure.com/api/projects/{1}', split(existingAiFoundryAiProjectResourceId, '/')[8], split(existingAiFoundryAiProjectResourceId, '/')[10]) : '' var existingAIServicesName = !empty(existingAiFoundryAiProjectResourceId) ? split(existingAiFoundryAiProjectResourceId, '/')[8] : '' var existingAIProjectName = !empty(existingAiFoundryAiProjectResourceId) ? split(existingAiFoundryAiProjectResourceId, '/')[10] : '' @@ -538,10 +668,7 @@ var aiFoundryAiProjectResourceName = useExistingAiFoundryAiProject ? split(existingAiFoundryAiProjectResourceId, '/')[10] : 'proj-${solutionSuffix}' -// NOTE: Required version 'Microsoft.CognitiveServices/accounts@2024-04-01-preview' not available in AVM -// var aiFoundryAiServicesResourceName = 'aif-${solutionSuffix}' var aiFoundryAiServicesAiProjectResourceName = 'proj-${solutionSuffix}' -var aiFoundryAIservicesEnabled = true var aiModelDeployments = [ { name: gptModelName @@ -562,7 +689,7 @@ var aiModelDeployments = [ name: 'GlobalStandard' capacity: embeddingDeploymentCapacity } - version: '2' + version: '1' raiPolicyName: 'Microsoft.Default' } ] @@ -577,7 +704,7 @@ resource existingAiFoundryAiServicesProject 'Microsoft.CognitiveServices/account parent: existingAiFoundryAiServices } -module aiFoundryAiServices 'modules/ai-services.bicep' = if (aiFoundryAIservicesEnabled) { +module aiFoundryAiServices 'modules/ai-services.bicep' = { name: take('avm.res.cognitive-services.account.${aiFoundryAiServicesResourceName}', 64) params: { name: aiFoundryAiServicesResourceName @@ -691,79 +818,6 @@ module aiFoundryPrivateEndpoint 'br/public:avm/res/network/private-endpoint:0.8. } } -// AI Foundry: AI Services Content Understanding -var aiFoundryAiServicesCUResourceName = 'aif-${solutionSuffix}-cu' -var aiServicesNameCu = 'aisa-${solutionSuffix}-cu' -module cognitiveServicesCu 'br/public:avm/res/cognitive-services/account:0.14.1' = { - name: take('avm.res.cognitive-services.account.${aiFoundryAiServicesCUResourceName}', 64) - params: { - name: aiServicesNameCu - location: contentUnderstandingLocation - tags: tags - enableTelemetry: enableTelemetry - diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null - sku: 'S0' - kind: 'AIServices' - networkAcls: { - defaultAction: 'Allow' - virtualNetworkRules: [] - ipRules: [] - } - managedIdentities: { userAssignedResourceIds: [userAssignedIdentity!.outputs.resourceId] } //To create accounts or projects, you must enable a managed identity on your resource - disableLocalAuth: true - customSubDomainName: aiServicesNameCu - apiProperties: { - // staticsEnabled: false - } - publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' - privateEndpoints: [] - roleAssignments: [ - { - roleDefinitionIdOrName: '53ca6127-db72-4b80-b1b0-d745d6d5456d' // Azure AI User - principalId: userAssignedIdentity.outputs.principalId - principalType: 'ServicePrincipal' - } - ] - } -} - -// ========== AI Services CU: Separate Private Endpoint ========== // -module cognitiveServicesCuPrivateEndpoint 'br/public:avm/res/network/private-endpoint:0.8.1' = if (enablePrivateNetworking) { - name: take('pep-${aiFoundryAiServicesCUResourceName}-deployment', 64) - params: { - name: 'pep-${aiFoundryAiServicesCUResourceName}' - customNetworkInterfaceName: 'nic-${aiFoundryAiServicesCUResourceName}' - location: location - tags: tags - privateLinkServiceConnections: [ - { - name: 'pep-${aiFoundryAiServicesCUResourceName}-connection' - properties: { - privateLinkServiceId: cognitiveServicesCu.outputs.resourceId - groupIds: ['account'] - } - } - ] - privateDnsZoneGroup: { - privateDnsZoneGroupConfigs: [ - { - name: 'ai-services-cu-dns-zone-cognitiveservices' - privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.cognitiveServices]!.outputs.resourceId - } - { - name: 'ai-services-cu-dns-zone-openai' - privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.openAI]!.outputs.resourceId - } - { - name: 'ai-services-cu-dns-zone-aiservices' - privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.aiServices]!.outputs.resourceId - } - ] - } - subnetResourceId: virtualNetwork!.outputs.pepsSubnetResourceId - } -} - // ========== AVM WAF ========== // // ========== AI Foundry: AI Search ========== // var aiSearchName = 'srch-${solutionSuffix}' @@ -783,6 +837,7 @@ module searchServiceUpdate 'br/public:avm/res/search/search-service:0.12.0' = { params: { // Required parameters name: aiSearchName + location: location enableTelemetry: enableTelemetry diagnosticSettings: enableMonitoring ? [ { @@ -957,6 +1012,7 @@ module storageAccount 'br/public:avm/res/storage/storage-account:0.31.0' = { allowSharedKeyAccess: true allowBlobPublicAccess: false publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' + requireInfrastructureEncryption: true privateEndpoints: enablePrivateNetworking ? [ { @@ -1329,10 +1385,6 @@ module webSiteBackend 'modules/web-sites.bicep' = { AGENT_NAME_TITLE: '' API_APP_NAME: 'api-${solutionSuffix}' AI_FOUNDRY_RESOURCE_ID: aiFoundryAiServices.outputs.resourceId - AZURE_OPENAI_DEPLOYMENT_MODEL: gptModelName - AZURE_OPENAI_ENDPOINT: !empty(existingOpenAIEndpoint) ? existingOpenAIEndpoint : 'https://${aiFoundryAiServices.outputs.name}.openai.azure.com/' - AZURE_OPENAI_API_VERSION: azureOpenAIApiVersion - AZURE_OPENAI_RESOURCE: aiFoundryAiServices.outputs.name AZURE_AI_AGENT_ENDPOINT: !empty(existingProjEndpoint) ? existingProjEndpoint : aiFoundryAiServices.outputs.aiProjectInfo.apiEndpoint AZURE_AI_AGENT_API_VERSION: azureAiAgentApiVersion AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME: gptModelName @@ -1362,12 +1414,28 @@ module webSiteBackend 'modules/web-sites.bicep' = { applicationInsightResourceId: enableMonitoring ? applicationInsights!.outputs.resourceId : null } ] + e2eEncryptionEnabled: true diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null // WAF aligned configuration for Private Networking vnetRouteAllEnabled: enablePrivateNetworking ? true : false vnetImagePullEnabled: enablePrivateNetworking ? true : false virtualNetworkSubnetId: enablePrivateNetworking ? virtualNetwork!.outputs.webSubnetResourceId : null - publicNetworkAccess: 'Enabled' + publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' + privateEndpoints: enablePrivateNetworking + ? [ + { + name: 'pep-${backendWebSiteResourceName}' + customNetworkInterfaceName: 'nic-${backendWebSiteResourceName}' + privateDnsZoneGroup: { + privateDnsZoneGroupConfigs: [ + { privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.webApp]!.outputs.resourceId } + ] + } + service: 'sites' + subnetResourceId: virtualNetwork!.outputs.pepsSubnetResourceId + } + ] + : [] } } @@ -1394,11 +1462,13 @@ module webSiteFrontend 'modules/web-sites.bicep' = { { name: 'appsettings' properties: { - APP_API_BASE_URL: 'https://api-${solutionSuffix}.azurewebsites.net' + APP_API_BASE_URL: enablePrivateNetworking ? '' : 'https://api-${solutionSuffix}.azurewebsites.net' + BACKEND_API_HOST: enablePrivateNetworking ? 'api-${solutionSuffix}.azurewebsites.net' : '' } applicationInsightResourceId: enableMonitoring ? applicationInsights!.outputs.resourceId : null } ] + e2eEncryptionEnabled: true vnetRouteAllEnabled: enablePrivateNetworking ? true : false vnetImagePullEnabled: enablePrivateNetworking ? true : false virtualNetworkSubnetId: enablePrivateNetworking ? virtualNetwork!.outputs.webSubnetResourceId : null @@ -1417,9 +1487,6 @@ output RESOURCE_GROUP_NAME string = resourceGroup().name @description('Contains Resource Group Location.') output RESOURCE_GROUP_LOCATION string = location -@description('Contains Azure Content Understanding Location.') -output AZURE_CONTENT_UNDERSTANDING_LOCATION string = contentUnderstandingLocation - // @description('Contains Azure Secondary Location.') // output AZURE_SECONDARY_LOCATION string = secondaryLocation @@ -1463,25 +1530,22 @@ output AZURE_COSMOSDB_DATABASE string = 'db_conversation_history' output AZURE_COSMOSDB_ENABLE_FEEDBACK string = 'True' @description('Contains Azure OpenAI deployment model name.') -output AZURE_OPENAI_DEPLOYMENT_MODEL string = gptModelName +output AZURE_ENV_GPT_MODEL_NAME string = gptModelName @description('Contains Azure OpenAI deployment model capacity.') -output AZURE_OPENAI_DEPLOYMENT_MODEL_CAPACITY int = gptDeploymentCapacity +output AZURE_ENV_GPT_MODEL_CAPACITY int = gptDeploymentCapacity @description('Contains Azure OpenAI endpoint URL.') output AZURE_OPENAI_ENDPOINT string = 'https://${aiFoundryAiServices.outputs.name}.openai.azure.com/' @description('Contains Azure OpenAI model deployment type.') -output AZURE_OPENAI_MODEL_DEPLOYMENT_TYPE string = deploymentType +output AZURE_ENV_MODEL_DEPLOYMENT_TYPE string = deploymentType @description('Contains Azure OpenAI embedding model name.') -output AZURE_OPENAI_EMBEDDING_MODEL string = embeddingModel +output AZURE_ENV_EMBEDDING_MODEL_NAME string = embeddingModel @description('Contains Azure OpenAI embedding model capacity.') -output AZURE_OPENAI_EMBEDDING_MODEL_CAPACITY int = embeddingDeploymentCapacity - -@description('Contains Azure OpenAI API version.') -output AZURE_OPENAI_API_VERSION string = azureOpenAIApiVersion +output AZURE_ENV_EMBEDDING_DEPLOYMENT_CAPACITY int = embeddingDeploymentCapacity @description('Contains Content Understanding API version.') output AZURE_CONTENT_UNDERSTANDING_API_VERSION string = azureContentUnderstandingApiVersion @@ -1523,10 +1587,7 @@ output AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME string = gptModelName output ACR_NAME string = acrName @description('Contains Azure environment image tag.') -output AZURE_ENV_IMAGETAG string = backendContainerImageTag - -@description('Contains existing AI project resource ID.') -output AZURE_EXISTING_AI_PROJECT_RESOURCE_ID string = existingAiFoundryAiProjectResourceId +output AZURE_ENV_IMAGE_TAG string = backendContainerImageTag @description('Contains Application Insights connection string.') output APPLICATIONINSIGHTS_CONNECTION_STRING string = enableMonitoring ? applicationInsights!.outputs.connectionString : '' @@ -1546,11 +1607,8 @@ output STORAGE_CONTAINER_NAME string = 'data' @description('Resource ID of the AI Foundry.') output AI_FOUNDRY_RESOURCE_ID string = aiFoundryAiServices.outputs.resourceId -@description('Resource ID of the Content Understanding AI Foundry.') -output CU_FOUNDRY_RESOURCE_ID string = cognitiveServicesCu.outputs.resourceId - @description('Azure OpenAI Content Understanding endpoint URL.') -output AZURE_OPENAI_CU_ENDPOINT string = cognitiveServicesCu.outputs.endpoint +output AZURE_OPENAI_CU_ENDPOINT string = aiFoundryAiServices.outputs.endpoints['Content Understanding'] @description('Contains API application name.') output API_APP_NAME string = 'api-${solutionSuffix}' diff --git a/infra/main.json b/infra/main.json index 6d14002ab..93af46a47 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,8 +5,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "8063559554038132231" + "version": "0.43.8.12551", + "templateHash": "9558335949477141360" } }, "parameters": { @@ -44,10 +44,11 @@ "australiaeast", "eastus", "eastus2", - "francecentral", "japaneast", + "southcentralus", "swedencentral", "uksouth", + "westeurope", "westus", "westus3" ], @@ -56,7 +57,7 @@ "type": "location", "usageName": [ "OpenAI.GlobalStandard.gpt-4o-mini,150", - "OpenAI.GlobalStandard.text-embedding-ada-002,80" + "OpenAI.GlobalStandard.text-embedding-3-small,80" ] }, "description": "Required. Location for AI Foundry deployment. This is the location where the AI Foundry resources will be deployed." @@ -73,21 +74,6 @@ "description": "Required. Industry use case for deployment." } }, - "contentUnderstandingLocation": { - "type": "string", - "defaultValue": "swedencentral", - "allowedValues": [ - "swedencentral", - "australiaeast" - ], - "metadata": { - "azd": { - "type": "location" - }, - "description": "Optional. Location for the Content Understanding service deployment." - }, - "minLength": 1 - }, "secondaryLocation": { "type": "string", "defaultValue": "eastus2", @@ -122,13 +108,6 @@ "description": "Optional. Version of the GPT model to deploy." } }, - "azureOpenAIApiVersion": { - "type": "string", - "defaultValue": "2025-01-01-preview", - "metadata": { - "description": "Optional. Version of the Azure OpenAI API." - } - }, "azureAiAgentApiVersion": { "type": "string", "defaultValue": "2025-05-01", @@ -138,7 +117,7 @@ }, "azureContentUnderstandingApiVersion": { "type": "string", - "defaultValue": "2024-12-01-preview", + "defaultValue": "2025-11-01", "metadata": { "description": "Optional. Version of Content Understanding API." } @@ -153,9 +132,9 @@ }, "embeddingModel": { "type": "string", - "defaultValue": "text-embedding-ada-002", + "defaultValue": "text-embedding-3-small", "allowedValues": [ - "text-embedding-ada-002" + "text-embedding-3-small" ], "minLength": 1, "metadata": { @@ -338,10 +317,14 @@ }, "cosmosDbHaLocation": "[variables('cosmosDbZoneRedundantHaRegionPairs')[resourceGroup().location]]", "useExistingLogAnalytics": "[not(empty(parameters('existingLogAnalyticsWorkspaceId')))]", + "existingLawSubscription": "[if(variables('useExistingLogAnalytics'), split(parameters('existingLogAnalyticsWorkspaceId'), '/')[2], '')]", + "existingLawResourceGroup": "[if(variables('useExistingLogAnalytics'), split(parameters('existingLogAnalyticsWorkspaceId'), '/')[4], '')]", + "existingLawName": "[if(variables('useExistingLogAnalytics'), split(parameters('existingLogAnalyticsWorkspaceId'), '/')[8], '')]", "existingTags": "[coalesce(resourceGroup().tags, createObject())]", "logAnalyticsWorkspaceResourceName": "[format('log-{0}', variables('solutionSuffix'))]", "applicationInsightsResourceName": "[format('appi-{0}', variables('solutionSuffix'))]", "bastionHostName": "[format('bas-{0}', variables('solutionSuffix'))]", + "dataCollectionRulesResourceName": "[format('dcr-{0}', variables('solutionSuffix'))]", "jumpboxVmName": "[take(format('vm-jumpbox-{0}', variables('solutionSuffix')), 15)]", "privateDnsZones": [ "privatelink.cognitiveservices.azure.com", @@ -353,7 +336,8 @@ "[format('privatelink.dfs.{0}', environment().suffixes.storage)]", "privatelink.documents.azure.com", "[format('privatelink{0}', environment().suffixes.sqlServerHostname)]", - "privatelink.search.windows.net" + "privatelink.search.windows.net", + "privatelink.azurewebsites.net" ], "dnsZoneIndex": { "cognitiveServices": 0, @@ -365,11 +349,11 @@ "storageDfs": 6, "cosmosDB": 7, "sqlServer": 8, - "search": 9 + "search": 9, + "webApp": 10 }, "userAssignedIdentityResourceName": "[format('id-{0}', variables('solutionSuffix'))]", "backendUserAssignedIdentityResourceName": "[format('id-backend-{0}', variables('solutionSuffix'))]", - "existingOpenAIEndpoint": "[if(not(empty(parameters('existingAiFoundryAiProjectResourceId'))), format('https://{0}.openai.azure.com/', split(parameters('existingAiFoundryAiProjectResourceId'), '/')[8]), '')]", "existingProjEndpoint": "[if(not(empty(parameters('existingAiFoundryAiProjectResourceId'))), format('https://{0}.services.ai.azure.com/api/projects/{1}', split(parameters('existingAiFoundryAiProjectResourceId'), '/')[8], split(parameters('existingAiFoundryAiProjectResourceId'), '/')[10]), '')]", "existingAIServicesName": "[if(not(empty(parameters('existingAiFoundryAiProjectResourceId'))), split(parameters('existingAiFoundryAiProjectResourceId'), '/')[8], '')]", "existingAIProjectName": "[if(not(empty(parameters('existingAiFoundryAiProjectResourceId'))), split(parameters('existingAiFoundryAiProjectResourceId'), '/')[10], '')]", @@ -379,7 +363,6 @@ "aiFoundryAiServicesResourceName": "[if(variables('useExistingAiFoundryAiProject'), split(parameters('existingAiFoundryAiProjectResourceId'), '/')[8], format('aif-{0}', variables('solutionSuffix')))]", "aiFoundryAiProjectResourceName": "[if(variables('useExistingAiFoundryAiProject'), split(parameters('existingAiFoundryAiProjectResourceId'), '/')[10], format('proj-{0}', variables('solutionSuffix')))]", "aiFoundryAiServicesAiProjectResourceName": "[format('proj-{0}', variables('solutionSuffix'))]", - "aiFoundryAIservicesEnabled": true, "aiModelDeployments": [ { "name": "[parameters('gptModelName')]", @@ -400,12 +383,10 @@ "name": "GlobalStandard", "capacity": "[parameters('embeddingDeploymentCapacity')]" }, - "version": "2", + "version": "1", "raiPolicyName": "Microsoft.Default" } ], - "aiFoundryAiServicesCUResourceName": "[format('aif-{0}-cu', variables('solutionSuffix'))]", - "aiServicesNameCu": "[format('aisa-{0}-cu', variables('solutionSuffix'))]", "aiSearchName": "[format('srch-{0}', variables('solutionSuffix'))]", "aiSearchConnectionName": "[format('foundry-search-connection-{0}', variables('solutionSuffix'))]", "storageAccountName": "[format('st{0}', variables('solutionSuffix'))]", @@ -420,6 +401,15 @@ "webSiteResourceName": "[format('app-{0}', variables('solutionSuffix'))]" }, "resources": { + "existingLogAnalyticsWorkspace": { + "condition": "[variables('useExistingLogAnalytics')]", + "existing": true, + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2025-07-01", + "subscriptionId": "[variables('existingLawSubscription')]", + "resourceGroup": "[variables('existingLawResourceGroup')]", + "name": "[variables('existingLawName')]" + }, "resourceGroupTags": { "type": "Microsoft.Resources/tags", "apiVersion": "2025-04-01", @@ -4438,8 +4428,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "7835683830649565955" + "version": "0.43.8.12551", + "templateHash": "14263337584307645476" } }, "definitions": { @@ -8990,11 +8980,11 @@ "virtualNetwork" ] }, - "jumpboxVM": { - "condition": "[parameters('enablePrivateNetworking')]", + "windowsVmDataCollectionRules": { + "condition": "[and(parameters('enablePrivateNetworking'), parameters('enableMonitoring'))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[take(format('avm.res.compute.virtual-machine.{0}', variables('jumpboxVmName')), 64)]", + "name": "[take(format('avm.res.insights.data-collection-rule.{0}', variables('dataCollectionRulesResourceName')), 64)]", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -9002,81 +8992,119 @@ "mode": "Incremental", "parameters": { "name": { - "value": "[take(variables('jumpboxVmName'), 15)]" - }, - "vmSize": { - "value": "[coalesce(parameters('vmSize'), 'Standard_D2s_v5')]" - }, - "location": { - "value": "[parameters('location')]" - }, - "adminUsername": { - "value": "[coalesce(parameters('vmAdminUsername'), 'JumpboxAdminUser')]" - }, - "adminPassword": { - "value": "[coalesce(parameters('vmAdminPassword'), 'JumpboxAdminP@ssw0rd1234!')]" + "value": "[variables('dataCollectionRulesResourceName')]" }, "tags": { "value": "[parameters('tags')]" }, - "availabilityZone": { - "value": -1 - }, - "imageReference": { - "value": { - "publisher": "microsoft-dsvm", - "offer": "dsvm-win-2022", - "sku": "winserver-2022", - "version": "latest" - } - }, - "osType": { - "value": "Windows" + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" }, - "osDisk": { + "location": "[if(variables('useExistingLogAnalytics'), createObject('value', reference('existingLogAnalyticsWorkspace', '2025-07-01', 'full').location), createObject('value', reference('logAnalyticsWorkspace').outputs.location.value))]", + "dataCollectionRuleProperties": { "value": { - "name": "[format('osdisk-{0}', variables('jumpboxVmName'))]", - "managedDisk": { - "storageAccountType": "Standard_LRS" - } - } - }, - "encryptionAtHost": { - "value": false - }, - "nicConfigurations": { - "value": [ - { - "name": "[format('nic-{0}', variables('jumpboxVmName'))]", - "ipConfigurations": [ + "kind": "Windows", + "dataSources": { + "performanceCounters": [ { - "name": "ipconfig1", - "subnetResourceId": "[reference('virtualNetwork').outputs.jumpboxSubnetResourceId.value]" + "streams": [ + "Microsoft-Perf" + ], + "samplingFrequencyInSeconds": 60, + "counterSpecifiers": [ + "\\Processor Information(_Total)\\% Processor Time", + "\\Processor Information(_Total)\\% Privileged Time", + "\\Processor Information(_Total)\\% User Time", + "\\Processor Information(_Total)\\Processor Frequency", + "\\System\\Processes", + "\\Process(_Total)\\Thread Count", + "\\Process(_Total)\\Handle Count", + "\\System\\System Up Time", + "\\System\\Context Switches/sec", + "\\System\\Processor Queue Length", + "\\Memory\\% Committed Bytes In Use", + "\\Memory\\Available Bytes", + "\\Memory\\Committed Bytes", + "\\Memory\\Cache Bytes", + "\\Memory\\Pool Paged Bytes", + "\\Memory\\Pool Nonpaged Bytes", + "\\Memory\\Pages/sec", + "\\Memory\\Page Faults/sec", + "\\Process(_Total)\\Working Set", + "\\Process(_Total)\\Working Set - Private", + "\\LogicalDisk(_Total)\\% Disk Time", + "\\LogicalDisk(_Total)\\% Disk Read Time", + "\\LogicalDisk(_Total)\\% Disk Write Time", + "\\LogicalDisk(_Total)\\% Idle Time", + "\\LogicalDisk(_Total)\\Disk Bytes/sec", + "\\LogicalDisk(_Total)\\Disk Read Bytes/sec", + "\\LogicalDisk(_Total)\\Disk Write Bytes/sec", + "\\LogicalDisk(_Total)\\Disk Transfers/sec", + "\\LogicalDisk(_Total)\\Disk Reads/sec", + "\\LogicalDisk(_Total)\\Disk Writes/sec", + "\\LogicalDisk(_Total)\\Avg. Disk sec/Transfer", + "\\LogicalDisk(_Total)\\Avg. Disk sec/Read", + "\\LogicalDisk(_Total)\\Avg. Disk sec/Write", + "\\LogicalDisk(_Total)\\Avg. Disk Queue Length", + "\\LogicalDisk(_Total)\\Avg. Disk Read Queue Length", + "\\LogicalDisk(_Total)\\Avg. Disk Write Queue Length", + "\\LogicalDisk(_Total)\\% Free Space", + "\\LogicalDisk(_Total)\\Free Megabytes", + "\\Network Interface(*)\\Bytes Total/sec", + "\\Network Interface(*)\\Bytes Sent/sec", + "\\Network Interface(*)\\Bytes Received/sec", + "\\Network Interface(*)\\Packets/sec", + "\\Network Interface(*)\\Packets Sent/sec", + "\\Network Interface(*)\\Packets Received/sec", + "\\Network Interface(*)\\Packets Outbound Errors", + "\\Network Interface(*)\\Packets Received Errors" + ], + "name": "perfCounterDataSource60" } ], - "diagnosticSettings": [ + "windowsEventLogs": [ { - "name": "jumpboxDiagnostics", - "workspaceResourceId": "[if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), reference('logAnalyticsWorkspace').outputs.resourceId.value)]", - "logCategoriesAndGroups": [ - { - "categoryGroup": "allLogs", - "enabled": true - } + "name": "SecurityAuditEvents", + "streams": [ + "Microsoft-Event" ], - "metricCategories": [ - { - "category": "AllMetrics", - "enabled": true - } + "xPathQueries": [ + "Security!*[System[(band(Keywords,13510798882111488)) and (EventID != 4624)]]" ] } ] - } - ] - }, - "enableTelemetry": { - "value": "[parameters('enableTelemetry')]" + }, + "destinations": { + "logAnalytics": [ + { + "workspaceResourceId": "[if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), reference('logAnalyticsWorkspace').outputs.resourceId.value)]", + "name": "la--1264800308" + } + ] + }, + "dataFlows": [ + { + "streams": [ + "Microsoft-Perf" + ], + "destinations": [ + "la--1264800308" + ], + "transformKql": "source", + "outputStream": "Microsoft-Perf" + }, + { + "streams": [ + "Microsoft-Event" + ], + "destinations": [ + "la--1264800308" + ], + "transformKql": "source", + "outputStream": "Microsoft-Event" + } + ] + } } }, "template": { @@ -9086,2033 +9114,2716 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "11442373542874951910" + "version": "0.41.2.15936", + "templateHash": "2441324888126124697" }, - "name": "Virtual Machines", - "description": "This module deploys a Virtual Machine with one or multiple NICs and optionally one or multiple public IPs." + "name": "Data Collection Rules", + "description": "This module deploys a Data Collection Rule." }, "definitions": { - "osDiskType": { + "dataCollectionRulePropertiesType": { + "type": "object", + "discriminator": { + "propertyName": "kind", + "mapping": { + "Linux": { + "$ref": "#/definitions/linuxDcrPropertiesType" + }, + "Windows": { + "$ref": "#/definitions/windowsDcrPropertiesType" + }, + "All": { + "$ref": "#/definitions/allPlatformsDcrPropertiesType" + }, + "AgentSettings": { + "$ref": "#/definitions/agentSettingsDcrPropertiesType" + }, + "Direct": { + "$ref": "#/definitions/directDcrPropertiesType" + }, + "WorkspaceTransforms": { + "$ref": "#/definitions/workspaceTransformsDcrPropertiesType" + }, + "PlatformTelemetry": { + "$ref": "#/definitions/platformTelemetryDcrPropertiesType" + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Required. The type for data collection rule properties. Depending on the kind, the properties will be different." + } + }, + "linuxDcrPropertiesType": { "type": "object", "properties": { - "name": { + "kind": { "type": "string", - "nullable": true, + "allowedValues": [ + "Linux" + ], "metadata": { - "description": "Optional. The disk name." + "description": "Required. The kind of the resource." } }, - "diskSizeGB": { - "type": "int", - "nullable": true, + "dataSources": { + "type": "object", "metadata": { - "description": "Optional. Specifies the size of an empty data disk in gigabytes." + "__bicep_resource_derived_type!": { + "source": "Microsoft.Insights/dataCollectionRules@2024-03-11#properties/properties/properties/dataSources" + }, + "description": "Required. Specification of data sources that will be collected." } }, - "createOption": { - "type": "string", - "allowedValues": [ - "Attach", - "Empty", - "FromImage" - ], - "nullable": true, + "dataFlows": { + "type": "array", "metadata": { - "description": "Optional. Specifies how the virtual machine should be created." + "__bicep_resource_derived_type!": { + "source": "Microsoft.Insights/dataCollectionRules@2024-03-11#properties/properties/properties/dataFlows" + }, + "description": "Required. The specification of data flows." } }, - "deleteOption": { - "type": "string", - "allowedValues": [ - "Delete", - "Detach" - ], - "nullable": true, + "destinations": { + "type": "object", "metadata": { - "description": "Optional. Specifies whether data disk should be deleted or detached upon VM deletion." + "__bicep_resource_derived_type!": { + "source": "Microsoft.Insights/dataCollectionRules@2024-03-11#properties/properties/properties/destinations" + }, + "description": "Required. Specification of destinations that can be used in data flows." } }, - "caching": { + "dataCollectionEndpointResourceId": { "type": "string", - "allowedValues": [ - "None", - "ReadOnly", - "ReadWrite" - ], "nullable": true, "metadata": { - "description": "Optional. Specifies the caching requirements." + "description": "Optional. The resource ID of the data collection endpoint that this rule can be used with." } }, - "diffDiskSettings": { + "streamDeclarations": { "type": "object", - "properties": { - "placement": { - "type": "string", - "allowedValues": [ - "CacheDisk", - "NvmeDisk", - "ResourceDisk" - ], - "metadata": { - "description": "Required. Specifies the ephemeral disk placement for the operating system disk." - } - } - }, - "nullable": true, "metadata": { - "description": "Optional. Specifies the ephemeral Disk Settings for the operating system disk." - } - }, - "managedDisk": { - "type": "object", - "properties": { - "storageAccountType": { - "type": "string", - "allowedValues": [ - "PremiumV2_LRS", - "Premium_LRS", - "Premium_ZRS", - "StandardSSD_LRS", - "StandardSSD_ZRS", - "Standard_LRS", - "UltraSSD_LRS" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specifies the storage account type for the managed disk." - } - }, - "diskEncryptionSetResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specifies the customer managed disk encryption set resource id for the managed disk." - } + "__bicep_resource_derived_type!": { + "source": "Microsoft.Insights/dataCollectionRules@2024-03-11#properties/properties/properties/streamDeclarations" }, - "resourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specifies the resource id of a pre-existing managed disk. If the disk should be created, this property should be empty." - } - } + "description": "Optional. Declaration of custom streams used in this rule." }, + "nullable": true + }, + "description": { + "type": "string", + "nullable": true, "metadata": { - "description": "Required. The managed disk parameters." + "description": "Optional. Description of the data collection rule." } } }, "metadata": { - "__bicep_export!": true, - "description": "The type describing an OS disk." + "description": "The type for the properties of the 'Linux' data collection rule." } }, - "dataDiskType": { + "windowsDcrPropertiesType": { "type": "object", "properties": { - "name": { + "kind": { "type": "string", - "nullable": true, + "allowedValues": [ + "Windows" + ], "metadata": { - "description": "Optional. The disk name. When attaching a pre-existing disk, this name is ignored and the name of the existing disk is used." + "description": "Required. The kind of the resource." } }, - "lun": { - "type": "int", - "nullable": true, + "dataSources": { + "type": "object", "metadata": { - "description": "Optional. Specifies the logical unit number of the data disk." + "__bicep_resource_derived_type!": { + "source": "Microsoft.Insights/dataCollectionRules@2024-03-11#properties/properties/properties/dataSources" + }, + "description": "Required. Specification of data sources that will be collected." } }, - "diskSizeGB": { - "type": "int", - "nullable": true, + "dataFlows": { + "type": "array", "metadata": { - "description": "Optional. Specifies the size of an empty data disk in gigabytes. This property is ignored when attaching a pre-existing disk." + "__bicep_resource_derived_type!": { + "source": "Microsoft.Insights/dataCollectionRules@2024-03-11#properties/properties/properties/dataFlows" + }, + "description": "Required. The specification of data flows." } }, - "createOption": { + "destinations": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Insights/dataCollectionRules@2024-03-11#properties/properties/properties/destinations" + }, + "description": "Required. Specification of destinations that can be used in data flows." + } + }, + "dataCollectionEndpointResourceId": { "type": "string", - "allowedValues": [ - "Attach", - "Empty", - "FromImage" - ], "nullable": true, "metadata": { - "description": "Optional. Specifies how the virtual machine should be created. This property is automatically set to 'Attach' when attaching a pre-existing disk." + "description": "Optional. The resource ID of the data collection endpoint that this rule can be used with." } }, - "deleteOption": { + "streamDeclarations": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Insights/dataCollectionRules@2024-03-11#properties/properties/properties/streamDeclarations" + }, + "description": "Optional. Declaration of custom streams used in this rule." + }, + "nullable": true + }, + "description": { "type": "string", - "allowedValues": [ - "Delete", - "Detach" - ], "nullable": true, "metadata": { - "description": "Optional. Specifies whether data disk should be deleted or detached upon VM deletion. This property is automatically set to 'Detach' when attaching a pre-existing disk." + "description": "Optional. Description of the data collection rule." } - }, - "caching": { + } + }, + "metadata": { + "description": "The type for the properties of the 'Windows' data collection rule." + } + }, + "allPlatformsDcrPropertiesType": { + "type": "object", + "properties": { + "kind": { "type": "string", "allowedValues": [ - "None", - "ReadOnly", - "ReadWrite" + "All" ], - "nullable": true, "metadata": { - "description": "Optional. Specifies the caching requirements. This property is automatically set to 'None' when attaching a pre-existing disk." + "description": "Required. The kind of the resource." } }, - "diskIOPSReadWrite": { - "type": "int", - "nullable": true, + "dataSources": { + "type": "object", "metadata": { - "description": "Optional. The number of IOPS allowed for this disk; only settable for UltraSSD disks. One operation can transfer between 4k and 256k bytes. Ignored when attaching a pre-existing disk." + "__bicep_resource_derived_type!": { + "source": "Microsoft.Insights/dataCollectionRules@2024-03-11#properties/properties/properties/dataSources" + }, + "description": "Required. Specification of data sources that will be collected." } }, - "diskMBpsReadWrite": { - "type": "int", - "nullable": true, + "dataFlows": { + "type": "array", "metadata": { - "description": "Optional. The bandwidth allowed for this disk; only settable for UltraSSD disks. MBps means millions of bytes per second - MB here uses the ISO notation, of powers of 10. Ignored when attaching a pre-existing disk." + "__bicep_resource_derived_type!": { + "source": "Microsoft.Insights/dataCollectionRules@2024-03-11#properties/properties/properties/dataFlows" + }, + "description": "Required. The specification of data flows." } }, - "managedDisk": { + "destinations": { "type": "object", - "properties": { - "storageAccountType": { - "type": "string", - "allowedValues": [ - "PremiumV2_LRS", - "Premium_LRS", - "Premium_ZRS", - "StandardSSD_LRS", - "StandardSSD_ZRS", - "Standard_LRS", - "UltraSSD_LRS" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specifies the storage account type for the managed disk. Ignored when attaching a pre-existing disk." - } - }, - "diskEncryptionSetResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specifies the customer managed disk encryption set resource id for the managed disk." - } + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Insights/dataCollectionRules@2024-03-11#properties/properties/properties/destinations" }, - "resourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specifies the resource id of a pre-existing managed disk. If the disk should be created, this property should be empty." - } - } - }, + "description": "Required. Specification of destinations that can be used in data flows." + } + }, + "dataCollectionEndpointResourceId": { + "type": "string", + "nullable": true, "metadata": { - "description": "Required. The managed disk parameters." + "description": "Optional. The resource ID of the data collection endpoint that this rule can be used with." } }, - "tags": { + "streamDeclarations": { "type": "object", "metadata": { "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/disks@2025-01-02#properties/tags" + "source": "Microsoft.Insights/dataCollectionRules@2024-03-11#properties/properties/properties/streamDeclarations" }, - "description": "Optional. The tags of the public IP address. Valid only when creating a new managed disk." + "description": "Optional. Declaration of custom streams used in this rule." }, "nullable": true + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Description of the data collection rule." + } } }, "metadata": { - "__bicep_export!": true, - "description": "The type describing a data disk." + "description": "The type for the properties of the data collection rule of the kind 'All'." } }, - "publicKeyType": { + "agentSettingsDcrPropertiesType": { "type": "object", "properties": { - "keyData": { + "kind": { "type": "string", + "allowedValues": [ + "AgentSettings" + ], "metadata": { - "description": "Required. Specifies the SSH public key data used to authenticate through ssh." + "description": "Required. The kind of the resource." } }, - "path": { + "description": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. Specifies the full path on the created VM where ssh public key is stored. If the file already exists, the specified key is appended to the file." + "description": "Optional. Description of the data collection rule." + } + }, + "agentSettings": { + "$ref": "#/definitions/agentSettingsType", + "metadata": { + "description": "Required. Agent settings used to modify agent behavior on a given host." } } + }, + "metadata": { + "description": "The type for the properties of the 'AgentSettings' data collection rule." } }, - "nicConfigurationType": { + "agentSettingsType": { "type": "object", "properties": { - "name": { - "type": "string", - "nullable": true, + "logs": { + "type": "array", + "items": { + "$ref": "#/definitions/agentSettingType" + }, "metadata": { - "description": "Optional. The name of the NIC configuration." + "description": "Required. All the settings that are applicable to the logs agent (AMA)." } - }, - "nicSuffix": { + } + }, + "metadata": { + "description": "The type for the agent settings." + } + }, + "agentSettingType": { + "type": "object", + "properties": { + "name": { "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The suffix to append to the NIC name." - } - }, - "enableIPForwarding": { - "type": "bool", - "nullable": true, + "allowedValues": [ + "MaxDiskQuotaInMB", + "UseTimeReceivedForForwardedEvents" + ], "metadata": { - "description": "Optional. Indicates whether IP forwarding is enabled on this network interface." + "description": "Required. The name of the agent setting." } }, - "enableAcceleratedNetworking": { - "type": "bool", - "nullable": true, + "value": { + "type": "string", "metadata": { - "description": "Optional. If the network interface is accelerated networking enabled." + "description": "Required. The value of the agent setting." } - }, - "deleteOption": { + } + }, + "metadata": { + "description": "The type for the (single) agent setting." + } + }, + "directDcrPropertiesType": { + "type": "object", + "properties": { + "kind": { "type": "string", "allowedValues": [ - "Delete", - "Detach" + "Direct" ], - "nullable": true, "metadata": { - "description": "Optional. Specify what happens to the network interface when the VM is deleted." + "description": "Required. The kind of the resource." } }, - "dnsServers": { + "dataFlows": { "type": "array", - "items": { - "type": "string" - }, - "nullable": true, "metadata": { - "description": "Optional. List of DNS servers IP addresses. Use 'AzureProvidedDNS' to switch to azure provided DNS resolution. 'AzureProvidedDNS' value cannot be combined with other IPs, it must be the only value in dnsServers collection." + "__bicep_resource_derived_type!": { + "source": "Microsoft.Insights/dataCollectionRules@2024-03-11#properties/properties/properties/dataFlows" + }, + "description": "Required. The specification of data flows." } }, - "networkSecurityGroupResourceId": { + "destinations": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Insights/dataCollectionRules@2024-03-11#properties/properties/properties/destinations" + }, + "description": "Required. Specification of destinations that can be used in data flows." + } + }, + "dataCollectionEndpointResourceId": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The network security group (NSG) to attach to the network interface." + "description": "Optional. The resource ID of the data collection endpoint that this rule can be used with." } }, - "ipConfigurations": { - "type": "array", - "items": { - "$ref": "#/definitions/ipConfigurationType" - }, + "streamDeclarations": { + "type": "object", "metadata": { - "description": "Required. The IP configurations of the network interface." + "__bicep_resource_derived_type!": { + "source": "Microsoft.Insights/dataCollectionRules@2024-03-11#properties/properties/properties/streamDeclarations" + }, + "description": "Required. Declaration of custom streams used in this rule." } }, - "lock": { - "$ref": "#/definitions/lockType", + "description": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. The lock settings of the service." + "description": "Optional. Description of the data collection rule." } - }, - "tags": { - "type": "object", - "nullable": true, + } + }, + "metadata": { + "description": "The type for the properties of the 'Direct' data collection rule." + } + }, + "workspaceTransformsDcrPropertiesType": { + "type": "object", + "properties": { + "kind": { + "type": "string", + "allowedValues": [ + "WorkspaceTransforms" + ], "metadata": { - "description": "Optional. The tags of the public IP address." + "description": "Required. The kind of the resource." } }, - "enableTelemetry": { - "type": "bool", - "nullable": true, + "dataFlows": { + "type": "array", "metadata": { - "description": "Optional. Enable/Disable usage telemetry for the module." + "__bicep_resource_derived_type!": { + "source": "Microsoft.Insights/dataCollectionRules@2024-03-11#properties/properties/properties/dataFlows" + }, + "description": "Required. The specification of data flows. Should include a separate dataflow for each table that will have a transformation. Use a where clause in the query if only certain records should be transformed." } }, - "diagnosticSettings": { - "type": "array", - "items": { - "$ref": "#/definitions/diagnosticSettingFullType" - }, - "nullable": true, + "destinations": { + "type": "object", "metadata": { - "description": "Optional. The diagnostic settings of the IP configuration." + "__bicep_resource_derived_type!": { + "source": "Microsoft.Insights/dataCollectionRules@2024-03-11#properties/properties/properties/destinations" + }, + "description": "Required. Specification of destinations that can be used in data flows. For WorkspaceTransforms, only one Log Analytics workspace destination is supported." } }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, + "description": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Array of role assignments to create." + "description": "Optional. Description of the data collection rule." } } }, "metadata": { - "__bicep_export!": true, - "description": "The type for the NIC configuration." + "description": "The type for the properties of the 'WorkspaceTransforms' data collection rule." } }, - "imageReferenceType": { + "platformTelemetryDcrPropertiesType": { "type": "object", "properties": { - "communityGalleryImageId": { + "kind": { "type": "string", - "nullable": true, + "allowedValues": [ + "PlatformTelemetry" + ], "metadata": { - "description": "Optional. Specified the community gallery image unique id for vm deployment. This can be fetched from community gallery image GET call." + "description": "Required. The kind of the resource." } }, - "id": { + "description": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The resource Id of the image reference." + "description": "Optional. Description of the data collection rule." } }, - "offer": { - "type": "string", - "nullable": true, + "dataSources": { + "type": "object", + "properties": { + "platformTelemetry": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Insights/dataCollectionRules@2024-03-11#properties/properties/properties/dataSources/properties/platformTelemetry" + }, + "description": "Required. The list of platform telemetry configurations." + } + } + }, "metadata": { - "description": "Optional. Specifies the offer of the platform image or marketplace image used to create the virtual machine." + "description": "Required. Specification of data sources that will be collected." } }, - "publisher": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The image publisher." - } - }, - "sku": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The SKU of the image." - } - }, - "version": { - "type": "string", - "nullable": true, + "destinations": { + "type": "object", + "properties": { + "logAnalytics": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Insights/dataCollectionRules@2024-03-11#properties/properties/properties/destinations/properties/logAnalytics" + }, + "description": "Optional. The list of Log Analytics destinations." + }, + "nullable": true + }, + "storageAccounts": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Insights/dataCollectionRules@2024-03-11#properties/properties/properties/destinations/properties/storageAccounts" + }, + "description": "Optional. The list of Storage Account destinations." + }, + "nullable": true + }, + "eventHubs": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Insights/dataCollectionRules@2024-03-11#properties/properties/properties/destinations/properties/eventHubs" + }, + "description": "Optional. The list of Event Hub destinations." + }, + "nullable": true + } + }, "metadata": { - "description": "Optional. Specifies the version of the platform image or marketplace image used to create the virtual machine. The allowed formats are Major.Minor.Build or 'latest'. Even if you use 'latest', the VM image will not automatically update after deploy time even if a new version becomes available." + "description": "Required. Specification of destinations. Choose a single destination type of either logAnalytics, storageAccounts, or eventHubs." } }, - "sharedGalleryImageId": { - "type": "string", - "nullable": true, + "dataFlows": { + "type": "array", "metadata": { - "description": "Optional. Specified the shared gallery image unique id for vm deployment. This can be fetched from shared gallery image GET call." + "__bicep_resource_derived_type!": { + "source": "Microsoft.Insights/dataCollectionRules@2024-03-11#properties/properties/properties/dataFlows" + }, + "description": "Required. The specification of data flows." } } }, "metadata": { - "__bicep_export!": true, - "description": "The type describing the image reference." + "description": "The type for the properties of the 'PlatformTelemetry' data collection rule." } }, - "planType": { + "lockType": { "type": "object", "properties": { "name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The name of the plan." - } - }, - "product": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specifies the product of the image from the marketplace." - } - }, - "publisher": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The publisher ID." + "description": "Optional. Specify the name of lock." } }, - "promotionCode": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The promotion code." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "Specifies information about the marketplace image used to create the virtual machine." - } - }, - "autoShutDownConfigType": { - "type": "object", - "properties": { - "status": { + "kind": { "type": "string", "allowedValues": [ - "Disabled", - "Enabled" + "CanNotDelete", + "None", + "ReadOnly" ], "nullable": true, "metadata": { - "description": "Optional. The status of the auto shutdown configuration." - } - }, - "timeZone": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The time zone ID (e.g. China Standard Time, Greenland Standard Time, Pacific Standard time, etc.)." + "description": "Optional. Specify the type of lock." } }, - "dailyRecurrenceTime": { + "notes": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The time of day the schedule will occur." - } - }, - "notificationSettings": { - "type": "object", - "properties": { - "status": { - "type": "string", - "allowedValues": [ - "Disabled", - "Enabled" - ], - "nullable": true, - "metadata": { - "description": "Optional. The status of the notification settings." - } - }, - "emailRecipient": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The email address to send notifications to (can be a list of semi-colon separated email addresses)." - } - }, - "notificationLocale": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The locale to use when sending a notification (fallback for unsupported languages is EN)." - } - }, - "webhookUrl": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The webhook URL to which the notification will be sent." - } - }, - "timeInMinutes": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. The time in minutes before shutdown to send notifications." - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The resource ID of the schedule." + "description": "Optional. Specify the notes of the lock." } } }, "metadata": { - "__bicep_export!": true, - "description": "The type describing the configuration profile." + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.7.0" + } } }, - "vaultSecretGroupType": { + "managedIdentityAllType": { "type": "object", "properties": { - "sourceVault": { - "$ref": "#/definitions/subResourceType", + "systemAssigned": { + "type": "bool", "nullable": true, "metadata": { - "description": "Optional. The relative URL of the Key Vault containing all of the certificates in VaultCertificates." + "description": "Optional. Enables system assigned managed identity on the resource." } }, - "vaultCertificates": { + "userAssignedResourceIds": { "type": "array", "items": { - "type": "object", - "properties": { - "certificateStore": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. For Windows VMs, specifies the certificate store on the Virtual Machine to which the certificate should be added. The specified certificate store is implicitly in the LocalMachine account. For Linux VMs, the certificate file is placed under the /var/lib/waagent directory, with the file name .crt for the X509 certificate file and .prv for private key. Both of these files are .pem formatted." - } - }, - "certificateUrl": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. This is the URL of a certificate that has been uploaded to Key Vault as a secret." - } - } - } + "type": "string" }, "nullable": true, "metadata": { - "description": "Optional. The list of key vault references in SourceVault which contain certificates." + "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." } } }, "metadata": { - "__bicep_export!": true, - "description": "The type describing the set of certificates that should be installed onto the virtual machine." + "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.7.0" + } } }, - "vmGalleryApplicationType": { + "roleAssignmentType": { "type": "object", "properties": { - "packageReferenceId": { + "name": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. Specifies the GalleryApplicationVersion resource id on the form of /subscriptions/{SubscriptionId}/resourceGroups/{ResourceGroupName}/providers/Microsoft.Compute/galleries/{galleryName}/applications/{application}/versions/{version}." + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." } }, - "configurationReference": { + "roleDefinitionIdOrName": { "type": "string", - "nullable": true, "metadata": { - "description": "Optional. Specifies the uri to an azure blob that will replace the default configuration for the package if provided." + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." } }, - "enableAutomaticUpgrade": { - "type": "bool", - "nullable": true, + "principalId": { + "type": "string", "metadata": { - "description": "Optional. If set to true, when a new Gallery Application version is available in PIR/SIG, it will be automatically updated for the VM/VMSS." + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." } }, - "order": { - "type": "int", + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], "nullable": true, "metadata": { - "description": "Optional. Specifies the order in which the packages have to be installed." + "description": "Optional. The principal type of the assigned principal ID." } }, - "tags": { + "description": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Specifies a passthrough value for more generic context." + "description": "Optional. The description of the role assignment." } }, - "treatFailureAsDeploymentFailure": { - "type": "bool", + "condition": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. If true, any failure for any operation in the VmApplication will fail the deployment." + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type describing the gallery application that should be made available to the VM/VMSS." - } - }, - "additionalUnattendContentType": { - "type": "object", - "properties": { - "settingName": { + }, + "conditionVersion": { "type": "string", "allowedValues": [ - "AutoLogon", - "FirstLogonCommands" + "2.0" ], "nullable": true, "metadata": { - "description": "Optional. Specifies the name of the setting to which the content applies." + "description": "Optional. Version of the condition." } }, - "content": { + "delegatedManagedIdentityResourceId": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Specifies the XML formatted content that is added to the unattend.xml file for the specified path and component. The XML must be less than 4KB and must include the root element for the setting or feature that is being inserted." + "description": "Optional. The Resource Id of the delegated managed identity resource." } } }, "metadata": { - "__bicep_export!": true, - "description": "The type describing additional base-64 encoded XML formatted information that can be included in the Unattend.xml file, which is used by Windows Setup." + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.7.0" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the data collection rule. The name is case insensitive." } }, - "winRMListenerType": { - "type": "object", - "properties": { - "certificateUrl": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The URL of a certificate that has been uploaded to Key Vault as a secret." - } - }, - "protocol": { - "type": "string", - "allowedValues": [ - "Http", - "Https" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specifies the protocol of WinRM listener." - } - } + "dataCollectionRuleProperties": { + "$ref": "#/definitions/dataCollectionRulePropertiesType", + "metadata": { + "description": "Required. The kind of data collection rule." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityAllType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" }, + "nullable": true, "metadata": { - "__bicep_export!": true, - "description": "The type describing a Windows Remote Management listener." + "description": "Optional. Array of role assignments to create." } }, - "nicConfigurationOutputType": { + "tags": { "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the NIC configuration." - } + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Insights/dataCollectionRules@2024-03-11#properties/tags" }, - "ipConfigurations": { - "type": "array", - "items": { - "$ref": "#/definitions/networkInterfaceIPConfigurationOutputType" - }, - "metadata": { - "description": "Required. List of IP configurations of the NIC configuration." - } - } + "description": "Optional. Resource tags." }, - "metadata": { - "__bicep_export!": true, - "description": "The type describing the network interface configuration output." + "nullable": true + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" } + ], + "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned,UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', 'None')), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" }, - "extensionCustomScriptConfigType": { - "type": "object", + "dataCollectionRulePropertiesUnion": "[union(createObject('description', tryGet(parameters('dataCollectionRuleProperties'), 'description')), if(contains(createArray('Linux', 'Windows', 'All', 'PlatformTelemetry'), parameters('dataCollectionRuleProperties').kind), createObject('dataSources', parameters('dataCollectionRuleProperties').dataSources), createObject()), if(contains(createArray('Linux', 'Windows', 'All', 'Direct', 'WorkspaceTransforms', 'PlatformTelemetry'), parameters('dataCollectionRuleProperties').kind), createObject('dataFlows', parameters('dataCollectionRuleProperties').dataFlows, 'destinations', parameters('dataCollectionRuleProperties').destinations), createObject()), if(contains(createArray('Linux', 'Windows', 'All', 'Direct', 'WorkspaceTransforms'), parameters('dataCollectionRuleProperties').kind), createObject('dataCollectionEndpointId', tryGet(parameters('dataCollectionRuleProperties'), 'dataCollectionEndpointResourceId'), 'streamDeclarations', tryGet(parameters('dataCollectionRuleProperties'), 'streamDeclarations')), createObject()), if(equals(parameters('dataCollectionRuleProperties').kind, 'AgentSettings'), createObject('agentSettings', parameters('dataCollectionRuleProperties').agentSettings), createObject()))]", + "enableReferencedModulesTelemetry": false + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('46d3xbcp.res.insights-datacollectionrule.{0}.{1}', replace('0.11.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the virtual machine extension. Defaults to `CustomScriptExtension`." - } - }, - "typeHandlerVersion": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specifies the version of the script handler. Defaults to `1.10` for Windows and `2.1` for Linux." + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } } + } + } + }, + "dataCollectionRule": { + "condition": "[not(equals(parameters('dataCollectionRuleProperties').kind, 'All'))]", + "type": "Microsoft.Insights/dataCollectionRules", + "apiVersion": "2024-03-11", + "name": "[parameters('name')]", + "kind": "[parameters('dataCollectionRuleProperties').kind]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "identity": "[variables('identity')]", + "properties": "[variables('dataCollectionRulePropertiesUnion')]" + }, + "dataCollectionRuleAll": { + "condition": "[equals(parameters('dataCollectionRuleProperties').kind, 'All')]", + "type": "Microsoft.Insights/dataCollectionRules", + "apiVersion": "2024-03-11", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "identity": "[variables('identity')]", + "properties": "[variables('dataCollectionRulePropertiesUnion')]" + }, + "dataCollectionRule_conditionalScopeLock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-DCR-Lock', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" }, - "autoUpgradeMinorVersion": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true. Defaults to `true`." + "mode": "Incremental", + "parameters": { + "dataCollectionRuleName": "[if(equals(parameters('dataCollectionRuleProperties').kind, 'All'), createObject('value', parameters('name')), createObject('value', parameters('name')))]", + "lock": { + "value": "[parameters('lock')]" } }, - "forceUpdateTag": { - "type": "string", - "nullable": true, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", "metadata": { - "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." - } - }, - "settings": { - "type": "object", - "properties": { - "commandToExecute": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Conditional. The entry point script to run. If the command contains any credentials, use the same property of the `protectedSettings` instead. Required if `protectedSettings.commandToExecute` is not provided." - } - }, - "fileUris": { - "type": "array", - "items": { - "type": "string" + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "2876136109547890997" + } + }, + "definitions": { + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } }, - "nullable": true, "metadata": { - "description": "Optional. URLs for files to be downloaded. If URLs are sensitive, for example, if they contain keys, this field should be specified in `protectedSettings`." + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } } } }, - "nullable": true, - "metadata": { - "description": "Optional. The configuration of the custom script extension. Note: You can provide any property either in the `settings` or `protectedSettings` but not both. If your property contains secrets, use `protectedSettings`." - } - }, - "protectedSettings": { - "type": "secureObject", - "properties": { - "commandToExecute": { - "type": "string", + "parameters": { + "lock": { + "$ref": "#/definitions/lockType", "nullable": true, "metadata": { - "description": "Conditional. The entry point script to run. Use this property if your command contains secrets such as passwords or if your file URIs are sensitive. Required if `settings.commandToExecute` is not provided." + "description": "Optional. The lock settings of the service." } }, - "storageAccountName": { + "dataCollectionRuleName": { "type": "string", - "nullable": true, "metadata": { - "description": "Optional. The name of storage account. If you specify storage credentials, all fileUris values must be URLs for Azure blobs.." + "description": "Required. Name of the Data Collection Rule to assign the role(s) to." } + } + }, + "resources": { + "dataCollectionRule": { + "existing": true, + "type": "Microsoft.Insights/dataCollectionRules", + "apiVersion": "2024-03-11", + "name": "[parameters('dataCollectionRuleName')]" }, - "storageAccountKey": { + "dataCollectionRule_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[resourceId('Microsoft.Insights/dataCollectionRules', parameters('dataCollectionRuleName'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('dataCollectionRuleName')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + } + } + } + } + }, + "dependsOn": [ + "dataCollectionRule", + "dataCollectionRuleAll" + ] + }, + "dataCollectionRule_roleAssignments": { + "copy": { + "name": "dataCollectionRule_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-DCR-RoleAssignments-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "resourceId": "[if(equals(parameters('dataCollectionRuleProperties').kind, 'All'), createObject('value', resourceId('Microsoft.Insights/dataCollectionRules', parameters('name'))), createObject('value', resourceId('Microsoft.Insights/dataCollectionRules', parameters('name'))))]", + "name": { + "value": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name')]" + }, + "roleDefinitionId": { + "value": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]" + }, + "principalId": { + "value": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]" + }, + "description": { + "value": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]" + }, + "principalType": { + "value": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.32.4.45862", + "templateHash": "14634305923902101494" + }, + "name": "Resource-scoped role assignment", + "description": "This module deploys a Role Assignment for a specific resource." + }, + "parameters": { + "resourceId": { "type": "string", - "nullable": true, "metadata": { - "description": "Optional. The access key of the storage account." + "description": "Required. The scope for the role assignment, fully qualified resourceId." } }, - "managedIdentityResourceId": { + "name": { "type": "string", - "nullable": true, + "defaultValue": "[guid(parameters('resourceId'), parameters('principalId'), if(contains(parameters('roleDefinitionId'), '/providers/Microsoft.Authorization/roleDefinitions/'), parameters('roleDefinitionId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', parameters('roleDefinitionId'))))]", "metadata": { - "description": "Optional. The managed identity for downloading files. Must not be used in conjunction with the `storageAccountName` or `storageAccountKey` property. If you want to use the VM's system assigned identity, set the `value` to an empty string." + "description": "Optional. The unique guid name for the role assignment." } }, - "fileUris": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, + "roleDefinitionId": { + "type": "string", "metadata": { - "description": "Optional. URLs for files to be downloaded." + "description": "Required. The role definition ID for the role assignment." } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The configuration of the custom script extension. Note: You can provide any property either in the `settings` or `protectedSettings` but not both. If your property contains secrets, use `protectedSettings`." - } - }, - "supressFailures": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). Defaults to `false`." - } - }, - "enableAutomaticUpgrade": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available. Defaults to `false`." - } - }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" }, - "description": "Optional. Tags of the resource." - }, - "nullable": true - }, - "protectedSettingsFromKeyVault": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + "roleName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The name for the role, used for logging." + } }, - "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." - }, - "nullable": true - }, - "provisionAfterExtensions": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The Principal or Object ID of the Security Principal (User, Group, Service Principal, Managed Identity)." + } }, - "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + "principalType": { + "type": "string", + "defaultValue": "", + "allowedValues": [ + "ServicePrincipal", + "Group", + "User", + "ForeignGroup", + "Device", + "" + ], + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The description of role assignment." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } }, - "nullable": true - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type of a 'CustomScriptExtension' extension." - } - }, - "_1.applicationGatewayBackendAddressPoolsType": { - "type": "object", - "properties": { - "id": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the backend address pool." - } - }, - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of the backend address pool that is unique within an Application Gateway." - } - }, - "properties": { - "type": "object", - "properties": { - "backendAddresses": { - "type": "array", - "items": { - "type": "object", - "properties": { - "ipAddress": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. IP address of the backend address." + "variables": { + "$fxv#0": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "scope": { + "type": "string" + }, + "name": { + "type": "string" + }, + "roleDefinitionId": { + "type": "string" + }, + "principalId": { + "type": "string" + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User", + "" + ], + "defaultValue": "", + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[[parameters('scope')]", + "name": "[[parameters('name')]", + "properties": { + "roleDefinitionId": "[[parameters('roleDefinitionId')]", + "principalId": "[[parameters('principalId')]", + "principalType": "[[parameters('principalType')]", + "description": "[[parameters('description')]" + } + } + ], + "outputs": { + "roleAssignmentId": { + "type": "string", + "value": "[[extensionResourceId(parameters('scope'), 'Microsoft.Authorization/roleAssignments', parameters('name'))]" + } + } + } + }, + "resources": [ + { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.ptn.authorization-resourceroleassignment.{0}.{1}', replace('0.1.2', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2023-07-01", + "name": "[format('{0}-ResourceRoleAssignment', guid(parameters('resourceId'), parameters('principalId'), parameters('roleDefinitionId')))]", + "properties": { + "mode": "Incremental", + "expressionEvaluationOptions": { + "scope": "Outer" + }, + "template": "[variables('$fxv#0')]", + "parameters": { + "scope": { + "value": "[parameters('resourceId')]" }, - "fqdn": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. FQDN of the backend address." - } + "name": { + "value": "[parameters('name')]" + }, + "roleDefinitionId": { + "value": "[if(contains(parameters('roleDefinitionId'), '/providers/Microsoft.Authorization/roleDefinitions/'), parameters('roleDefinitionId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', parameters('roleDefinitionId')))]" + }, + "principalId": { + "value": "[parameters('principalId')]" + }, + "principalType": { + "value": "[parameters('principalType')]" + }, + "description": { + "value": "[parameters('description')]" } } + } + } + ], + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The GUID of the Role Assignment." }, - "nullable": true, + "value": "[parameters('name')]" + }, + "roleName": { + "type": "string", "metadata": { - "description": "Optional. Backend addresses." - } + "description": "The name for the role, used for logging." + }, + "value": "[parameters('roleName')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the Role Assignment." + }, + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-ResourceRoleAssignment', guid(parameters('resourceId'), parameters('principalId'), parameters('roleDefinitionId')))), '2023-07-01').outputs.roleAssignmentId.value]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the role assignment was applied at." + }, + "value": "[resourceGroup().name]" } - }, - "nullable": true, - "metadata": { - "description": "Optional. Properties of the application gateway backend address pool." } } }, + "dependsOn": [ + "dataCollectionRule", + "dataCollectionRuleAll" + ] + } + }, + "outputs": { + "name": { + "type": "string", "metadata": { - "description": "The type for the application gateway backend address pool.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" - } + "description": "The name of the dataCollectionRule." + }, + "value": "[if(equals(parameters('dataCollectionRuleProperties').kind, 'All'), parameters('name'), parameters('name'))]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the dataCollectionRule." + }, + "value": "[if(equals(parameters('dataCollectionRuleProperties').kind, 'All'), resourceId('Microsoft.Insights/dataCollectionRules', parameters('name')), resourceId('Microsoft.Insights/dataCollectionRules', parameters('name')))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the dataCollectionRule was created in." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[if(equals(parameters('dataCollectionRuleProperties').kind, 'All'), reference('dataCollectionRuleAll', '2024-03-11', 'full').location, reference('dataCollectionRule', '2024-03-11', 'full').location)]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[if(equals(parameters('dataCollectionRuleProperties').kind, 'All'), tryGet(tryGet(if(equals(parameters('dataCollectionRuleProperties').kind, 'All'), reference('dataCollectionRuleAll', '2024-03-11', 'full'), null()), 'identity'), 'principalId'), tryGet(tryGet(if(not(equals(parameters('dataCollectionRuleProperties').kind, 'All')), reference('dataCollectionRule', '2024-03-11', 'full'), null()), 'identity'), 'principalId'))]" + }, + "endpoints": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Insights/dataCollectionRules@2024-03-11#properties/properties/properties/endpoints", + "output": true + }, + "description": "The endpoints of the dataCollectionRule, if created." + }, + "nullable": true, + "value": "[if(equals(parameters('dataCollectionRuleProperties').kind, 'All'), tryGet(reference('dataCollectionRuleAll'), 'endpoints'), tryGet(reference('dataCollectionRule'), 'endpoints'))]" + }, + "immutableId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The ImmutableId of the dataCollectionRule." + }, + "value": "[if(equals(parameters('dataCollectionRuleProperties').kind, 'All'), tryGet(reference('dataCollectionRuleAll'), 'immutableId'), tryGet(reference('dataCollectionRule'), 'immutableId'))]" + } + } + } + }, + "dependsOn": [ + "existingLogAnalyticsWorkspace", + "logAnalyticsWorkspace" + ] + }, + "jumpboxVM": { + "condition": "[parameters('enablePrivateNetworking')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('avm.res.compute.virtual-machine.{0}', variables('jumpboxVmName')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[take(variables('jumpboxVmName'), 15)]" + }, + "vmSize": { + "value": "[coalesce(parameters('vmSize'), 'Standard_D2s_v5')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "adminUsername": { + "value": "[coalesce(parameters('vmAdminUsername'), 'JumpboxAdminUser')]" + }, + "adminPassword": { + "value": "[coalesce(parameters('vmAdminPassword'), 'JumpboxAdminP@ssw0rd1234!')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "availabilityZone": { + "value": -1 + }, + "imageReference": { + "value": { + "publisher": "microsoft-dsvm", + "offer": "dsvm-win-2022", + "sku": "winserver-2022", + "version": "latest" + } + }, + "osType": { + "value": "Windows" + }, + "osDisk": { + "value": { + "name": "[format('osdisk-{0}', variables('jumpboxVmName'))]", + "managedDisk": { + "storageAccountType": "Standard_LRS" + } + } + }, + "encryptionAtHost": { + "value": false + }, + "nicConfigurations": { + "value": [ + { + "name": "[format('nic-{0}', variables('jumpboxVmName'))]", + "ipConfigurations": [ + { + "name": "ipconfig1", + "subnetResourceId": "[reference('virtualNetwork').outputs.jumpboxSubnetResourceId.value]" + } + ], + "diagnosticSettings": [ + { + "name": "jumpboxDiagnostics", + "workspaceResourceId": "[if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), reference('logAnalyticsWorkspace').outputs.resourceId.value)]", + "logCategoriesAndGroups": [ + { + "categoryGroup": "allLogs", + "enabled": true + } + ], + "metricCategories": [ + { + "category": "AllMetrics", + "enabled": true + } + ] + } + ] } + ] + }, + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" + }, + "extensionMonitoringAgentConfig": "[if(parameters('enableMonitoring'), createObject('value', createObject('dataCollectionRuleAssociations', createArray(createObject('dataCollectionRuleResourceId', reference('windowsVmDataCollectionRules').outputs.resourceId.value, 'name', format('send-{0}', variables('logAnalyticsWorkspaceResourceName')))), 'enabled', true(), 'tags', parameters('tags'))), createObject('value', null()))]" + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "11442373542874951910" }, - "_1.applicationSecurityGroupType": { + "name": "Virtual Machines", + "description": "This module deploys a Virtual Machine with one or multiple NICs and optionally one or multiple public IPs." + }, + "definitions": { + "osDiskType": { "type": "object", "properties": { - "id": { + "name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Resource ID of the application security group." + "description": "Optional. The disk name." } }, - "location": { - "type": "string", + "diskSizeGB": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. Location of the application security group." + "description": "Optional. Specifies the size of an empty data disk in gigabytes." } }, - "properties": { - "type": "object", + "createOption": { + "type": "string", + "allowedValues": [ + "Attach", + "Empty", + "FromImage" + ], "nullable": true, "metadata": { - "description": "Optional. Properties of the application security group." + "description": "Optional. Specifies how the virtual machine should be created." } }, - "tags": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Tags of the application security group." - } - } - }, - "metadata": { - "description": "The type for the application security group.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" - } - } - }, - "_1.backendAddressPoolType": { - "type": "object", - "properties": { - "id": { + "deleteOption": { "type": "string", + "allowedValues": [ + "Delete", + "Detach" + ], "nullable": true, "metadata": { - "description": "Optional. The resource ID of the backend address pool." + "description": "Optional. Specifies whether data disk should be deleted or detached upon VM deletion." } }, - "name": { + "caching": { "type": "string", + "allowedValues": [ + "None", + "ReadOnly", + "ReadWrite" + ], "nullable": true, "metadata": { - "description": "Optional. The name of the backend address pool." + "description": "Optional. Specifies the caching requirements." } }, - "properties": { + "diffDiskSettings": { "type": "object", + "properties": { + "placement": { + "type": "string", + "allowedValues": [ + "CacheDisk", + "NvmeDisk", + "ResourceDisk" + ], + "metadata": { + "description": "Required. Specifies the ephemeral disk placement for the operating system disk." + } + } + }, "nullable": true, "metadata": { - "description": "Optional. The properties of the backend address pool." - } - } - }, - "metadata": { - "description": "The type for a backend address pool.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" - } - } - }, - "_1.inboundNatRuleType": { - "type": "object", - "properties": { - "id": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the inbound NAT rule." - } - }, - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of the resource that is unique within the set of inbound NAT rules used by the load balancer. This name can be used to access the resource." + "description": "Optional. Specifies the ephemeral Disk Settings for the operating system disk." } }, - "properties": { + "managedDisk": { "type": "object", "properties": { - "backendAddressPool": { - "$ref": "#/definitions/subResourceType", - "nullable": true, - "metadata": { - "description": "Optional. A reference to backendAddressPool resource." - } - }, - "backendPort": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. The port used for the internal endpoint. Acceptable values range from 1 to 65535." - } - }, - "enableFloatingIP": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Configures a virtual machine's endpoint for the floating IP capability required to configure a SQL AlwaysOn Availability Group. This setting is required when using the SQL AlwaysOn Availability Groups in SQL server. This setting can't be changed after you create the endpoint." - } - }, - "enableTcpReset": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Receive bidirectional TCP Reset on TCP flow idle timeout or unexpected connection termination. This element is only used when the protocol is set to TCP." - } - }, - "frontendIPConfiguration": { - "$ref": "#/definitions/subResourceType", - "nullable": true, - "metadata": { - "description": "Optional. A reference to frontend IP addresses." - } - }, - "frontendPort": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. The port for the external endpoint. Port numbers for each rule must be unique within the Load Balancer. Acceptable values range from 1 to 65534." - } - }, - "frontendPortRangeStart": { - "type": "int", + "storageAccountType": { + "type": "string", + "allowedValues": [ + "PremiumV2_LRS", + "Premium_LRS", + "Premium_ZRS", + "StandardSSD_LRS", + "StandardSSD_ZRS", + "Standard_LRS", + "UltraSSD_LRS" + ], "nullable": true, "metadata": { - "description": "Optional. The port range start for the external endpoint. This property is used together with BackendAddressPool and FrontendPortRangeEnd. Individual inbound NAT rule port mappings will be created for each backend address from BackendAddressPool. Acceptable values range from 1 to 65534." + "description": "Optional. Specifies the storage account type for the managed disk." } }, - "frontendPortRangeEnd": { - "type": "int", + "diskEncryptionSetResourceId": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. The port range end for the external endpoint. This property is used together with BackendAddressPool and FrontendPortRangeStart. Individual inbound NAT rule port mappings will be created for each backend address from BackendAddressPool. Acceptable values range from 1 to 65534." + "description": "Optional. Specifies the customer managed disk encryption set resource id for the managed disk." } }, - "protocol": { + "resourceId": { "type": "string", - "allowedValues": [ - "All", - "Tcp", - "Udp" - ], "nullable": true, "metadata": { - "description": "Optional. The reference to the transport protocol used by the load balancing rule." + "description": "Optional. Specifies the resource id of a pre-existing managed disk. If the disk should be created, this property should be empty." } } }, - "nullable": true, "metadata": { - "description": "Optional. Properties of the inbound NAT rule." + "description": "Required. The managed disk parameters." } } }, "metadata": { - "description": "The type for the inbound NAT rule.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" - } + "__bicep_export!": true, + "description": "The type describing an OS disk." } }, - "_1.virtualNetworkTapType": { + "dataDiskType": { "type": "object", "properties": { - "id": { + "name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Resource ID of the virtual network tap." + "description": "Optional. The disk name. When attaching a pre-existing disk, this name is ignored and the name of the existing disk is used." } }, - "location": { - "type": "string", + "lun": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. Location of the virtual network tap." + "description": "Optional. Specifies the logical unit number of the data disk." } }, - "properties": { - "type": "object", + "diskSizeGB": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. Properties of the virtual network tap." + "description": "Optional. Specifies the size of an empty data disk in gigabytes. This property is ignored when attaching a pre-existing disk." } }, - "tags": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Tags of the virtual network tap." - } - } - }, - "metadata": { - "description": "The type for the virtual network tap.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" - } - } - }, - "_2.ddosSettingsType": { - "type": "object", - "properties": { - "ddosProtectionPlan": { - "type": "object", - "properties": { - "id": { - "type": "string", - "metadata": { - "description": "Required. The resource ID of the DDOS protection plan associated with the public IP address." - } - } - }, + "createOption": { + "type": "string", + "allowedValues": [ + "Attach", + "Empty", + "FromImage" + ], "nullable": true, "metadata": { - "description": "Optional. The DDoS protection plan associated with the public IP address." + "description": "Optional. Specifies how the virtual machine should be created. This property is automatically set to 'Attach' when attaching a pre-existing disk." } }, - "protectionMode": { + "deleteOption": { "type": "string", "allowedValues": [ - "Enabled" + "Delete", + "Detach" ], + "nullable": true, "metadata": { - "description": "Required. The DDoS protection policy customizations." - } - } - }, - "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.8.0" - } - } - }, - "_2.dnsSettingsType": { - "type": "object", - "properties": { - "domainNameLabel": { - "type": "string", - "metadata": { - "description": "Required. The domain name label. The concatenation of the domain name label and the regionalized DNS zone make up the fully qualified domain name associated with the public IP address. If a domain name label is specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system." + "description": "Optional. Specifies whether data disk should be deleted or detached upon VM deletion. This property is automatically set to 'Detach' when attaching a pre-existing disk." } }, - "domainNameLabelScope": { + "caching": { "type": "string", "allowedValues": [ - "NoReuse", - "ResourceGroupReuse", - "SubscriptionReuse", - "TenantReuse" + "None", + "ReadOnly", + "ReadWrite" ], "nullable": true, "metadata": { - "description": "Optional. The domain name label scope. If a domain name label and a domain name label scope are specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system with a hashed value includes in FQDN." + "description": "Optional. Specifies the caching requirements. This property is automatically set to 'None' when attaching a pre-existing disk." } }, - "fqdn": { - "type": "string", + "diskIOPSReadWrite": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. The Fully Qualified Domain Name of the A DNS record associated with the public IP. This is the concatenation of the domainNameLabel and the regionalized DNS zone." + "description": "Optional. The number of IOPS allowed for this disk; only settable for UltraSSD disks. One operation can transfer between 4k and 256k bytes. Ignored when attaching a pre-existing disk." } }, - "reverseFqdn": { - "type": "string", + "diskMBpsReadWrite": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. The reverse FQDN. A user-visible, fully qualified domain name that resolves to this public IP address. If the reverseFqdn is specified, then a PTR DNS record is created pointing from the IP address in the in-addr.arpa domain to the reverse FQDN." + "description": "Optional. The bandwidth allowed for this disk; only settable for UltraSSD disks. MBps means millions of bytes per second - MB here uses the ISO notation, of powers of 10. Ignored when attaching a pre-existing disk." + } + }, + "managedDisk": { + "type": "object", + "properties": { + "storageAccountType": { + "type": "string", + "allowedValues": [ + "PremiumV2_LRS", + "Premium_LRS", + "Premium_ZRS", + "StandardSSD_LRS", + "StandardSSD_ZRS", + "Standard_LRS", + "UltraSSD_LRS" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies the storage account type for the managed disk. Ignored when attaching a pre-existing disk." + } + }, + "diskEncryptionSetResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the customer managed disk encryption set resource id for the managed disk." + } + }, + "resourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the resource id of a pre-existing managed disk. If the disk should be created, this property should be empty." + } + } + }, + "metadata": { + "description": "Required. The managed disk parameters." } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/disks@2025-01-02#properties/tags" + }, + "description": "Optional. The tags of the public IP address. Valid only when creating a new managed disk." + }, + "nullable": true } }, "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.8.0" - } + "__bicep_export!": true, + "description": "The type describing a data disk." } }, - "_2.ipTagType": { + "publicKeyType": { "type": "object", "properties": { - "ipTagType": { + "keyData": { "type": "string", "metadata": { - "description": "Required. The IP tag type." + "description": "Required. Specifies the SSH public key data used to authenticate through ssh." } }, - "tag": { + "path": { "type": "string", "metadata": { - "description": "Required. The IP tag." + "description": "Required. Specifies the full path on the created VM where ssh public key is stored. If the file already exists, the specified key is appended to the file." } } - }, - "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.8.0" - } } }, - "_3.diagnosticSettingFullType": { + "nicConfigurationType": { "type": "object", "properties": { "name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The name of the diagnostic setting." + "description": "Optional. The name of the NIC configuration." } }, - "logCategoriesAndGroups": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." - } - }, - "categoryGroup": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, + "nicSuffix": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + "description": "Optional. The suffix to append to the NIC name." } }, - "metricCategories": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "metadata": { - "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, + "enableIPForwarding": { + "type": "bool", "nullable": true, "metadata": { - "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + "description": "Optional. Indicates whether IP forwarding is enabled on this network interface." } }, - "logAnalyticsDestinationType": { + "enableAcceleratedNetworking": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. If the network interface is accelerated networking enabled." + } + }, + "deleteOption": { "type": "string", "allowedValues": [ - "AzureDiagnostics", - "Dedicated" + "Delete", + "Detach" ], "nullable": true, "metadata": { - "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + "description": "Optional. Specify what happens to the network interface when the VM is deleted." } }, - "workspaceResourceId": { - "type": "string", + "dnsServers": { + "type": "array", + "items": { + "type": "string" + }, "nullable": true, "metadata": { - "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + "description": "Optional. List of DNS servers IP addresses. Use 'AzureProvidedDNS' to switch to azure provided DNS resolution. 'AzureProvidedDNS' value cannot be combined with other IPs, it must be the only value in dnsServers collection." } }, - "storageAccountResourceId": { + "networkSecurityGroupResourceId": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + "description": "Optional. The network security group (NSG) to attach to the network interface." } }, - "eventHubAuthorizationRuleResourceId": { - "type": "string", - "nullable": true, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/ipConfigurationType" + }, "metadata": { - "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + "description": "Required. The IP configurations of the network interface." } }, - "eventHubName": { - "type": "string", + "lock": { + "$ref": "#/definitions/lockType", "nullable": true, "metadata": { - "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + "description": "Optional. The lock settings of the service." } }, - "marketplacePartnerResourceId": { - "type": "string", + "tags": { + "type": "object", "nullable": true, "metadata": { - "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + "description": "Optional. The tags of the public IP address." } - } - }, - "metadata": { - "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "_3.lockType": { - "type": "object", - "properties": { - "name": { - "type": "string", + }, + "enableTelemetry": { + "type": "bool", "nullable": true, "metadata": { - "description": "Optional. Specify the name of lock." + "description": "Optional. Enable/Disable usage telemetry for the module." } }, - "kind": { - "type": "string", - "allowedValues": [ - "CanNotDelete", - "None", - "ReadOnly" - ], + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, "nullable": true, "metadata": { - "description": "Optional. Specify the type of lock." + "description": "Optional. The diagnostic settings of the IP configuration." } }, - "notes": { - "type": "string", + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, "nullable": true, "metadata": { - "description": "Optional. Specify the notes of the lock." + "description": "Optional. Array of role assignments to create." } } }, "metadata": { - "description": "An AVM-aligned type for a lock.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } + "__bicep_export!": true, + "description": "The type for the NIC configuration." } }, - "_3.roleAssignmentType": { + "imageReferenceType": { "type": "object", "properties": { - "name": { + "communityGalleryImageId": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + "description": "Optional. Specified the community gallery image unique id for vm deployment. This can be fetched from community gallery image GET call." } }, - "principalId": { + "id": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + "description": "Optional. The resource Id of the image reference." } }, - "principalType": { + "offer": { "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], "nullable": true, "metadata": { - "description": "Optional. The principal type of the assigned principal ID." + "description": "Optional. Specifies the offer of the platform image or marketplace image used to create the virtual machine." } }, - "description": { + "publisher": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The description of the role assignment." + "description": "Optional. The image publisher." } }, - "condition": { + "sku": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + "description": "Optional. The SKU of the image." } }, - "conditionVersion": { + "version": { "type": "string", - "allowedValues": [ - "2.0" - ], "nullable": true, "metadata": { - "description": "Optional. Version of the condition." + "description": "Optional. Specifies the version of the platform image or marketplace image used to create the virtual machine. The allowed formats are Major.Minor.Build or 'latest'. Even if you use 'latest', the VM image will not automatically update after deploy time even if a new version becomes available." } }, - "delegatedManagedIdentityResourceId": { + "sharedGalleryImageId": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." + "description": "Optional. Specified the shared gallery image unique id for vm deployment. This can be fetched from shared gallery image GET call." } } }, "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } + "__bicep_export!": true, + "description": "The type describing the image reference." } }, - "_4.publicIPConfigurationType": { + "planType": { "type": "object", "properties": { "name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The name of the Public IP Address." + "description": "Optional. The name of the plan." } }, - "publicIPAddressResourceId": { + "product": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The resource ID of the public IP address." + "description": "Optional. Specifies the product of the image from the marketplace." } }, - "diagnosticSettings": { - "type": "array", - "items": { - "$ref": "#/definitions/_3.diagnosticSettingFullType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Diagnostic settings for the public IP address." - } - }, - "location": { + "publisher": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The idle timeout in minutes." - } - }, - "lock": { - "$ref": "#/definitions/_3.lockType", - "nullable": true, - "metadata": { - "description": "Optional. The lock settings of the public IP address." - } - }, - "idleTimeoutInMinutes": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. The idle timeout of the public IP address." - } - }, - "ddosSettings": { - "$ref": "#/definitions/_2.ddosSettingsType", - "nullable": true, - "metadata": { - "description": "Optional. The DDoS protection plan configuration associated with the public IP address." - } - }, - "dnsSettings": { - "$ref": "#/definitions/_2.dnsSettingsType", - "nullable": true, - "metadata": { - "description": "Optional. The DNS settings of the public IP address." + "description": "Optional. The publisher ID." } }, - "publicIPAddressVersion": { + "promotionCode": { "type": "string", - "allowedValues": [ - "IPv4", - "IPv6" - ], "nullable": true, "metadata": { - "description": "Optional. The public IP address version." + "description": "Optional. The promotion code." } - }, - "publicIPAllocationMethod": { + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Specifies information about the marketplace image used to create the virtual machine." + } + }, + "autoShutDownConfigType": { + "type": "object", + "properties": { + "status": { "type": "string", "allowedValues": [ - "Dynamic", - "Static" + "Disabled", + "Enabled" ], "nullable": true, "metadata": { - "description": "Optional. The public IP address allocation method." - } - }, - "publicIpPrefixResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the Public IP Prefix object. This is only needed if you want your Public IPs created in a PIP Prefix." - } - }, - "publicIpNameSuffix": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name suffix of the public IP address resource." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/_3.roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." + "description": "Optional. The status of the auto shutdown configuration." } }, - "skuName": { + "timeZone": { "type": "string", - "allowedValues": [ - "Basic", - "Standard" - ], "nullable": true, "metadata": { - "description": "Optional. The SKU name of the public IP address." + "description": "Optional. The time zone ID (e.g. China Standard Time, Greenland Standard Time, Pacific Standard time, etc.)." } }, - "skuTier": { + "dailyRecurrenceTime": { "type": "string", - "allowedValues": [ - "Global", - "Regional" - ], "nullable": true, "metadata": { - "description": "Optional. The SKU tier of the public IP address." + "description": "Optional. The time of day the schedule will occur." } }, - "tags": { + "notificationSettings": { "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/publicIPAddresses@2024-07-01#properties/tags" + "properties": { + "status": { + "type": "string", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "nullable": true, + "metadata": { + "description": "Optional. The status of the notification settings." + } }, - "description": "Optional. The tags of the public IP address." - }, - "nullable": true - }, - "availabilityZones": { - "type": "array", - "allowedValues": [ - 1, - 2, - 3 - ], - "nullable": true, - "metadata": { - "description": "Optional. The zones of the public IP address." - } - }, - "ipTags": { - "type": "array", - "items": { - "$ref": "#/definitions/_2.ipTagType" + "emailRecipient": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The email address to send notifications to (can be a list of semi-colon separated email addresses)." + } + }, + "notificationLocale": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The locale to use when sending a notification (fallback for unsupported languages is EN)." + } + }, + "webhookUrl": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The webhook URL to which the notification will be sent." + } + }, + "timeInMinutes": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The time in minutes before shutdown to send notifications." + } + } }, "nullable": true, "metadata": { - "description": "Optional. The list of tags associated with the public IP address." - } - }, - "enableTelemetry": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for the module." + "description": "Optional. The resource ID of the schedule." } } }, "metadata": { - "description": "The type for the public IP address configuration.", - "__bicep_imported_from!": { - "sourceTemplate": "modules/nic-configuration.bicep" - } + "__bicep_export!": true, + "description": "The type describing the configuration profile." } }, - "diagnosticSettingFullType": { + "vaultSecretGroupType": { "type": "object", "properties": { - "name": { - "type": "string", + "sourceVault": { + "$ref": "#/definitions/subResourceType", "nullable": true, "metadata": { - "description": "Optional. The name of the diagnostic setting." + "description": "Optional. The relative URL of the Key Vault containing all of the certificates in VaultCertificates." } }, - "logCategoriesAndGroups": { + "vaultCertificates": { "type": "array", "items": { "type": "object", "properties": { - "category": { + "certificateStore": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + "description": "Optional. For Windows VMs, specifies the certificate store on the Virtual Machine to which the certificate should be added. The specified certificate store is implicitly in the LocalMachine account. For Linux VMs, the certificate file is placed under the /var/lib/waagent directory, with the file name .crt for the X509 certificate file and .prv for private key. Both of these files are .pem formatted." } }, - "categoryGroup": { + "certificateUrl": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." + "description": "Optional. This is the URL of a certificate that has been uploaded to Key Vault as a secret." } } } }, "nullable": true, "metadata": { - "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + "description": "Optional. The list of key vault references in SourceVault which contain certificates." } - }, - "metricCategories": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "metadata": { - "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, - "nullable": true, + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type describing the set of certificates that should be installed onto the virtual machine." + } + }, + "vmGalleryApplicationType": { + "type": "object", + "properties": { + "packageReferenceId": { + "type": "string", "metadata": { - "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + "description": "Required. Specifies the GalleryApplicationVersion resource id on the form of /subscriptions/{SubscriptionId}/resourceGroups/{ResourceGroupName}/providers/Microsoft.Compute/galleries/{galleryName}/applications/{application}/versions/{version}." } }, - "logAnalyticsDestinationType": { + "configurationReference": { "type": "string", - "allowedValues": [ - "AzureDiagnostics", - "Dedicated" - ], "nullable": true, "metadata": { - "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + "description": "Optional. Specifies the uri to an azure blob that will replace the default configuration for the package if provided." } }, - "workspaceResourceId": { - "type": "string", + "enableAutomaticUpgrade": { + "type": "bool", "nullable": true, "metadata": { - "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + "description": "Optional. If set to true, when a new Gallery Application version is available in PIR/SIG, it will be automatically updated for the VM/VMSS." } }, - "storageAccountResourceId": { - "type": "string", + "order": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + "description": "Optional. Specifies the order in which the packages have to be installed." } }, - "eventHubAuthorizationRuleResourceId": { + "tags": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + "description": "Optional. Specifies a passthrough value for more generic context." } }, - "eventHubName": { + "treatFailureAsDeploymentFailure": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. If true, any failure for any operation in the VmApplication will fail the deployment." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type describing the gallery application that should be made available to the VM/VMSS." + } + }, + "additionalUnattendContentType": { + "type": "object", + "properties": { + "settingName": { "type": "string", + "allowedValues": [ + "AutoLogon", + "FirstLogonCommands" + ], "nullable": true, "metadata": { - "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + "description": "Optional. Specifies the name of the setting to which the content applies." } }, - "marketplacePartnerResourceId": { + "content": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + "description": "Optional. Specifies the XML formatted content that is added to the unattend.xml file for the specified path and component. The XML must be less than 4KB and must include the root element for the setting or feature that is being inserted." } } }, "metadata": { - "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } + "__bicep_export!": true, + "description": "The type describing additional base-64 encoded XML formatted information that can be included in the Unattend.xml file, which is used by Windows Setup." } }, - "ipConfigurationType": { + "winRMListenerType": { "type": "object", "properties": { - "name": { + "certificateUrl": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The name of the IP configuration." + "description": "Optional. The URL of a certificate that has been uploaded to Key Vault as a secret." } }, - "privateIPAllocationMethod": { + "protocol": { "type": "string", "allowedValues": [ - "Dynamic", - "Static" + "Http", + "Https" ], "nullable": true, "metadata": { - "description": "Optional. The private IP address allocation method." - } - }, - "privateIPAddress": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The private IP address." + "description": "Optional. Specifies the protocol of WinRM listener." } - }, - "subnetResourceId": { + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type describing a Windows Remote Management listener." + } + }, + "nicConfigurationOutputType": { + "type": "object", + "properties": { + "name": { "type": "string", "metadata": { - "description": "Required. The resource ID of the subnet." + "description": "Required. The name of the NIC configuration." } }, - "loadBalancerBackendAddressPools": { + "ipConfigurations": { "type": "array", "items": { - "$ref": "#/definitions/_1.backendAddressPoolType" + "$ref": "#/definitions/networkInterfaceIPConfigurationOutputType" }, - "nullable": true, "metadata": { - "description": "Optional. The load balancer backend address pools." + "description": "Required. List of IP configurations of the NIC configuration." } - }, - "applicationSecurityGroups": { - "type": "array", - "items": { - "$ref": "#/definitions/_1.applicationSecurityGroupType" - }, + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type describing the network interface configuration output." + } + }, + "extensionCustomScriptConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. The application security groups." + "description": "Optional. The name of the virtual machine extension. Defaults to `CustomScriptExtension`." } }, - "applicationGatewayBackendAddressPools": { - "type": "array", - "items": { - "$ref": "#/definitions/_1.applicationGatewayBackendAddressPoolsType" - }, + "typeHandlerVersion": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. The application gateway backend address pools." + "description": "Optional. Specifies the version of the script handler. Defaults to `1.10` for Windows and `2.1` for Linux." } }, - "gatewayLoadBalancer": { - "$ref": "#/definitions/subResourceType", + "autoUpgradeMinorVersion": { + "type": "bool", "nullable": true, "metadata": { - "description": "Optional. The gateway load balancer settings." + "description": "Optional. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true. Defaults to `true`." } }, - "loadBalancerInboundNatRules": { - "type": "array", - "items": { - "$ref": "#/definitions/_1.inboundNatRuleType" - }, + "forceUpdateTag": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. The load balancer inbound NAT rules." + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." } }, - "privateIPAddressVersion": { - "type": "string", - "allowedValues": [ - "IPv4", - "IPv6" - ], + "settings": { + "type": "object", + "properties": { + "commandToExecute": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Conditional. The entry point script to run. If the command contains any credentials, use the same property of the `protectedSettings` instead. Required if `protectedSettings.commandToExecute` is not provided." + } + }, + "fileUris": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. URLs for files to be downloaded. If URLs are sensitive, for example, if they contain keys, this field should be specified in `protectedSettings`." + } + } + }, "nullable": true, "metadata": { - "description": "Optional. The private IP address version." + "description": "Optional. The configuration of the custom script extension. Note: You can provide any property either in the `settings` or `protectedSettings` but not both. If your property contains secrets, use `protectedSettings`." } }, - "virtualNetworkTaps": { - "type": "array", - "items": { - "$ref": "#/definitions/_1.virtualNetworkTapType" + "protectedSettings": { + "type": "secureObject", + "properties": { + "commandToExecute": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Conditional. The entry point script to run. Use this property if your command contains secrets such as passwords or if your file URIs are sensitive. Required if `settings.commandToExecute` is not provided." + } + }, + "storageAccountName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of storage account. If you specify storage credentials, all fileUris values must be URLs for Azure blobs.." + } + }, + "storageAccountKey": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The access key of the storage account." + } + }, + "managedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity for downloading files. Must not be used in conjunction with the `storageAccountName` or `storageAccountKey` property. If you want to use the VM's system assigned identity, set the `value` to an empty string." + } + }, + "fileUris": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. URLs for files to be downloaded." + } + } }, "nullable": true, "metadata": { - "description": "Optional. The virtual network taps." + "description": "Optional. The configuration of the custom script extension. Note: You can provide any property either in the `settings` or `protectedSettings` but not both. If your property contains secrets, use `protectedSettings`." } }, - "pipConfiguration": { - "$ref": "#/definitions/_4.publicIPConfigurationType", + "supressFailures": { + "type": "bool", "nullable": true, "metadata": { - "description": "Optional. The public IP address configuration." + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). Defaults to `false`." } }, - "diagnosticSettings": { - "type": "array", - "items": { - "$ref": "#/definitions/_3.diagnosticSettingFullType" - }, + "enableAutomaticUpgrade": { + "type": "bool", "nullable": true, "metadata": { - "description": "Optional. The diagnostic settings of the IP configuration." + "description": "Optional. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available. Defaults to `false`." } }, "tags": { "type": "object", "metadata": { "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/networkInterfaces@2024-07-01#properties/tags" + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" }, - "description": "Optional. The tags of the public IP address." + "description": "Optional. Tags of the resource." }, "nullable": true }, - "enableTelemetry": { - "type": "bool", - "nullable": true, + "protectedSettingsFromKeyVault": { + "type": "object", "metadata": { - "description": "Optional. Enable/Disable usage telemetry for the module." - } + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + }, + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + }, + "nullable": true + }, + "provisionAfterExtensions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + }, + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true } }, "metadata": { - "description": "The type for the IP configuration.", - "__bicep_imported_from!": { - "sourceTemplate": "modules/nic-configuration.bicep" + "__bicep_export!": true, + "description": "The type of a 'CustomScriptExtension' extension." + } + }, + "_1.applicationGatewayBackendAddressPoolsType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the backend address pool." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the backend address pool that is unique within an Application Gateway." + } + }, + "properties": { + "type": "object", + "properties": { + "backendAddresses": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ipAddress": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. IP address of the backend address." + } + }, + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN of the backend address." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Backend addresses." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Properties of the application gateway backend address pool." + } + } + }, + "metadata": { + "description": "The type for the application gateway backend address pool.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" } } }, - "lockType": { + "_1.applicationSecurityGroupType": { "type": "object", "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the application security group." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Location of the application security group." + } + }, + "properties": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Properties of the application security group." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the application security group." + } + } + }, + "metadata": { + "description": "The type for the application security group.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } + }, + "_1.backendAddressPoolType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the backend address pool." + } + }, "name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Specify the name of lock." + "description": "Optional. The name of the backend address pool." } }, - "kind": { + "properties": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. The properties of the backend address pool." + } + } + }, + "metadata": { + "description": "The type for a backend address pool.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } + }, + "_1.inboundNatRuleType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the inbound NAT rule." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the resource that is unique within the set of inbound NAT rules used by the load balancer. This name can be used to access the resource." + } + }, + "properties": { + "type": "object", + "properties": { + "backendAddressPool": { + "$ref": "#/definitions/subResourceType", + "nullable": true, + "metadata": { + "description": "Optional. A reference to backendAddressPool resource." + } + }, + "backendPort": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port used for the internal endpoint. Acceptable values range from 1 to 65535." + } + }, + "enableFloatingIP": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Configures a virtual machine's endpoint for the floating IP capability required to configure a SQL AlwaysOn Availability Group. This setting is required when using the SQL AlwaysOn Availability Groups in SQL server. This setting can't be changed after you create the endpoint." + } + }, + "enableTcpReset": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Receive bidirectional TCP Reset on TCP flow idle timeout or unexpected connection termination. This element is only used when the protocol is set to TCP." + } + }, + "frontendIPConfiguration": { + "$ref": "#/definitions/subResourceType", + "nullable": true, + "metadata": { + "description": "Optional. A reference to frontend IP addresses." + } + }, + "frontendPort": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port for the external endpoint. Port numbers for each rule must be unique within the Load Balancer. Acceptable values range from 1 to 65534." + } + }, + "frontendPortRangeStart": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port range start for the external endpoint. This property is used together with BackendAddressPool and FrontendPortRangeEnd. Individual inbound NAT rule port mappings will be created for each backend address from BackendAddressPool. Acceptable values range from 1 to 65534." + } + }, + "frontendPortRangeEnd": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port range end for the external endpoint. This property is used together with BackendAddressPool and FrontendPortRangeStart. Individual inbound NAT rule port mappings will be created for each backend address from BackendAddressPool. Acceptable values range from 1 to 65534." + } + }, + "protocol": { + "type": "string", + "allowedValues": [ + "All", + "Tcp", + "Udp" + ], + "nullable": true, + "metadata": { + "description": "Optional. The reference to the transport protocol used by the load balancing rule." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Properties of the inbound NAT rule." + } + } + }, + "metadata": { + "description": "The type for the inbound NAT rule.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } + }, + "_1.virtualNetworkTapType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the virtual network tap." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Location of the virtual network tap." + } + }, + "properties": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Properties of the virtual network tap." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the virtual network tap." + } + } + }, + "metadata": { + "description": "The type for the virtual network tap.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } + }, + "_2.ddosSettingsType": { + "type": "object", + "properties": { + "ddosProtectionPlan": { + "type": "object", + "properties": { + "id": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the DDOS protection plan associated with the public IP address." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The DDoS protection plan associated with the public IP address." + } + }, + "protectionMode": { "type": "string", "allowedValues": [ - "CanNotDelete", - "None", - "ReadOnly" + "Enabled" + ], + "metadata": { + "description": "Required. The DDoS protection policy customizations." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.8.0" + } + } + }, + "_2.dnsSettingsType": { + "type": "object", + "properties": { + "domainNameLabel": { + "type": "string", + "metadata": { + "description": "Required. The domain name label. The concatenation of the domain name label and the regionalized DNS zone make up the fully qualified domain name associated with the public IP address. If a domain name label is specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system." + } + }, + "domainNameLabelScope": { + "type": "string", + "allowedValues": [ + "NoReuse", + "ResourceGroupReuse", + "SubscriptionReuse", + "TenantReuse" ], "nullable": true, "metadata": { - "description": "Optional. Specify the type of lock." + "description": "Optional. The domain name label scope. If a domain name label and a domain name label scope are specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system with a hashed value includes in FQDN." } }, - "notes": { + "fqdn": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Specify the notes of the lock." + "description": "Optional. The Fully Qualified Domain Name of the A DNS record associated with the public IP. This is the concatenation of the domainNameLabel and the regionalized DNS zone." + } + }, + "reverseFqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The reverse FQDN. A user-visible, fully qualified domain name that resolves to this public IP address. If the reverseFqdn is specified, then a PTR DNS record is created pointing from the IP address in the in-addr.arpa domain to the reverse FQDN." } } }, "metadata": { - "description": "An AVM-aligned type for a lock.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.8.0" } } }, - "managedIdentityAllType": { + "_2.ipTagType": { "type": "object", "properties": { - "systemAssigned": { - "type": "bool", + "ipTagType": { + "type": "string", + "metadata": { + "description": "Required. The IP tag type." + } + }, + "tag": { + "type": "string", + "metadata": { + "description": "Required. The IP tag." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.8.0" + } + } + }, + "_3.diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Enables system assigned managed identity on the resource." + "description": "Optional. The name of the diagnostic setting." } }, - "userAssignedResourceIds": { + "logCategoriesAndGroups": { "type": "array", "items": { - "type": "string" + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } }, "nullable": true, "metadata": { - "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." } } }, "metadata": { - "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" } } }, - "networkInterfaceIPConfigurationOutputType": { + "_3.lockType": { "type": "object", "properties": { "name": { "type": "string", + "nullable": true, "metadata": { - "description": "The name of the IP configuration." + "description": "Optional. Specify the name of lock." } }, - "privateIP": { + "kind": { "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], "nullable": true, "metadata": { - "description": "The private IP address." + "description": "Optional. Specify the type of lock." } }, - "publicIP": { + "notes": { "type": "string", "nullable": true, "metadata": { - "description": "The public IP address." + "description": "Optional. Specify the notes of the lock." } } }, "metadata": { + "description": "An AVM-aligned type for a lock.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" } } }, - "roleAssignmentType": { + "_3.roleAssignmentType": { "type": "object", "properties": { "name": { @@ -11183,77 +11894,690 @@ "metadata": { "description": "An AVM-aligned type for a role assignment.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" } } }, - "subResourceType": { + "_4.publicIPConfigurationType": { "type": "object", "properties": { - "id": { + "name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Resource ID of the sub resource." + "description": "Optional. The name of the Public IP Address." } - } - }, - "metadata": { - "description": "The type for the sub resource.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" - } - } - } - }, - "parameters": { - "name": { - "type": "string", + }, + "publicIPAddressResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the public IP address." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/_3.diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Diagnostic settings for the public IP address." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The idle timeout in minutes." + } + }, + "lock": { + "$ref": "#/definitions/_3.lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the public IP address." + } + }, + "idleTimeoutInMinutes": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The idle timeout of the public IP address." + } + }, + "ddosSettings": { + "$ref": "#/definitions/_2.ddosSettingsType", + "nullable": true, + "metadata": { + "description": "Optional. The DDoS protection plan configuration associated with the public IP address." + } + }, + "dnsSettings": { + "$ref": "#/definitions/_2.dnsSettingsType", + "nullable": true, + "metadata": { + "description": "Optional. The DNS settings of the public IP address." + } + }, + "publicIPAddressVersion": { + "type": "string", + "allowedValues": [ + "IPv4", + "IPv6" + ], + "nullable": true, + "metadata": { + "description": "Optional. The public IP address version." + } + }, + "publicIPAllocationMethod": { + "type": "string", + "allowedValues": [ + "Dynamic", + "Static" + ], + "nullable": true, + "metadata": { + "description": "Optional. The public IP address allocation method." + } + }, + "publicIpPrefixResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the Public IP Prefix object. This is only needed if you want your Public IPs created in a PIP Prefix." + } + }, + "publicIpNameSuffix": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name suffix of the public IP address resource." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/_3.roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "skuName": { + "type": "string", + "allowedValues": [ + "Basic", + "Standard" + ], + "nullable": true, + "metadata": { + "description": "Optional. The SKU name of the public IP address." + } + }, + "skuTier": { + "type": "string", + "allowedValues": [ + "Global", + "Regional" + ], + "nullable": true, + "metadata": { + "description": "Optional. The SKU tier of the public IP address." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/publicIPAddresses@2024-07-01#properties/tags" + }, + "description": "Optional. The tags of the public IP address." + }, + "nullable": true + }, + "availabilityZones": { + "type": "array", + "allowedValues": [ + 1, + 2, + 3 + ], + "nullable": true, + "metadata": { + "description": "Optional. The zones of the public IP address." + } + }, + "ipTags": { + "type": "array", + "items": { + "$ref": "#/definitions/_2.ipTagType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The list of tags associated with the public IP address." + } + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for the module." + } + } + }, "metadata": { - "description": "Required. The name of the virtual machine to be created. You should use a unique prefix to reduce name collisions in Active Directory." + "description": "The type for the public IP address configuration.", + "__bicep_imported_from!": { + "sourceTemplate": "modules/nic-configuration.bicep" + } } }, - "computerName": { - "type": "string", - "defaultValue": "[parameters('name')]", + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, "metadata": { - "description": "Optional. Can be used if the computer name needs to be different from the Azure VM resource name. If not used, the resource name will be used as computer name." + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } } }, - "vmSize": { - "type": "string", + "ipConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the IP configuration." + } + }, + "privateIPAllocationMethod": { + "type": "string", + "allowedValues": [ + "Dynamic", + "Static" + ], + "nullable": true, + "metadata": { + "description": "Optional. The private IP address allocation method." + } + }, + "privateIPAddress": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The private IP address." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the subnet." + } + }, + "loadBalancerBackendAddressPools": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.backendAddressPoolType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The load balancer backend address pools." + } + }, + "applicationSecurityGroups": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.applicationSecurityGroupType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The application security groups." + } + }, + "applicationGatewayBackendAddressPools": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.applicationGatewayBackendAddressPoolsType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The application gateway backend address pools." + } + }, + "gatewayLoadBalancer": { + "$ref": "#/definitions/subResourceType", + "nullable": true, + "metadata": { + "description": "Optional. The gateway load balancer settings." + } + }, + "loadBalancerInboundNatRules": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.inboundNatRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The load balancer inbound NAT rules." + } + }, + "privateIPAddressVersion": { + "type": "string", + "allowedValues": [ + "IPv4", + "IPv6" + ], + "nullable": true, + "metadata": { + "description": "Optional. The private IP address version." + } + }, + "virtualNetworkTaps": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.virtualNetworkTapType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The virtual network taps." + } + }, + "pipConfiguration": { + "$ref": "#/definitions/_4.publicIPConfigurationType", + "nullable": true, + "metadata": { + "description": "Optional. The public IP address configuration." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/_3.diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the IP configuration." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/networkInterfaces@2024-07-01#properties/tags" + }, + "description": "Optional. The tags of the public IP address." + }, + "nullable": true + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for the module." + } + } + }, "metadata": { - "description": "Required. Specifies the size for the VMs." + "description": "The type for the IP configuration.", + "__bicep_imported_from!": { + "sourceTemplate": "modules/nic-configuration.bicep" + } } }, - "encryptionAtHost": { - "type": "bool", - "defaultValue": false, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, "metadata": { - "description": "Optional. This property can be used by user in the request to enable or disable the Host Encryption for the virtual machine. This will enable the encryption for all the disks including Resource/Temp disk at host itself. For security reasons, it is recommended to set encryptionAtHost to True. Restrictions: Cannot be enabled if Azure Disk Encryption (guest-VM encryption using bitlocker/DM-Crypt) is enabled on your VMs." + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } } }, - "securityType": { - "type": "string", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines@2025-04-01#properties/properties/properties/securityProfile/properties/securityType" + "managedIdentityAllType": { + "type": "object", + "properties": { + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } }, - "description": "Optional. Specifies the SecurityType of the virtual machine. It has to be set to any specified value to enable UefiSettings. The default behavior is: UefiSettings will not be enabled unless this property is set." + "userAssignedResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + } + } }, - "nullable": true - }, - "secureBootEnabled": { - "type": "bool", - "defaultValue": false, "metadata": { - "description": "Optional. Specifies whether secure boot should be enabled on the virtual machine. This parameter is part of the UefiSettings. SecurityType should be set to TrustedLaunch to enable UefiSettings." + "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } } }, - "vTpmEnabled": { - "type": "bool", - "defaultValue": false, - "metadata": { + "networkInterfaceIPConfigurationOutputType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the IP configuration." + } + }, + "privateIP": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The private IP address." + } + }, + "publicIP": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The public IP address." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "subResourceType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the sub resource." + } + } + }, + "metadata": { + "description": "The type for the sub resource.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the virtual machine to be created. You should use a unique prefix to reduce name collisions in Active Directory." + } + }, + "computerName": { + "type": "string", + "defaultValue": "[parameters('name')]", + "metadata": { + "description": "Optional. Can be used if the computer name needs to be different from the Azure VM resource name. If not used, the resource name will be used as computer name." + } + }, + "vmSize": { + "type": "string", + "metadata": { + "description": "Required. Specifies the size for the VMs." + } + }, + "encryptionAtHost": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. This property can be used by user in the request to enable or disable the Host Encryption for the virtual machine. This will enable the encryption for all the disks including Resource/Temp disk at host itself. For security reasons, it is recommended to set encryptionAtHost to True. Restrictions: Cannot be enabled if Azure Disk Encryption (guest-VM encryption using bitlocker/DM-Crypt) is enabled on your VMs." + } + }, + "securityType": { + "type": "string", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines@2025-04-01#properties/properties/properties/securityProfile/properties/securityType" + }, + "description": "Optional. Specifies the SecurityType of the virtual machine. It has to be set to any specified value to enable UefiSettings. The default behavior is: UefiSettings will not be enabled unless this property is set." + }, + "nullable": true + }, + "secureBootEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Specifies whether secure boot should be enabled on the virtual machine. This parameter is part of the UefiSettings. SecurityType should be set to TrustedLaunch to enable UefiSettings." + } + }, + "vTpmEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { "description": "Optional. Specifies whether vTPM should be enabled on the virtual machine. This parameter is part of the UefiSettings. SecurityType should be set to TrustedLaunch to enable UefiSettings." } }, @@ -18123,7 +19447,8 @@ }, "dependsOn": [ "logAnalyticsWorkspace", - "virtualNetwork" + "virtualNetwork", + "windowsVmDataCollectionRules" ] }, "avmPrivateDnsZones": { @@ -22490,7 +23815,6 @@ } }, "aiFoundryAiServices": { - "condition": "[variables('aiFoundryAIservicesEnabled')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "[take(format('avm.res.cognitive-services.account.{0}', variables('aiFoundryAiServicesResourceName')), 64)]", @@ -22602,8 +23926,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "11255056345205002263" + "version": "0.43.8.12551", + "templateHash": "16752595662856460651" }, "name": "Cognitive Services", "description": "This module deploys a Cognitive Service." @@ -23751,8 +25075,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "2352464251246464745" + "version": "0.43.8.12551", + "templateHash": "10918733563560027648" } }, "definitions": { @@ -25401,8 +26725,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "8527060477757998371" + "version": "0.43.8.12551", + "templateHash": "9910113619698591130" } }, "definitions": { @@ -25631,8 +26955,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "2352464251246464745" + "version": "0.43.8.12551", + "templateHash": "10918733563560027648" } }, "definitions": { @@ -27217,3170 +28541,38 @@ }, "description": "The custom DNS configurations of the private endpoint." }, - "value": "[reference('privateEndpoint').customDnsConfigs]" - }, - "networkInterfaceResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "The resource IDs of the network interfaces associated with the private endpoint." - }, - "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" - }, - "groupId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "The group Id for the private endpoint Group." - }, - "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" - } - } - } - }, - "dependsOn": [ - "cognitiveService_deployments" - ] - }, - "aiProject": { - "condition": "[or(not(empty(parameters('projectName'))), not(empty(parameters('existingFoundryProjectResourceId'))))]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[take(format('{0}-ai-project-{1}-deployment', parameters('name'), parameters('projectName')), 64)]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[parameters('projectName')]" - }, - "desc": { - "value": "[parameters('projectDescription')]" - }, - "aiServicesName": { - "value": "[parameters('name')]" - }, - "location": { - "value": "[parameters('location')]" - }, - "tags": { - "value": "[parameters('tags')]" - }, - "existingFoundryProjectResourceId": { - "value": "[parameters('existingFoundryProjectResourceId')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "8527060477757998371" - } - }, - "definitions": { - "aiProjectOutputType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the AI project." - } - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "Required. Resource ID of the AI project." - } - }, - "apiEndpoint": { - "type": "string", - "metadata": { - "description": "Required. API endpoint for the AI project." - } - }, - "aiprojectSystemAssignedMIPrincipalId": { - "type": "string", - "metadata": { - "description": "Required. System Assigned Managed Identity Principal Id of the AI project." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "Output type representing AI project information." - } - } - }, - "parameters": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the AI Services project." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Required. The location of the Project resource." - } - }, - "desc": { - "type": "string", - "defaultValue": "[parameters('name')]", - "metadata": { - "description": "Optional. The description of the AI Foundry project to create. Defaults to the project name." - } - }, - "aiServicesName": { - "type": "string", - "metadata": { - "description": "Required. Name of the existing Cognitive Services resource to create the AI Foundry project in." - } - }, - "tags": { - "type": "object", - "defaultValue": {}, - "metadata": { - "description": "Optional. Tags to be applied to the resources." - } - }, - "existingFoundryProjectResourceId": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Optional. Use this parameter to use an existing AI project resource ID from different resource group" - } - } - }, - "variables": { - "useExistingProject": "[not(empty(parameters('existingFoundryProjectResourceId')))]", - "existingProjName": "[if(variables('useExistingProject'), last(split(parameters('existingFoundryProjectResourceId'), '/')), '')]", - "existingAiFoundryAiServicesSubscriptionId": "[if(variables('useExistingProject'), split(parameters('existingFoundryProjectResourceId'), '/')[2], '')]", - "existingAiFoundryAiServicesResourceGroupName": "[if(variables('useExistingProject'), split(parameters('existingFoundryProjectResourceId'), '/')[4], '')]", - "existingAiFoundryAiServicesServiceName": "[if(variables('useExistingProject'), split(parameters('existingFoundryProjectResourceId'), '/')[8], '')]", - "existingProjEndpoint": "[if(variables('useExistingProject'), format('https://{0}.services.ai.azure.com/api/projects/{1}', variables('existingAiFoundryAiServicesServiceName'), variables('existingProjName')), '')]" - }, - "resources": { - "cogServiceReference": { - "existing": true, - "type": "Microsoft.CognitiveServices/accounts", - "apiVersion": "2025-06-01", - "name": "[parameters('aiServicesName')]" - }, - "aiProject": { - "condition": "[not(variables('useExistingProject'))]", - "type": "Microsoft.CognitiveServices/accounts/projects", - "apiVersion": "2025-06-01", - "name": "[format('{0}/{1}', parameters('aiServicesName'), parameters('name'))]", - "tags": "[parameters('tags')]", - "location": "[parameters('location')]", - "identity": { - "type": "SystemAssigned" - }, - "properties": { - "description": "[parameters('desc')]", - "displayName": "[parameters('name')]" - } - }, - "existingAiProject": { - "condition": "[variables('useExistingProject')]", - "existing": true, - "type": "Microsoft.CognitiveServices/accounts/projects", - "apiVersion": "2025-06-01", - "subscriptionId": "[variables('existingAiFoundryAiServicesSubscriptionId')]", - "resourceGroup": "[variables('existingAiFoundryAiServicesResourceGroupName')]", - "name": "[format('{0}/{1}', variables('existingAiFoundryAiServicesServiceName'), variables('existingProjName'))]" - } - }, - "outputs": { - "aiProjectInfo": { - "$ref": "#/definitions/aiProjectOutputType", - "metadata": { - "description": "AI Project metadata including name, resource ID, and API endpoint." - }, - "value": { - "name": "[if(variables('useExistingProject'), variables('existingProjName'), parameters('name'))]", - "resourceId": "[if(variables('useExistingProject'), parameters('existingFoundryProjectResourceId'), resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('aiServicesName'), parameters('name')))]", - "apiEndpoint": "[if(variables('useExistingProject'), variables('existingProjEndpoint'), reference('aiProject').endpoints['AI Foundry API'])]", - "aiprojectSystemAssignedMIPrincipalId": "[if(variables('useExistingProject'), reference('existingAiProject', '2025-06-01', 'full').identity.principalId, reference('aiProject', '2025-06-01', 'full').identity.principalId)]" - } - } - } - } - }, - "dependsOn": [ - "cognitiveService_deployments" - ] - } - }, - "outputs": { - "privateEndpoints": { - "type": "array", - "items": { - "$ref": "#/definitions/privateEndpointOutputType" - }, - "metadata": { - "description": "The private endpoints of the congitive services account." - }, - "copy": { - "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]", - "input": { - "name": "[reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs.name.value]", - "resourceId": "[reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs.resourceId.value]", - "groupId": "[tryGet(tryGet(reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs, 'groupId'), 'value')]", - "customDnsConfigs": "[reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs.customDnsConfigs.value]", - "networkInterfaceResourceIds": "[reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs.networkInterfaceResourceIds.value]" - } - } - }, - "aiProjectInfo": { - "$ref": "#/definitions/aiProjectOutputType", - "value": "[reference('aiProject').outputs.aiProjectInfo.value]" - } - } - } - } - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the cognitive services account." - }, - "value": "[if(variables('useExistingService'), variables('existingCognitiveServiceDetails')[8], parameters('name'))]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the cognitive services account." - }, - "value": "[if(variables('useExistingService'), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('existingCognitiveServiceDetails')[2], variables('existingCognitiveServiceDetails')[4]), 'Microsoft.CognitiveServices/accounts', variables('existingCognitiveServiceDetails')[8]), resourceId('Microsoft.CognitiveServices/accounts', parameters('name')))]" - }, - "subscriptionId": { - "type": "string", - "metadata": { - "description": "The resource group the cognitive services account was deployed into." - }, - "value": "[if(variables('useExistingService'), variables('existingCognitiveServiceDetails')[2], subscription().subscriptionId)]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group the cognitive services account was deployed into." - }, - "value": "[if(variables('useExistingService'), variables('existingCognitiveServiceDetails')[4], resourceGroup().name)]" - }, - "endpoint": { - "type": "string", - "metadata": { - "description": "The service endpoint of the cognitive services account." - }, - "value": "[if(variables('useExistingService'), reference('cognitiveServiceExisting').endpoint, if(variables('useExistingService'), reference('cognitiveServiceExisting', '2025-09-01', 'full'), reference('cognitiveServiceNew', '2025-06-01', 'full')).properties.endpoint)]" - }, - "endpoints": { - "$ref": "#/definitions/endpointType", - "metadata": { - "description": "All endpoints available for the cognitive services account, types depends on the cognitive service kind." - }, - "value": "[if(variables('useExistingService'), reference('cognitiveServiceExisting').endpoints, if(variables('useExistingService'), reference('cognitiveServiceExisting', '2025-09-01', 'full'), reference('cognitiveServiceNew', '2025-06-01', 'full')).properties.endpoints)]" - }, - "systemAssignedMIPrincipalId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "The principal ID of the system assigned identity." - }, - "value": "[if(variables('useExistingService'), reference('cognitiveServiceExisting', '2025-09-01', 'full').identity.principalId, tryGet(tryGet(if(variables('useExistingService'), reference('cognitiveServiceExisting', '2025-09-01', 'full'), reference('cognitiveServiceNew', '2025-06-01', 'full')), 'identity'), 'principalId'))]" - }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." - }, - "value": "[if(variables('useExistingService'), reference('cognitiveServiceExisting', '2025-09-01', 'full').location, if(variables('useExistingService'), reference('cognitiveServiceExisting', '2025-09-01', 'full'), reference('cognitiveServiceNew', '2025-06-01', 'full')).location)]" - }, - "privateEndpoints": { - "type": "array", - "items": { - "$ref": "#/definitions/privateEndpointOutputType" - }, - "metadata": { - "description": "The private endpoints of the congitive services account." - }, - "value": "[if(variables('useExistingService'), reference('existing_cognitive_service_dependencies').outputs.privateEndpoints.value, reference('cognitive_service_dependencies').outputs.privateEndpoints.value)]" - }, - "aiProjectInfo": { - "$ref": "#/definitions/aiProjectOutputType", - "value": "[if(variables('useExistingService'), reference('existing_cognitive_service_dependencies').outputs.aiProjectInfo.value, reference('cognitive_service_dependencies').outputs.aiProjectInfo.value)]" - } - } - } - }, - "dependsOn": [ - "backendUserAssignedIdentity", - "logAnalyticsWorkspace", - "userAssignedIdentity" - ] - }, - "aiFoundryPrivateEndpoint": { - "condition": "[and(parameters('enablePrivateNetworking'), not(variables('useExistingAiFoundryAiProject')))]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[take(format('pep-{0}-deployment', variables('aiFoundryAiServicesResourceName')), 64)]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[format('pep-{0}', variables('aiFoundryAiServicesResourceName'))]" - }, - "customNetworkInterfaceName": { - "value": "[format('nic-{0}', variables('aiFoundryAiServicesResourceName'))]" - }, - "location": { - "value": "[parameters('location')]" - }, - "tags": { - "value": "[parameters('tags')]" - }, - "privateLinkServiceConnections": { - "value": [ - { - "name": "[format('pep-{0}-connection', variables('aiFoundryAiServicesResourceName'))]", - "properties": { - "privateLinkServiceId": "[reference('aiFoundryAiServices').outputs.resourceId.value]", - "groupIds": [ - "account" - ] - } - } - ] - }, - "privateDnsZoneGroup": { - "value": { - "privateDnsZoneGroupConfigs": [ - { - "name": "ai-services-dns-zone-cognitiveservices", - "privateDnsZoneResourceId": "[reference(format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)).outputs.resourceId.value]" - }, - { - "name": "ai-services-dns-zone-openai", - "privateDnsZoneResourceId": "[reference(format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)).outputs.resourceId.value]" - }, - { - "name": "ai-services-dns-zone-aiservices", - "privateDnsZoneResourceId": "[reference(format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').aiServices)).outputs.resourceId.value]" - } - ] - } - }, - "subnetResourceId": { - "value": "[reference('virtualNetwork').outputs.pepsSubnetResourceId.value]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "2541425927059591098" - }, - "name": "Private Endpoints", - "description": "This module deploys a Private Endpoint.", - "owner": "Azure/module-maintainers" - }, - "definitions": { - "privateDnsZoneGroupType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the Private DNS Zone Group." - } - }, - "privateDnsZoneGroupConfigs": { - "type": "array", - "items": { - "$ref": "#/definitions/privateDnsZoneGroupConfigType" - }, - "metadata": { - "description": "Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones." - } - } - } - }, - "roleAssignmentType": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - } - }, - "nullable": true - }, - "lockType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the name of lock." - } - }, - "kind": { - "type": "string", - "allowedValues": [ - "CanNotDelete", - "None", - "ReadOnly" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specify the type of lock." - } - } - }, - "nullable": true - }, - "ipConfigurationsType": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the resource that is unique within a resource group." - } - }, - "properties": { - "type": "object", - "properties": { - "groupId": { - "type": "string", - "metadata": { - "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." - } - }, - "memberName": { - "type": "string", - "metadata": { - "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." - } - }, - "privateIPAddress": { - "type": "string", - "metadata": { - "description": "Required. A private IP address obtained from the private endpoint's subnet." - } - } - }, - "metadata": { - "description": "Required. Properties of private endpoint IP configurations." - } - } - } - }, - "nullable": true - }, - "manualPrivateLinkServiceConnectionsType": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the private link service connection." - } - }, - "properties": { - "type": "object", - "properties": { - "groupIds": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string array `[]`." - } - }, - "privateLinkServiceId": { - "type": "string", - "metadata": { - "description": "Required. The resource id of private link service." - } - }, - "requestMessage": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. A message passed to the owner of the remote resource with this connection request. Restricted to 140 chars." - } - } - }, - "metadata": { - "description": "Required. Properties of private link service connection." - } - } - } - }, - "nullable": true - }, - "privateLinkServiceConnectionsType": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the private link service connection." - } - }, - "properties": { - "type": "object", - "properties": { - "groupIds": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string array `[]`." - } - }, - "privateLinkServiceId": { - "type": "string", - "metadata": { - "description": "Required. The resource id of private link service." - } - }, - "requestMessage": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. A message passed to the owner of the remote resource with this connection request. Restricted to 140 chars." - } - } - }, - "metadata": { - "description": "Required. Properties of private link service connection." - } - } - } - }, - "nullable": true - }, - "customDnsConfigType": { - "type": "array", - "items": { - "type": "object", - "properties": { - "fqdn": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. FQDN that resolves to private endpoint IP address." - } - }, - "ipAddresses": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of private IP addresses of the private endpoint." - } - } - } - }, - "nullable": true - }, - "privateDnsZoneGroupConfigType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the private DNS zone group config." - } - }, - "privateDnsZoneResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource id of the private DNS zone." - } - } - }, - "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "private-dns-zone-group/main.bicep" - } - } - } - }, - "parameters": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the private endpoint resource to create." - } - }, - "subnetResourceId": { - "type": "string", - "metadata": { - "description": "Required. Resource ID of the subnet where the endpoint needs to be created." - } - }, - "applicationSecurityGroupResourceIds": { - "type": "array", - "nullable": true, - "metadata": { - "description": "Optional. Application security groups in which the private endpoint IP configuration is included." - } - }, - "customNetworkInterfaceName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The custom name of the network interface attached to the private endpoint." - } - }, - "ipConfigurations": { - "$ref": "#/definitions/ipConfigurationsType", - "metadata": { - "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." - } - }, - "privateDnsZoneGroup": { - "$ref": "#/definitions/privateDnsZoneGroupType", - "nullable": true, - "metadata": { - "description": "Optional. The private DNS zone group to configure for the private endpoint." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Optional. Location for all Resources." - } - }, - "lock": { - "$ref": "#/definitions/lockType", - "metadata": { - "description": "Optional. The lock settings of the service." - } - }, - "roleAssignments": { - "$ref": "#/definitions/roleAssignmentType", - "metadata": { - "description": "Optional. Array of role assignments to create." - } - }, - "tags": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." - } - }, - "customDnsConfigs": { - "$ref": "#/definitions/customDnsConfigType", - "metadata": { - "description": "Optional. Custom DNS configurations." - } - }, - "manualPrivateLinkServiceConnections": { - "$ref": "#/definitions/manualPrivateLinkServiceConnectionsType", - "metadata": { - "description": "Optional. A grouping of information about the connection to the remote resource. Used when the network admin does not have access to approve connections to the remote resource." - } - }, - "privateLinkServiceConnections": { - "$ref": "#/definitions/privateLinkServiceConnectionsType", - "metadata": { - "description": "Optional. A grouping of information about the connection to the remote resource." - } - }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." - } - } - }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" - } - ], - "builtInRoleNames": { - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", - "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", - "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", - "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", - "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" - } - }, - "resources": { - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.network-privateendpoint.{0}.{1}', replace('0.8.1', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } - } - } - } - }, - "privateEndpoint": { - "type": "Microsoft.Network/privateEndpoints", - "apiVersion": "2023-11-01", - "name": "[parameters('name')]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "properties": { - "copy": [ - { - "name": "applicationSecurityGroups", - "count": "[length(coalesce(parameters('applicationSecurityGroupResourceIds'), createArray()))]", - "input": { - "id": "[coalesce(parameters('applicationSecurityGroupResourceIds'), createArray())[copyIndex('applicationSecurityGroups')]]" - } - } - ], - "customDnsConfigs": "[coalesce(parameters('customDnsConfigs'), createArray())]", - "customNetworkInterfaceName": "[coalesce(parameters('customNetworkInterfaceName'), '')]", - "ipConfigurations": "[coalesce(parameters('ipConfigurations'), createArray())]", - "manualPrivateLinkServiceConnections": "[coalesce(parameters('manualPrivateLinkServiceConnections'), createArray())]", - "privateLinkServiceConnections": "[coalesce(parameters('privateLinkServiceConnections'), createArray())]", - "subnet": { - "id": "[parameters('subnetResourceId')]" - } - } - }, - "privateEndpoint_lock": { - "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", - "type": "Microsoft.Authorization/locks", - "apiVersion": "2020-05-01", - "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", - "properties": { - "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", - "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" - }, - "dependsOn": [ - "privateEndpoint" - ] - }, - "privateEndpoint_roleAssignments": { - "copy": { - "name": "privateEndpoint_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateEndpoints', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", - "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" - }, - "dependsOn": [ - "privateEndpoint" - ] - }, - "privateEndpoint_privateDnsZoneGroup": { - "condition": "[not(empty(parameters('privateDnsZoneGroup')))]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[tryGet(parameters('privateDnsZoneGroup'), 'name')]" - }, - "privateEndpointName": { - "value": "[parameters('name')]" - }, - "privateDnsZoneConfigs": { - "value": "[parameters('privateDnsZoneGroup').privateDnsZoneGroupConfigs]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "12329174801198479603" - }, - "name": "Private Endpoint Private DNS Zone Groups", - "description": "This module deploys a Private Endpoint Private DNS Zone Group.", - "owner": "Azure/module-maintainers" - }, - "definitions": { - "privateDnsZoneGroupConfigType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the private DNS zone group config." - } - }, - "privateDnsZoneResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource id of the private DNS zone." - } - } - }, - "metadata": { - "__bicep_export!": true - } - } - }, - "parameters": { - "privateEndpointName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent private endpoint. Required if the template is used in a standalone deployment." - } - }, - "privateDnsZoneConfigs": { - "type": "array", - "items": { - "$ref": "#/definitions/privateDnsZoneGroupConfigType" - }, - "minLength": 1, - "maxLength": 5, - "metadata": { - "description": "Required. Array of private DNS zone configurations of the private DNS zone group. A DNS zone group can support up to 5 DNS zones." - } - }, - "name": { - "type": "string", - "defaultValue": "default", - "metadata": { - "description": "Optional. The name of the private DNS zone group." - } - } - }, - "variables": { - "copy": [ - { - "name": "privateDnsZoneConfigsVar", - "count": "[length(parameters('privateDnsZoneConfigs'))]", - "input": { - "name": "[coalesce(tryGet(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')], 'name'), last(split(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId, '/')))]", - "properties": { - "privateDnsZoneId": "[parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId]" - } - } - } - ] - }, - "resources": { - "privateEndpoint": { - "existing": true, - "type": "Microsoft.Network/privateEndpoints", - "apiVersion": "2023-11-01", - "name": "[parameters('privateEndpointName')]" - }, - "privateDnsZoneGroup": { - "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", - "apiVersion": "2023-11-01", - "name": "[format('{0}/{1}', parameters('privateEndpointName'), parameters('name'))]", - "properties": { - "privateDnsZoneConfigs": "[variables('privateDnsZoneConfigsVar')]" - }, - "dependsOn": [ - "privateEndpoint" - ] - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the private endpoint DNS zone group." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the private endpoint DNS zone group." - }, - "value": "[resourceId('Microsoft.Network/privateEndpoints/privateDnsZoneGroups', parameters('privateEndpointName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group the private endpoint DNS zone group was deployed into." - }, - "value": "[resourceGroup().name]" - } - } - } - }, - "dependsOn": [ - "privateEndpoint" - ] - } - }, - "outputs": { - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group the private endpoint was deployed into." - }, - "value": "[resourceGroup().name]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the private endpoint." - }, - "value": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]" - }, - "name": { - "type": "string", - "metadata": { - "description": "The name of the private endpoint." - }, - "value": "[parameters('name')]" - }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." - }, - "value": "[reference('privateEndpoint', '2023-11-01', 'full').location]" - }, - "customDnsConfig": { - "$ref": "#/definitions/customDnsConfigType", - "metadata": { - "description": "The custom DNS configurations of the private endpoint." - }, - "value": "[reference('privateEndpoint').customDnsConfigs]" - }, - "networkInterfaceIds": { - "type": "array", - "metadata": { - "description": "The IDs of the network interfaces associated with the private endpoint." - }, - "value": "[reference('privateEndpoint').networkInterfaces]" - }, - "groupId": { - "type": "string", - "metadata": { - "description": "The group Id for the private endpoint Group." - }, - "value": "[if(and(not(empty(reference('privateEndpoint').manualPrivateLinkServiceConnections)), greater(length(tryGet(reference('privateEndpoint').manualPrivateLinkServiceConnections[0].properties, 'groupIds')), 0)), coalesce(tryGet(reference('privateEndpoint').manualPrivateLinkServiceConnections[0].properties, 'groupIds', 0), ''), if(and(not(empty(reference('privateEndpoint').privateLinkServiceConnections)), greater(length(tryGet(reference('privateEndpoint').privateLinkServiceConnections[0].properties, 'groupIds')), 0)), coalesce(tryGet(reference('privateEndpoint').privateLinkServiceConnections[0].properties, 'groupIds', 0), ''), ''))]" - } - } - } - }, - "dependsOn": [ - "aiFoundryAiServices", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').aiServices)]", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", - "virtualNetwork" - ] - }, - "cognitiveServicesCu": { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[take(format('avm.res.cognitive-services.account.{0}', variables('aiFoundryAiServicesCUResourceName')), 64)]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[variables('aiServicesNameCu')]" - }, - "location": { - "value": "[parameters('contentUnderstandingLocation')]" - }, - "tags": { - "value": "[parameters('tags')]" - }, - "enableTelemetry": { - "value": "[parameters('enableTelemetry')]" - }, - "diagnosticSettings": "[if(parameters('enableMonitoring'), createObject('value', createArray(createObject('workspaceResourceId', if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), reference('logAnalyticsWorkspace').outputs.resourceId.value)))), createObject('value', null()))]", - "sku": { - "value": "S0" - }, - "kind": { - "value": "AIServices" - }, - "networkAcls": { - "value": { - "defaultAction": "Allow", - "virtualNetworkRules": [], - "ipRules": [] - } - }, - "managedIdentities": { - "value": { - "userAssignedResourceIds": [ - "[reference('userAssignedIdentity').outputs.resourceId.value]" - ] - } - }, - "disableLocalAuth": { - "value": true - }, - "customSubDomainName": { - "value": "[variables('aiServicesNameCu')]" - }, - "apiProperties": { - "value": {} - }, - "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), createObject('value', 'Disabled'), createObject('value', 'Enabled'))]", - "privateEndpoints": { - "value": [] - }, - "roleAssignments": { - "value": [ - { - "roleDefinitionIdOrName": "53ca6127-db72-4b80-b1b0-d745d6d5456d", - "principalId": "[reference('userAssignedIdentity').outputs.principalId.value]", - "principalType": "ServicePrincipal" - } - ] - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "6544538318162038728" - }, - "name": "Cognitive Services", - "description": "This module deploys a Cognitive Service." - }, - "definitions": { - "privateEndpointOutputType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the private endpoint." - } - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the private endpoint." - } - }, - "groupId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "The group Id for the private endpoint Group." - } - }, - "customDnsConfigs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "fqdn": { - "type": "string", - "nullable": true, - "metadata": { - "description": "FQDN that resolves to private endpoint IP address." - } - }, - "ipAddresses": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "A list of private IP addresses of the private endpoint." - } - } - } - }, - "metadata": { - "description": "The custom DNS configurations of the private endpoint." - } - }, - "networkInterfaceResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "The IDs of the network interfaces associated with the private endpoint." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for the private endpoint output." - } - }, - "deploymentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the name of cognitive service account deployment." - } - }, - "model": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of Cognitive Services account deployment model." - } - }, - "format": { - "type": "string", - "metadata": { - "description": "Required. The format of Cognitive Services account deployment model." - } - }, - "version": { - "type": "string", - "metadata": { - "description": "Required. The version of Cognitive Services account deployment model." - } - } - }, - "metadata": { - "description": "Required. Properties of Cognitive Services account deployment model." - } - }, - "sku": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the resource model definition representing SKU." - } - }, - "capacity": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. The capacity of the resource model definition representing SKU." - } - }, - "tier": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The tier of the resource model definition representing SKU." - } - }, - "size": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The size of the resource model definition representing SKU." - } - }, - "family": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The family of the resource model definition representing SKU." - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The resource model definition representing SKU." - } - }, - "raiPolicyName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of RAI policy." - } - }, - "versionUpgradeOption": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The version upgrade option." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for a cognitive services account deployment." - } - }, - "endpointType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Type of the endpoint." - } - }, - "endpoint": { - "type": "string", - "nullable": true, - "metadata": { - "description": "The endpoint URI." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for a cognitive services account endpoint." - } - }, - "secretsExportConfigurationType": { - "type": "object", - "properties": { - "keyVaultResourceId": { - "type": "string", - "metadata": { - "description": "Required. The key vault name where to store the keys and connection strings generated by the modules." - } - }, - "accessKey1Name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name for the accessKey1 secret to create." - } - }, - "accessKey2Name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name for the accessKey2 secret to create." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type of the secrets exported to the provided Key Vault." - } - }, - "commitmentPlanType": { - "type": "object", - "properties": { - "autoRenew": { - "type": "bool", - "metadata": { - "description": "Required. Whether the plan should auto-renew at the end of the current commitment period." - } - }, - "current": { - "type": "object", - "properties": { - "count": { - "type": "int", - "metadata": { - "description": "Required. The number of committed instances (e.g., number of containers or cores)." - } - }, - "tier": { - "type": "string", - "metadata": { - "description": "Required. The tier of the commitment plan (e.g., T1, T2)." - } - } - }, - "metadata": { - "description": "Required. The current commitment configuration." - } - }, - "hostingModel": { - "type": "string", - "metadata": { - "description": "Required. The hosting model for the commitment plan. (e.g., DisconnectedContainer, ConnectedContainer, ProvisionedWeb, Web)." - } - }, - "planType": { - "type": "string", - "metadata": { - "description": "Required. The plan type indicating which capability the plan applies to (e.g., NTTS, STT, CUSTOMSTT, ADDON)." - } - }, - "commitmentPlanGuid": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The unique identifier of an existing commitment plan to update. Set to null to create a new plan." - } - }, - "next": { - "type": "object", - "properties": { - "count": { - "type": "int", - "metadata": { - "description": "Required. The number of committed instances for the next period." - } - }, - "tier": { - "type": "string", - "metadata": { - "description": "Required. The tier for the next commitment period." - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The configuration of the next commitment period, if scheduled." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for a disconnected container commitment plan." - } - }, - "networkInjectionType": { - "type": "object", - "properties": { - "scenario": { - "type": "string", - "allowedValues": [ - "agent", - "none" - ], - "metadata": { - "description": "Required. The scenario for the network injection." - } - }, - "subnetResourceId": { - "type": "string", - "metadata": { - "description": "Required. The Resource ID of the subnet on the Virtual Network on which to inject." - } - }, - "useMicrosoftManagedNetwork": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Whether to use Microsoft Managed Network. Defaults to false." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "Type for network configuration in AI Foundry where virtual network injection occurs to secure scenarios like Agents entirely within a private network." - } - }, - "_1.secretSetOutputType": { - "type": "object", - "properties": { - "secretResourceId": { - "type": "string", - "metadata": { - "description": "The resourceId of the exported secret." - } - }, - "secretUri": { - "type": "string", - "metadata": { - "description": "The secret URI of the exported secret." - } - }, - "secretUriWithVersion": { - "type": "string", - "metadata": { - "description": "The secret URI with version of the exported secret." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for the output of the secret set via the secrets export feature.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" - } - } - }, - "_2.lockType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the name of lock." - } - }, - "kind": { - "type": "string", - "allowedValues": [ - "CanNotDelete", - "None", - "ReadOnly" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specify the type of lock." - } - }, - "notes": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the notes of the lock." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a lock.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "_2.privateEndpointCustomDnsConfigType": { - "type": "object", - "properties": { - "fqdn": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. FQDN that resolves to private endpoint IP address." - } - }, - "ipAddresses": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of private IP addresses of the private endpoint." - } - } - }, - "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "_2.privateEndpointIpConfigurationType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the resource that is unique within a resource group." - } - }, - "properties": { - "type": "object", - "properties": { - "groupId": { - "type": "string", - "metadata": { - "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to." - } - }, - "memberName": { - "type": "string", - "metadata": { - "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to." - } - }, - "privateIPAddress": { - "type": "string", - "metadata": { - "description": "Required. A private IP address obtained from the private endpoint's subnet." - } - } - }, - "metadata": { - "description": "Required. Properties of private endpoint IP configurations." - } - } - }, - "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "_2.privateEndpointPrivateDnsZoneGroupType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the Private DNS Zone Group." - } - }, - "privateDnsZoneGroupConfigs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the private DNS Zone Group config." - } - }, - "privateDnsZoneResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource id of the private DNS zone." - } - } - } - }, - "metadata": { - "description": "Required. The private DNS Zone Groups to associate the Private Endpoint. A DNS Zone Group can support up to 5 DNS zones." - } - } - }, - "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "_2.roleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "customerManagedKeyType": { - "type": "object", - "properties": { - "keyVaultResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource ID of a key vault to reference a customer managed key for encryption from." - } - }, - "keyName": { - "type": "string", - "metadata": { - "description": "Required. The name of the customer managed key to use for encryption." - } - }, - "keyVersion": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The version of the customer managed key to reference for encryption. If not provided, the deployment will use the latest version available at deployment time." - } - }, - "userAssignedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. User assigned identity to use when fetching the customer managed key. Required if no system assigned identity is available for use." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a customer-managed key. To be used if the resource type does not support auto-rotation of the customer-managed key.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" - } - } - }, - "diagnosticSettingFullType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the diagnostic setting." - } - }, - "logCategoriesAndGroups": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." - } - }, - "categoryGroup": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." - } - }, - "metricCategories": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "metadata": { - "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." - } - }, - "logAnalyticsDestinationType": { - "type": "string", - "allowedValues": [ - "AzureDiagnostics", - "Dedicated" - ], - "nullable": true, - "metadata": { - "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." - } - }, - "workspaceResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "storageAccountResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "eventHubAuthorizationRuleResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." - } - }, - "eventHubName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "marketplacePartnerResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" - } - } - }, - "lockType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the name of lock." - } - }, - "kind": { - "type": "string", - "allowedValues": [ - "CanNotDelete", - "None", - "ReadOnly" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specify the type of lock." - } - }, - "notes": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the notes of the lock." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a lock.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" - } - } - }, - "managedIdentityAllType": { - "type": "object", - "properties": { - "systemAssigned": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enables system assigned managed identity on the resource." - } - }, - "userAssignedResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "metadata": { - "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" - } - } - }, - "privateEndpointSingleServiceType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the Private Endpoint." - } - }, - "location": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The location to deploy the Private Endpoint to." - } - }, - "privateLinkServiceConnectionName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the private link connection to create." - } - }, - "service": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The subresource to deploy the Private Endpoint for. For example \"vault\" for a Key Vault Private Endpoint." - } - }, - "subnetResourceId": { - "type": "string", - "metadata": { - "description": "Required. Resource ID of the subnet where the endpoint needs to be created." - } - }, - "resourceGroupResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The resource ID of the Resource Group the Private Endpoint will be created in. If not specified, the Resource Group of the provided Virtual Network Subnet is used." - } - }, - "privateDnsZoneGroup": { - "$ref": "#/definitions/_2.privateEndpointPrivateDnsZoneGroupType", - "nullable": true, - "metadata": { - "description": "Optional. The private DNS Zone Group to configure for the Private Endpoint." - } - }, - "isManualConnection": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. If Manual Private Link Connection is required." - } - }, - "manualConnectionRequestMessage": { - "type": "string", - "nullable": true, - "maxLength": 140, - "metadata": { - "description": "Optional. A message passed to the owner of the remote resource with the manual connection request." - } - }, - "customDnsConfigs": { - "type": "array", - "items": { - "$ref": "#/definitions/_2.privateEndpointCustomDnsConfigType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Custom DNS configurations." - } - }, - "ipConfigurations": { - "type": "array", - "items": { - "$ref": "#/definitions/_2.privateEndpointIpConfigurationType" - }, - "nullable": true, - "metadata": { - "description": "Optional. A list of IP configurations of the Private Endpoint. This will be used to map to the first-party Service endpoints." - } - }, - "applicationSecurityGroupResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "metadata": { - "description": "Optional. Application security groups in which the Private Endpoint IP configuration is included." - } - }, - "customNetworkInterfaceName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The custom name of the network interface attached to the Private Endpoint." - } - }, - "lock": { - "$ref": "#/definitions/_2.lockType", - "nullable": true, - "metadata": { - "description": "Optional. Specify the type of lock." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/_2.roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - }, - "tags": { - "type": "object", - "nullable": true, - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-07-01#properties/tags" - }, - "description": "Optional. Tags to be applied on all resources/Resource Groups in this deployment." - } - }, - "enableTelemetry": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a private endpoint. To be used if the private endpoint's default service / groupId can be assumed (i.e., for services that only have one Private Endpoint type like 'vault' for key vault).", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "roleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" - } - } - }, - "secretsOutputType": { - "type": "object", - "properties": {}, - "additionalProperties": { - "$ref": "#/definitions/_1.secretSetOutputType", - "metadata": { - "description": "An exported secret's references." - } - }, - "metadata": { - "description": "A map of the exported secrets", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" - } - } - } - }, - "parameters": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of Cognitive Services account." - } - }, - "kind": { - "type": "string", - "allowedValues": [ - "AIServices", - "AnomalyDetector", - "CognitiveServices", - "ComputerVision", - "ContentModerator", - "ContentSafety", - "ConversationalLanguageUnderstanding", - "CustomVision.Prediction", - "CustomVision.Training", - "Face", - "FormRecognizer", - "HealthInsights", - "ImmersiveReader", - "Internal.AllInOne", - "LUIS", - "LUIS.Authoring", - "LanguageAuthoring", - "MetricsAdvisor", - "OpenAI", - "Personalizer", - "QnAMaker.v2", - "SpeechServices", - "TextAnalytics", - "TextTranslation" - ], - "metadata": { - "description": "Required. Kind of the Cognitive Services account. Use 'Get-AzCognitiveServicesAccountSku' to determine a valid combinations of 'kind' and 'SKU' for your Azure region." - } - }, - "sku": { - "type": "string", - "defaultValue": "S0", - "allowedValues": [ - "C2", - "C3", - "C4", - "F0", - "F1", - "S", - "S0", - "S1", - "S10", - "S2", - "S3", - "S4", - "S5", - "S6", - "S7", - "S8", - "S9", - "DC0" - ], - "metadata": { - "description": "Optional. SKU of the Cognitive Services account. Use 'Get-AzCognitiveServicesAccountSku' to determine a valid combinations of 'kind' and 'SKU' for your Azure region." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Optional. Location for all Resources." - } - }, - "diagnosticSettings": { - "type": "array", - "items": { - "$ref": "#/definitions/diagnosticSettingFullType" - }, - "nullable": true, - "metadata": { - "description": "Optional. The diagnostic settings of the service." - } - }, - "publicNetworkAccess": { - "type": "string", - "nullable": true, - "allowedValues": [ - "Enabled", - "Disabled" - ], - "metadata": { - "description": "Optional. Whether or not public network access is allowed for this resource. For security reasons it should be disabled. If not specified, it will be disabled by default if private endpoints are set and networkAcls are not set." - } - }, - "customSubDomainName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Conditional. Subdomain name used for token-based authentication. Required if 'networkAcls' or 'privateEndpoints' are set." - } - }, - "networkAcls": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. A collection of rules governing the accessibility from specific network locations." - } - }, - "networkInjections": { - "$ref": "#/definitions/networkInjectionType", - "nullable": true, - "metadata": { - "description": "Optional. Specifies in AI Foundry where virtual network injection occurs to secure scenarios like Agents entirely within a private network." - } - }, - "privateEndpoints": { - "type": "array", - "items": { - "$ref": "#/definitions/privateEndpointSingleServiceType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible." - } - }, - "lock": { - "$ref": "#/definitions/lockType", - "nullable": true, - "metadata": { - "description": "Optional. The lock settings of the service." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - }, - "tags": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Tags of the resource." - } - }, - "allowedFqdnList": { - "type": "array", - "nullable": true, - "metadata": { - "description": "Optional. List of allowed FQDN." - } - }, - "apiProperties": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. The API properties for special APIs." - } - }, - "disableLocalAuth": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Allow only Azure AD authentication. Should be enabled for security reasons." - } - }, - "customerManagedKey": { - "$ref": "#/definitions/customerManagedKeyType", - "nullable": true, - "metadata": { - "description": "Optional. The customer managed key definition." - } - }, - "dynamicThrottlingEnabled": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. The flag to enable dynamic throttling." - } - }, - "migrationToken": { - "type": "securestring", - "nullable": true, - "metadata": { - "description": "Optional. Resource migration token." - } - }, - "restore": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Restore a soft-deleted cognitive service at deployment time. Will fail if no such soft-deleted resource exists." - } - }, - "restrictOutboundNetworkAccess": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Restrict outbound network access." - } - }, - "userOwnedStorage": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.CognitiveServices/accounts@2025-04-01-preview#properties/properties/properties/userOwnedStorage" - }, - "description": "Optional. The storage accounts for this resource." - }, - "nullable": true - }, - "managedIdentities": { - "$ref": "#/definitions/managedIdentityAllType", - "nullable": true, - "metadata": { - "description": "Optional. The managed identity definition for this resource." - } - }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." - } - }, - "deployments": { - "type": "array", - "items": { - "$ref": "#/definitions/deploymentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of deployments about cognitive service accounts to create." - } - }, - "secretsExportConfiguration": { - "$ref": "#/definitions/secretsExportConfigurationType", - "nullable": true, - "metadata": { - "description": "Optional. Key vault reference and secret settings for the module's secrets export." - } - }, - "allowProjectManagement": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable/Disable project management feature for AI Foundry." - } - }, - "commitmentPlans": { - "type": "array", - "items": { - "$ref": "#/definitions/commitmentPlanType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Commitment plans to deploy for the cognitive services account." - } - } - }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" - } - ], - "enableReferencedModulesTelemetry": false, - "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", - "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned, UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', null())), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", - "builtInRoleNames": { - "Cognitive Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68')]", - "Cognitive Services Custom Vision Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c1ff6cc2-c111-46fe-8896-e0ef812ad9f3')]", - "Cognitive Services Custom Vision Deployment": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5c4089e1-6d96-4d2f-b296-c1bc7137275f')]", - "Cognitive Services Custom Vision Labeler": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '88424f51-ebe7-446f-bc41-7fa16989e96c')]", - "Cognitive Services Custom Vision Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '93586559-c37d-4a6b-ba08-b9f0940c2d73')]", - "Cognitive Services Custom Vision Trainer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a5ae4ab-0d65-4eeb-be61-29fc9b54394b')]", - "Cognitive Services Data Reader (Preview)": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b59867f0-fa02-499b-be73-45a86b5b3e1c')]", - "Cognitive Services Face Recognizer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '9894cab4-e18a-44aa-828b-cb588cd6f2d7')]", - "Cognitive Services Immersive Reader User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b2de6794-95db-4659-8781-7e080d3f2b9d')]", - "Cognitive Services Language Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f07febfe-79bc-46b1-8b37-790e26e6e498')]", - "Cognitive Services Language Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7628b7b8-a8b2-4cdc-b46f-e9b35248918e')]", - "Cognitive Services Language Writer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f2310ca1-dc64-4889-bb49-c8e0fa3d47a8')]", - "Cognitive Services LUIS Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f72c8140-2111-481c-87ff-72b910f6e3f8')]", - "Cognitive Services LUIS Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18e81cdc-4e98-4e29-a639-e7d10c5a6226')]", - "Cognitive Services LUIS Writer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '6322a993-d5c9-4bed-b113-e49bbea25b27')]", - "Cognitive Services Metrics Advisor Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'cb43c632-a144-4ec5-977c-e80c4affc34a')]", - "Cognitive Services Metrics Advisor User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '3b20f47b-3825-43cb-8114-4bd2201156a8')]", - "Cognitive Services OpenAI Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a001fd3d-188f-4b5d-821b-7da978bf7442')]", - "Cognitive Services OpenAI User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", - "Cognitive Services QnA Maker Editor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f4cc2bf9-21be-47a1-bdf1-5c5804381025')]", - "Cognitive Services QnA Maker Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '466ccd10-b268-4a11-b098-b4849f024126')]", - "Cognitive Services Speech Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0e75ca1e-0464-4b4d-8b93-68208a576181')]", - "Cognitive Services Speech User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f2dc8367-1007-4938-bd23-fe263f013447')]", - "Cognitive Services User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908')]", - "Azure AI Developer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '64702f94-c441-49e6-a78b-ef80e0188fee')]", - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", - "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" - }, - "isHSMManagedCMK": "[equals(tryGet(split(coalesce(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), ''), '/'), 7), 'managedHSMs')]" - }, - "resources": { - "cMKKeyVault::cMKKey": { - "condition": "[and(and(not(empty(parameters('customerManagedKey'))), not(variables('isHSMManagedCMK'))), and(not(empty(parameters('customerManagedKey'))), not(variables('isHSMManagedCMK'))))]", - "existing": true, - "type": "Microsoft.KeyVault/vaults/keys", - "apiVersion": "2025-05-01", - "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[2]]", - "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[4]]", - "name": "[format('{0}/{1}', last(split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')), tryGet(parameters('customerManagedKey'), 'keyName'))]" - }, - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.cognitiveservices-account.{0}.{1}', replace('0.14.1', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } - } - } - } - }, - "cMKKeyVault": { - "condition": "[and(not(empty(parameters('customerManagedKey'))), not(variables('isHSMManagedCMK')))]", - "existing": true, - "type": "Microsoft.KeyVault/vaults", - "apiVersion": "2025-05-01", - "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[2]]", - "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[4]]", - "name": "[last(split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/'))]" - }, - "cMKUserAssignedIdentity": { - "condition": "[not(empty(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId')))]", - "existing": true, - "type": "Microsoft.ManagedIdentity/userAssignedIdentities", - "apiVersion": "2025-01-31-preview", - "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[2]]", - "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[4]]", - "name": "[last(split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/'))]" - }, - "cognitiveService": { - "type": "Microsoft.CognitiveServices/accounts", - "apiVersion": "2025-06-01", - "name": "[parameters('name')]", - "kind": "[parameters('kind')]", - "identity": "[variables('identity')]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "sku": { - "name": "[parameters('sku')]" - }, - "properties": { - "allowProjectManagement": "[parameters('allowProjectManagement')]", - "customSubDomainName": "[parameters('customSubDomainName')]", - "networkAcls": "[if(not(empty(coalesce(parameters('networkAcls'), createObject()))), createObject('defaultAction', tryGet(parameters('networkAcls'), 'defaultAction'), 'virtualNetworkRules', coalesce(tryGet(parameters('networkAcls'), 'virtualNetworkRules'), createArray()), 'ipRules', coalesce(tryGet(parameters('networkAcls'), 'ipRules'), createArray())), null())]", - "networkInjections": "[if(not(empty(parameters('networkInjections'))), createArray(createObject('scenario', tryGet(parameters('networkInjections'), 'scenario'), 'subnetArmId', tryGet(parameters('networkInjections'), 'subnetResourceId'), 'useMicrosoftManagedNetwork', coalesce(tryGet(parameters('networkInjections'), 'useMicrosoftManagedNetwork'), false()))), null())]", - "publicNetworkAccess": "[if(not(equals(parameters('publicNetworkAccess'), null())), parameters('publicNetworkAccess'), if(not(empty(parameters('networkAcls'))), 'Enabled', 'Disabled'))]", - "allowedFqdnList": "[parameters('allowedFqdnList')]", - "apiProperties": "[parameters('apiProperties')]", - "disableLocalAuth": "[parameters('disableLocalAuth')]", - "encryption": "[if(not(empty(parameters('customerManagedKey'))), createObject('keySource', 'Microsoft.KeyVault', 'keyVaultProperties', createObject('identityClientId', if(not(empty(coalesce(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), ''))), reference('cMKUserAssignedIdentity').clientId, null()), 'keyVaultUri', if(not(variables('isHSMManagedCMK')), reference('cMKKeyVault').vaultUri, format('https://{0}.managedhsm.azure.net/', last(split(parameters('customerManagedKey').keyVaultResourceId, '/')))), 'keyName', parameters('customerManagedKey').keyName, 'keyVersion', if(not(empty(tryGet(parameters('customerManagedKey'), 'keyVersion'))), parameters('customerManagedKey').keyVersion, if(not(variables('isHSMManagedCMK')), last(split(reference('cMKKeyVault::cMKKey').keyUriWithVersion, '/')), fail('Managed HSM CMK encryption requires specifying the ''keyVersion''.'))))), null())]", - "migrationToken": "[parameters('migrationToken')]", - "restore": "[parameters('restore')]", - "restrictOutboundNetworkAccess": "[parameters('restrictOutboundNetworkAccess')]", - "userOwnedStorage": "[if(not(empty(parameters('userOwnedStorage'))), parameters('userOwnedStorage'), null())]", - "dynamicThrottlingEnabled": "[parameters('dynamicThrottlingEnabled')]" - }, - "dependsOn": [ - "cMKKeyVault", - "cMKKeyVault::cMKKey", - "cMKUserAssignedIdentity" - ] - }, - "cognitiveService_deployments": { - "copy": { - "name": "cognitiveService_deployments", - "count": "[length(coalesce(parameters('deployments'), createArray()))]", - "mode": "serial", - "batchSize": 1 - }, - "type": "Microsoft.CognitiveServices/accounts/deployments", - "apiVersion": "2025-06-01", - "name": "[format('{0}/{1}', parameters('name'), coalesce(tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'name'), format('{0}-deployments', parameters('name'))))]", - "properties": { - "model": "[coalesce(parameters('deployments'), createArray())[copyIndex()].model]", - "raiPolicyName": "[tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'raiPolicyName')]", - "versionUpgradeOption": "[tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'versionUpgradeOption')]" - }, - "sku": "[coalesce(tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'sku'), createObject('name', parameters('sku'), 'capacity', tryGet(parameters('sku'), 'capacity'), 'tier', tryGet(parameters('sku'), 'tier'), 'size', tryGet(parameters('sku'), 'size'), 'family', tryGet(parameters('sku'), 'family')))]", - "dependsOn": [ - "cognitiveService" - ] - }, - "cognitiveService_lock": { - "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", - "type": "Microsoft.Authorization/locks", - "apiVersion": "2020-05-01", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", - "properties": { - "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", - "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" - }, - "dependsOn": [ - "cognitiveService" - ] - }, - "cognitiveService_commitmentPlans": { - "copy": { - "name": "cognitiveService_commitmentPlans", - "count": "[length(coalesce(parameters('commitmentPlans'), createArray()))]" - }, - "type": "Microsoft.CognitiveServices/accounts/commitmentPlans", - "apiVersion": "2025-06-01", - "name": "[format('{0}/{1}', parameters('name'), format('{0}-{1}', coalesce(parameters('commitmentPlans'), createArray())[copyIndex()].hostingModel, coalesce(parameters('commitmentPlans'), createArray())[copyIndex()].planType))]", - "properties": "[coalesce(parameters('commitmentPlans'), createArray())[copyIndex()]]", - "dependsOn": [ - "cognitiveService" - ] - }, - "cognitiveService_diagnosticSettings": { - "copy": { - "name": "cognitiveService_diagnosticSettings", - "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" - }, - "type": "Microsoft.Insights/diagnosticSettings", - "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", - "properties": { - "copy": [ - { - "name": "metrics", - "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", - "input": { - "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", - "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", - "timeGrain": null - } - }, - { - "name": "logs", - "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", - "input": { - "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", - "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", - "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" - } - } - ], - "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", - "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", - "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", - "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", - "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", - "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" - }, - "dependsOn": [ - "cognitiveService" - ] - }, - "cognitiveService_roleAssignments": { - "copy": { - "name": "cognitiveService_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", - "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" - }, - "dependsOn": [ - "cognitiveService" - ] - }, - "cognitiveService_privateEndpoints": { - "copy": { - "name": "cognitiveService_privateEndpoints", - "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]" - }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-cognitiveService-PrivateEndpoint-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", - "subscriptionId": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[2]]", - "resourceGroup": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[4]]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'name'), format('pep-{0}-{1}-{2}', last(split(resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'account'), copyIndex()))]" - }, - "privateLinkServiceConnections": "[if(not(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true())), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'account'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'account')))))), createObject('value', null()))]", - "manualPrivateLinkServiceConnections": "[if(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true()), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'account'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'account')), 'requestMessage', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'manualConnectionRequestMessage'), 'Manual approval required.'))))), createObject('value', null()))]", - "subnetResourceId": { - "value": "[coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId]" - }, - "enableTelemetry": { - "value": "[variables('enableReferencedModulesTelemetry')]" - }, - "location": { - "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'location'), reference(split(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location)]" - }, - "lock": { - "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'lock'), parameters('lock'))]" - }, - "privateDnsZoneGroup": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateDnsZoneGroup')]" - }, - "roleAssignments": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'roleAssignments')]" - }, - "tags": { - "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" - }, - "customDnsConfigs": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customDnsConfigs')]" - }, - "ipConfigurations": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'ipConfigurations')]" - }, - "applicationSecurityGroupResourceIds": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'applicationSecurityGroupResourceIds')]" - }, - "customNetworkInterfaceName": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customNetworkInterfaceName')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.38.5.1644", - "templateHash": "16604612898799598358" - }, - "name": "Private Endpoints", - "description": "This module deploys a Private Endpoint." - }, - "definitions": { - "privateDnsZoneGroupType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the Private DNS Zone Group." - } - }, - "privateDnsZoneGroupConfigs": { - "type": "array", - "items": { - "$ref": "#/definitions/privateDnsZoneGroupConfigType" - }, - "metadata": { - "description": "Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type of a private dns zone group." - } - }, - "lockType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the name of lock." - } - }, - "kind": { - "type": "string", - "allowedValues": [ - "CanNotDelete", - "None", - "ReadOnly" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specify the type of lock." - } - }, - "notes": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the notes of the lock." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a lock.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "privateDnsZoneGroupConfigType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the private DNS zone group config." - } - }, - "privateDnsZoneResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource id of the private DNS zone." - } - } - }, - "metadata": { - "description": "The type of a private DNS zone group configuration.", - "__bicep_imported_from!": { - "sourceTemplate": "private-dns-zone-group/main.bicep" - } - } - }, - "roleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - } - }, - "parameters": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the private endpoint resource to create." - } - }, - "subnetResourceId": { - "type": "string", - "metadata": { - "description": "Required. Resource ID of the subnet where the endpoint needs to be created." - } - }, - "applicationSecurityGroupResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "metadata": { - "description": "Optional. Application security groups in which the private endpoint IP configuration is included." - } - }, - "customNetworkInterfaceName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The custom name of the network interface attached to the private endpoint." - } - }, - "ipConfigurations": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/ipConfigurations" - }, - "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." - }, - "nullable": true - }, - "privateDnsZoneGroup": { - "$ref": "#/definitions/privateDnsZoneGroupType", - "nullable": true, - "metadata": { - "description": "Optional. The private DNS zone group to configure for the private endpoint." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Optional. Location for all Resources." - } - }, - "lock": { - "$ref": "#/definitions/lockType", - "nullable": true, - "metadata": { - "description": "Optional. The lock settings of the service." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/tags" - }, - "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." - }, - "nullable": true - }, - "customDnsConfigs": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/customDnsConfigs" - }, - "description": "Optional. Custom DNS configurations." - }, - "nullable": true - }, - "manualPrivateLinkServiceConnections": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/manualPrivateLinkServiceConnections" - }, - "description": "Conditional. A grouping of information about the connection to the remote resource. Used when the network admin does not have access to approve connections to the remote resource. Required if `privateLinkServiceConnections` is empty." - }, - "nullable": true - }, - "privateLinkServiceConnections": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/privateLinkServiceConnections" - }, - "description": "Conditional. A grouping of information about the connection to the remote resource. Required if `manualPrivateLinkServiceConnections` is empty." - }, - "nullable": true - }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." - } - } - }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" - } - ], - "builtInRoleNames": { - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", - "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", - "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", - "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", - "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" - } - }, - "resources": { - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('46d3xbcp.res.network-privateendpoint.{0}.{1}', replace('0.11.1', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } - } - } - } - }, - "privateEndpoint": { - "type": "Microsoft.Network/privateEndpoints", - "apiVersion": "2024-10-01", - "name": "[parameters('name')]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "properties": { - "copy": [ - { - "name": "applicationSecurityGroups", - "count": "[length(coalesce(parameters('applicationSecurityGroupResourceIds'), createArray()))]", - "input": { - "id": "[coalesce(parameters('applicationSecurityGroupResourceIds'), createArray())[copyIndex('applicationSecurityGroups')]]" - } - } - ], - "customDnsConfigs": "[coalesce(parameters('customDnsConfigs'), createArray())]", - "customNetworkInterfaceName": "[coalesce(parameters('customNetworkInterfaceName'), '')]", - "ipConfigurations": "[coalesce(parameters('ipConfigurations'), createArray())]", - "manualPrivateLinkServiceConnections": "[coalesce(parameters('manualPrivateLinkServiceConnections'), createArray())]", - "privateLinkServiceConnections": "[coalesce(parameters('privateLinkServiceConnections'), createArray())]", - "subnet": { - "id": "[parameters('subnetResourceId')]" - } - } - }, - "privateEndpoint_lock": { - "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", - "type": "Microsoft.Authorization/locks", - "apiVersion": "2020-05-01", - "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", - "properties": { - "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", - "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" - }, - "dependsOn": [ - "privateEndpoint" - ] - }, - "privateEndpoint_roleAssignments": { - "copy": { - "name": "privateEndpoint_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateEndpoints', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", - "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + "value": "[reference('privateEndpoint').customDnsConfigs]" + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The resource IDs of the network interfaces associated with the private endpoint." + }, + "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group Id for the private endpoint Group." + }, + "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" + } + } + } }, "dependsOn": [ - "privateEndpoint" + "cognitiveService_deployments" ] }, - "privateEndpoint_privateDnsZoneGroup": { - "condition": "[not(empty(parameters('privateDnsZoneGroup')))]", + "aiProject": { + "condition": "[or(not(empty(parameters('projectName'))), not(empty(parameters('existingFoundryProjectResourceId'))))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", + "name": "[take(format('{0}-ai-project-{1}-deployment', parameters('name'), parameters('projectName')), 64)]", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -30388,13 +28580,22 @@ "mode": "Incremental", "parameters": { "name": { - "value": "[tryGet(parameters('privateDnsZoneGroup'), 'name')]" + "value": "[parameters('projectName')]" }, - "privateEndpointName": { + "desc": { + "value": "[parameters('projectDescription')]" + }, + "aiServicesName": { "value": "[parameters('name')]" }, - "privateDnsZoneConfigs": { - "value": "[parameters('privateDnsZoneGroup').privateDnsZoneGroupConfigs]" + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "existingFoundryProjectResourceId": { + "value": "[parameters('existingFoundryProjectResourceId')]" } }, "template": { @@ -30404,330 +28605,175 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.5.1644", - "templateHash": "24141742673128945" - }, - "name": "Private Endpoint Private DNS Zone Groups", - "description": "This module deploys a Private Endpoint Private DNS Zone Group." + "version": "0.43.8.12551", + "templateHash": "9910113619698591130" + } }, "definitions": { - "privateDnsZoneGroupConfigType": { + "aiProjectOutputType": { "type": "object", "properties": { "name": { "type": "string", - "nullable": true, "metadata": { - "description": "Optional. The name of the private DNS zone group config." + "description": "Required. Name of the AI project." } }, - "privateDnsZoneResourceId": { + "resourceId": { "type": "string", "metadata": { - "description": "Required. The resource id of the private DNS zone." + "description": "Required. Resource ID of the AI project." + } + }, + "apiEndpoint": { + "type": "string", + "metadata": { + "description": "Required. API endpoint for the AI project." + } + }, + "aiprojectSystemAssignedMIPrincipalId": { + "type": "string", + "metadata": { + "description": "Required. System Assigned Managed Identity Principal Id of the AI project." } } }, "metadata": { "__bicep_export!": true, - "description": "The type of a private DNS zone group configuration." + "description": "Output type representing AI project information." } } }, "parameters": { - "privateEndpointName": { + "name": { "type": "string", "metadata": { - "description": "Conditional. The name of the parent private endpoint. Required if the template is used in a standalone deployment." + "description": "Required. Name of the AI Services project." } }, - "privateDnsZoneConfigs": { - "type": "array", - "items": { - "$ref": "#/definitions/privateDnsZoneGroupConfigType" - }, - "minLength": 1, - "maxLength": 5, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", "metadata": { - "description": "Required. Array of private DNS zone configurations of the private DNS zone group. A DNS zone group can support up to 5 DNS zones." + "description": "Required. The location of the Project resource." } }, - "name": { + "desc": { "type": "string", - "defaultValue": "default", + "defaultValue": "[parameters('name')]", "metadata": { - "description": "Optional. The name of the private DNS zone group." + "description": "Optional. The description of the AI Foundry project to create. Defaults to the project name." + } + }, + "aiServicesName": { + "type": "string", + "metadata": { + "description": "Required. Name of the existing Cognitive Services resource to create the AI Foundry project in." + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Tags to be applied to the resources." + } + }, + "existingFoundryProjectResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Use this parameter to use an existing AI project resource ID from different resource group" } } }, + "variables": { + "useExistingProject": "[not(empty(parameters('existingFoundryProjectResourceId')))]", + "existingProjName": "[if(variables('useExistingProject'), last(split(parameters('existingFoundryProjectResourceId'), '/')), '')]", + "existingAiFoundryAiServicesSubscriptionId": "[if(variables('useExistingProject'), split(parameters('existingFoundryProjectResourceId'), '/')[2], '')]", + "existingAiFoundryAiServicesResourceGroupName": "[if(variables('useExistingProject'), split(parameters('existingFoundryProjectResourceId'), '/')[4], '')]", + "existingAiFoundryAiServicesServiceName": "[if(variables('useExistingProject'), split(parameters('existingFoundryProjectResourceId'), '/')[8], '')]", + "existingProjEndpoint": "[if(variables('useExistingProject'), format('https://{0}.services.ai.azure.com/api/projects/{1}', variables('existingAiFoundryAiServicesServiceName'), variables('existingProjName')), '')]" + }, "resources": { - "privateEndpoint": { + "cogServiceReference": { "existing": true, - "type": "Microsoft.Network/privateEndpoints", - "apiVersion": "2024-10-01", - "name": "[parameters('privateEndpointName')]" + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2025-06-01", + "name": "[parameters('aiServicesName')]" }, - "privateDnsZoneGroup": { - "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", - "apiVersion": "2024-10-01", - "name": "[format('{0}/{1}', parameters('privateEndpointName'), parameters('name'))]", + "aiProject": { + "condition": "[not(variables('useExistingProject'))]", + "type": "Microsoft.CognitiveServices/accounts/projects", + "apiVersion": "2025-06-01", + "name": "[format('{0}/{1}', parameters('aiServicesName'), parameters('name'))]", + "tags": "[parameters('tags')]", + "location": "[parameters('location')]", + "identity": { + "type": "SystemAssigned" + }, "properties": { - "copy": [ - { - "name": "privateDnsZoneConfigs", - "count": "[length(parameters('privateDnsZoneConfigs'))]", - "input": { - "name": "[coalesce(tryGet(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigs')], 'name'), last(split(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigs')].privateDnsZoneResourceId, '/')))]", - "properties": { - "privateDnsZoneId": "[parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigs')].privateDnsZoneResourceId]" - } - } - } - ] + "description": "[parameters('desc')]", + "displayName": "[parameters('name')]" } + }, + "existingAiProject": { + "condition": "[variables('useExistingProject')]", + "existing": true, + "type": "Microsoft.CognitiveServices/accounts/projects", + "apiVersion": "2025-06-01", + "subscriptionId": "[variables('existingAiFoundryAiServicesSubscriptionId')]", + "resourceGroup": "[variables('existingAiFoundryAiServicesResourceGroupName')]", + "name": "[format('{0}/{1}', variables('existingAiFoundryAiServicesServiceName'), variables('existingProjName'))]" } }, "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the private endpoint DNS zone group." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the private endpoint DNS zone group." - }, - "value": "[resourceId('Microsoft.Network/privateEndpoints/privateDnsZoneGroups', parameters('privateEndpointName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", + "aiProjectInfo": { + "$ref": "#/definitions/aiProjectOutputType", "metadata": { - "description": "The resource group the private endpoint DNS zone group was deployed into." + "description": "AI Project metadata including name, resource ID, and API endpoint." }, - "value": "[resourceGroup().name]" + "value": { + "name": "[if(variables('useExistingProject'), variables('existingProjName'), parameters('name'))]", + "resourceId": "[if(variables('useExistingProject'), parameters('existingFoundryProjectResourceId'), resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('aiServicesName'), parameters('name')))]", + "apiEndpoint": "[if(variables('useExistingProject'), variables('existingProjEndpoint'), reference('aiProject').endpoints['AI Foundry API'])]", + "aiprojectSystemAssignedMIPrincipalId": "[if(variables('useExistingProject'), reference('existingAiProject', '2025-06-01', 'full').identity.principalId, reference('aiProject', '2025-06-01', 'full').identity.principalId)]" + } } } } }, "dependsOn": [ - "privateEndpoint" + "cognitiveService_deployments" ] } }, "outputs": { - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group the private endpoint was deployed into." - }, - "value": "[resourceGroup().name]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the private endpoint." - }, - "value": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]" - }, - "name": { - "type": "string", - "metadata": { - "description": "The name of the private endpoint." - }, - "value": "[parameters('name')]" - }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." - }, - "value": "[reference('privateEndpoint', '2024-10-01', 'full').location]" - }, - "customDnsConfigs": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/customDnsConfigs", - "output": true - }, - "description": "The custom DNS configurations of the private endpoint." - }, - "value": "[reference('privateEndpoint').customDnsConfigs]" - }, - "networkInterfaceResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "The resource IDs of the network interfaces associated with the private endpoint." - }, - "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" - }, - "groupId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "The group Id for the private endpoint Group." - }, - "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" - } - } - } - }, - "dependsOn": [ - "cognitiveService" - ] - }, - "secretsExport": { - "condition": "[not(equals(parameters('secretsExportConfiguration'), null()))]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-secrets-kv', uniqueString(deployment().name, parameters('location')))]", - "subscriptionId": "[split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/')[2]]", - "resourceGroup": "[split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/')[4]]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "keyVaultName": { - "value": "[last(split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/'))]" - }, - "secretsToSet": { - "value": "[union(createArray(), if(contains(parameters('secretsExportConfiguration'), 'accessKey1Name'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'accessKey1Name'), 'value', listKeys('cognitiveService', '2025-06-01').key1)), createArray()), if(contains(parameters('secretsExportConfiguration'), 'accessKey2Name'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'accessKey2Name'), 'value', listKeys('cognitiveService', '2025-06-01').key2)), createArray()))]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "356315690886888607" - } - }, - "definitions": { - "secretSetOutputType": { - "type": "object", - "properties": { - "secretResourceId": { - "type": "string", - "metadata": { - "description": "The resourceId of the exported secret." - } - }, - "secretUri": { - "type": "string", - "metadata": { - "description": "The secret URI of the exported secret." - } - }, - "secretUriWithVersion": { - "type": "string", - "metadata": { - "description": "The secret URI with version of the exported secret." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for the output of the secret set via the secrets export feature.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "secretToSetType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the secret to set." - } - }, - "value": { - "type": "securestring", - "metadata": { - "description": "Required. The value of the secret to set." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for the secret to set via the secrets export feature.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - } - }, - "parameters": { - "keyVaultName": { - "type": "string", - "metadata": { - "description": "Required. The name of the Key Vault to set the ecrets in." - } - }, - "secretsToSet": { - "type": "array", - "items": { - "$ref": "#/definitions/secretToSetType" - }, - "metadata": { - "description": "Required. The secrets to set in the Key Vault." - } - } - }, - "resources": { - "keyVault": { - "existing": true, - "type": "Microsoft.KeyVault/vaults", - "apiVersion": "2025-05-01", - "name": "[parameters('keyVaultName')]" - }, - "secrets": { - "copy": { - "name": "secrets", - "count": "[length(parameters('secretsToSet'))]" - }, - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2025-05-01", - "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('secretsToSet')[copyIndex()].name)]", - "properties": { - "value": "[parameters('secretsToSet')[copyIndex()].value]" - } - } - }, - "outputs": { - "secretsSet": { + "privateEndpoints": { "type": "array", "items": { - "$ref": "#/definitions/secretSetOutputType" + "$ref": "#/definitions/privateEndpointOutputType" }, "metadata": { - "description": "The references to the secrets exported to the provided Key Vault." + "description": "The private endpoints of the congitive services account." }, "copy": { - "count": "[length(range(0, length(coalesce(parameters('secretsToSet'), createArray()))))]", + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]", "input": { - "secretResourceId": "[resourceId('Microsoft.KeyVault/vaults/secrets', parameters('keyVaultName'), parameters('secretsToSet')[range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()]].name)]", - "secretUri": "[reference(format('secrets[{0}]', range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()])).secretUri]", - "secretUriWithVersion": "[reference(format('secrets[{0}]', range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()])).secretUriWithVersion]" + "name": "[reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs.name.value]", + "resourceId": "[reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs.resourceId.value]", + "groupId": "[tryGet(tryGet(reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs, 'groupId'), 'value')]", + "customDnsConfigs": "[reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs.customDnsConfigs.value]", + "networkInterfaceResourceIds": "[reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs.networkInterfaceResourceIds.value]" } } + }, + "aiProjectInfo": { + "$ref": "#/definitions/aiProjectOutputType", + "value": "[reference('aiProject').outputs.aiProjectInfo.value]" } } } - }, - "dependsOn": [ - "cognitiveService" - ] + } } }, "outputs": { @@ -30736,35 +28782,42 @@ "metadata": { "description": "The name of the cognitive services account." }, - "value": "[parameters('name')]" + "value": "[if(variables('useExistingService'), variables('existingCognitiveServiceDetails')[8], parameters('name'))]" }, "resourceId": { "type": "string", "metadata": { "description": "The resource ID of the cognitive services account." }, - "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('name'))]" + "value": "[if(variables('useExistingService'), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('existingCognitiveServiceDetails')[2], variables('existingCognitiveServiceDetails')[4]), 'Microsoft.CognitiveServices/accounts', variables('existingCognitiveServiceDetails')[8]), resourceId('Microsoft.CognitiveServices/accounts', parameters('name')))]" + }, + "subscriptionId": { + "type": "string", + "metadata": { + "description": "The resource group the cognitive services account was deployed into." + }, + "value": "[if(variables('useExistingService'), variables('existingCognitiveServiceDetails')[2], subscription().subscriptionId)]" }, "resourceGroupName": { "type": "string", "metadata": { "description": "The resource group the cognitive services account was deployed into." }, - "value": "[resourceGroup().name]" + "value": "[if(variables('useExistingService'), variables('existingCognitiveServiceDetails')[4], resourceGroup().name)]" }, "endpoint": { "type": "string", "metadata": { "description": "The service endpoint of the cognitive services account." }, - "value": "[reference('cognitiveService').endpoint]" + "value": "[if(variables('useExistingService'), reference('cognitiveServiceExisting').endpoint, if(variables('useExistingService'), reference('cognitiveServiceExisting', '2025-09-01', 'full'), reference('cognitiveServiceNew', '2025-06-01', 'full')).properties.endpoint)]" }, "endpoints": { "$ref": "#/definitions/endpointType", "metadata": { "description": "All endpoints available for the cognitive services account, types depends on the cognitive service kind." }, - "value": "[reference('cognitiveService').endpoints]" + "value": "[if(variables('useExistingService'), reference('cognitiveServiceExisting').endpoints, if(variables('useExistingService'), reference('cognitiveServiceExisting', '2025-09-01', 'full'), reference('cognitiveServiceNew', '2025-06-01', 'full')).properties.endpoints)]" }, "systemAssignedMIPrincipalId": { "type": "string", @@ -30772,21 +28825,14 @@ "metadata": { "description": "The principal ID of the system assigned identity." }, - "value": "[tryGet(tryGet(reference('cognitiveService', '2025-06-01', 'full'), 'identity'), 'principalId')]" + "value": "[if(variables('useExistingService'), reference('cognitiveServiceExisting', '2025-09-01', 'full').identity.principalId, tryGet(tryGet(if(variables('useExistingService'), reference('cognitiveServiceExisting', '2025-09-01', 'full'), reference('cognitiveServiceNew', '2025-06-01', 'full')), 'identity'), 'principalId'))]" }, "location": { "type": "string", "metadata": { "description": "The location the resource was deployed into." }, - "value": "[reference('cognitiveService', '2025-06-01', 'full').location]" - }, - "exportedSecrets": { - "$ref": "#/definitions/secretsOutputType", - "metadata": { - "description": "A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret's name." - }, - "value": "[if(not(equals(parameters('secretsExportConfiguration'), null())), toObject(reference('secretsExport').outputs.secretsSet.value, lambda('secret', last(split(lambdaVariables('secret').secretResourceId, '/'))), lambda('secret', lambdaVariables('secret'))), createObject())]" + "value": "[if(variables('useExistingService'), reference('cognitiveServiceExisting', '2025-09-01', 'full').location, if(variables('useExistingService'), reference('cognitiveServiceExisting', '2025-09-01', 'full'), reference('cognitiveServiceNew', '2025-06-01', 'full')).location)]" }, "privateEndpoints": { "type": "array", @@ -30796,46 +28842,26 @@ "metadata": { "description": "The private endpoints of the congitive services account." }, - "copy": { - "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]", - "input": { - "name": "[reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs.name.value]", - "resourceId": "[reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs.resourceId.value]", - "groupId": "[tryGet(tryGet(reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs, 'groupId'), 'value')]", - "customDnsConfigs": "[reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs.customDnsConfigs.value]", - "networkInterfaceResourceIds": "[reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs.networkInterfaceResourceIds.value]" - } - } - }, - "primaryKey": { - "type": "securestring", - "nullable": true, - "metadata": { - "description": "The primary access key." - }, - "value": "[if(not(parameters('disableLocalAuth')), listKeys('cognitiveService', '2025-06-01').key1, null())]" + "value": "[if(variables('useExistingService'), reference('existing_cognitive_service_dependencies').outputs.privateEndpoints.value, reference('cognitive_service_dependencies').outputs.privateEndpoints.value)]" }, - "secondaryKey": { - "type": "securestring", - "nullable": true, - "metadata": { - "description": "The secondary access key." - }, - "value": "[if(not(parameters('disableLocalAuth')), listKeys('cognitiveService', '2025-06-01').key2, null())]" + "aiProjectInfo": { + "$ref": "#/definitions/aiProjectOutputType", + "value": "[if(variables('useExistingService'), reference('existing_cognitive_service_dependencies').outputs.aiProjectInfo.value, reference('cognitive_service_dependencies').outputs.aiProjectInfo.value)]" } } } }, "dependsOn": [ + "backendUserAssignedIdentity", "logAnalyticsWorkspace", "userAssignedIdentity" ] }, - "cognitiveServicesCuPrivateEndpoint": { - "condition": "[parameters('enablePrivateNetworking')]", + "aiFoundryPrivateEndpoint": { + "condition": "[and(parameters('enablePrivateNetworking'), not(variables('useExistingAiFoundryAiProject')))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[take(format('pep-{0}-deployment', variables('aiFoundryAiServicesCUResourceName')), 64)]", + "name": "[take(format('pep-{0}-deployment', variables('aiFoundryAiServicesResourceName')), 64)]", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -30843,10 +28869,10 @@ "mode": "Incremental", "parameters": { "name": { - "value": "[format('pep-{0}', variables('aiFoundryAiServicesCUResourceName'))]" + "value": "[format('pep-{0}', variables('aiFoundryAiServicesResourceName'))]" }, "customNetworkInterfaceName": { - "value": "[format('nic-{0}', variables('aiFoundryAiServicesCUResourceName'))]" + "value": "[format('nic-{0}', variables('aiFoundryAiServicesResourceName'))]" }, "location": { "value": "[parameters('location')]" @@ -30857,9 +28883,9 @@ "privateLinkServiceConnections": { "value": [ { - "name": "[format('pep-{0}-connection', variables('aiFoundryAiServicesCUResourceName'))]", + "name": "[format('pep-{0}-connection', variables('aiFoundryAiServicesResourceName'))]", "properties": { - "privateLinkServiceId": "[reference('cognitiveServicesCu').outputs.resourceId.value]", + "privateLinkServiceId": "[reference('aiFoundryAiServices').outputs.resourceId.value]", "groupIds": [ "account" ] @@ -30871,15 +28897,15 @@ "value": { "privateDnsZoneGroupConfigs": [ { - "name": "ai-services-cu-dns-zone-cognitiveservices", + "name": "ai-services-dns-zone-cognitiveservices", "privateDnsZoneResourceId": "[reference(format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)).outputs.resourceId.value]" }, { - "name": "ai-services-cu-dns-zone-openai", + "name": "ai-services-dns-zone-openai", "privateDnsZoneResourceId": "[reference(format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)).outputs.resourceId.value]" }, { - "name": "ai-services-cu-dns-zone-aiservices", + "name": "ai-services-dns-zone-aiservices", "privateDnsZoneResourceId": "[reference(format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').aiServices)).outputs.resourceId.value]" } ] @@ -31600,10 +29626,10 @@ } }, "dependsOn": [ - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", + "aiFoundryAiServices", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').aiServices)]", - "cognitiveServicesCu", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", "virtualNetwork" ] }, @@ -31620,6 +29646,9 @@ "name": { "value": "[variables('aiSearchName')]" }, + "location": { + "value": "[parameters('location')]" + }, "enableTelemetry": { "value": "[parameters('enableTelemetry')]" }, @@ -33781,8 +31810,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "13998466922971349048" + "version": "0.43.8.12551", + "templateHash": "2967490583129753981" } }, "parameters": { @@ -33876,8 +31905,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "15810098719925301401" + "version": "0.43.8.12551", + "templateHash": "13704406247129243046" } }, "parameters": { @@ -34000,6 +32029,9 @@ "value": false }, "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), createObject('value', 'Disabled'), createObject('value', 'Enabled'))]", + "requireInfrastructureEncryption": { + "value": true + }, "privateEndpoints": "[if(parameters('enablePrivateNetworking'), createObject('value', createArray(createObject('name', format('pep-blob-{0}', variables('solutionSuffix')), 'service', 'blob', 'subnetResourceId', reference('virtualNetwork').outputs.pepsSubnetResourceId.value, 'privateDnsZoneGroup', createObject('privateDnsZoneGroupConfigs', createArray(createObject('name', 'storage-dns-zone-group-blob', 'privateDnsZoneResourceId', reference(format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageBlob)).outputs.resourceId.value)))), createObject('name', format('pep-queue-{0}', variables('solutionSuffix')), 'service', 'queue', 'subnetResourceId', reference('virtualNetwork').outputs.pepsSubnetResourceId.value, 'privateDnsZoneGroup', createObject('privateDnsZoneGroupConfigs', createArray(createObject('name', 'storage-dns-zone-group-queue', 'privateDnsZoneResourceId', reference(format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageQueue)).outputs.resourceId.value)))), createObject('name', format('pep-file-{0}', variables('solutionSuffix')), 'service', 'file', 'subnetResourceId', reference('virtualNetwork').outputs.pepsSubnetResourceId.value, 'privateDnsZoneGroup', createObject('privateDnsZoneGroupConfigs', createArray(createObject('name', 'storage-dns-zone-group-file', 'privateDnsZoneResourceId', reference(format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageFile)).outputs.resourceId.value)))), createObject('name', format('pep-dfs-{0}', variables('solutionSuffix')), 'service', 'dfs', 'subnetResourceId', reference('virtualNetwork').outputs.pepsSubnetResourceId.value, 'privateDnsZoneGroup', createObject('privateDnsZoneGroupConfigs', createArray(createObject('name', 'storage-dns-zone-group-dfs', 'privateDnsZoneResourceId', reference(format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageDfs)).outputs.resourceId.value)))))), createObject('value', createArray()))]", "blobServices": { "value": { @@ -41911,10 +39943,10 @@ } }, "dependsOn": [ - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageFile)]", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageQueue)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageBlob)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageDfs)]", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageQueue)]", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageFile)]", "userAssignedIdentity", "virtualNetwork" ] @@ -55687,10 +53719,6 @@ "AGENT_NAME_TITLE": "", "API_APP_NAME": "[format('api-{0}', variables('solutionSuffix'))]", "AI_FOUNDRY_RESOURCE_ID": "[reference('aiFoundryAiServices').outputs.resourceId.value]", - "AZURE_OPENAI_DEPLOYMENT_MODEL": "[parameters('gptModelName')]", - "AZURE_OPENAI_ENDPOINT": "[if(not(empty(variables('existingOpenAIEndpoint'))), variables('existingOpenAIEndpoint'), format('https://{0}.openai.azure.com/', reference('aiFoundryAiServices').outputs.name.value))]", - "AZURE_OPENAI_API_VERSION": "[parameters('azureOpenAIApiVersion')]", - "AZURE_OPENAI_RESOURCE": "[reference('aiFoundryAiServices').outputs.name.value]", "AZURE_AI_AGENT_ENDPOINT": "[if(not(empty(variables('existingProjEndpoint'))), variables('existingProjEndpoint'), reference('aiFoundryAiServices').outputs.aiProjectInfo.value.apiEndpoint)]", "AZURE_AI_AGENT_API_VERSION": "[parameters('azureAiAgentApiVersion')]", "AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME": "[parameters('gptModelName')]", @@ -55720,13 +53748,15 @@ } ] }, + "e2eEncryptionEnabled": { + "value": true + }, "diagnosticSettings": "[if(parameters('enableMonitoring'), createObject('value', createArray(createObject('workspaceResourceId', if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), reference('logAnalyticsWorkspace').outputs.resourceId.value)))), createObject('value', null()))]", "vnetRouteAllEnabled": "[if(parameters('enablePrivateNetworking'), createObject('value', true()), createObject('value', false()))]", "vnetImagePullEnabled": "[if(parameters('enablePrivateNetworking'), createObject('value', true()), createObject('value', false()))]", "virtualNetworkSubnetId": "[if(parameters('enablePrivateNetworking'), createObject('value', reference('virtualNetwork').outputs.webSubnetResourceId.value), createObject('value', null()))]", - "publicNetworkAccess": { - "value": "Enabled" - } + "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), createObject('value', 'Disabled'), createObject('value', 'Enabled'))]", + "privateEndpoints": "[if(parameters('enablePrivateNetworking'), createObject('value', createArray(createObject('name', format('pep-{0}', variables('backendWebSiteResourceName')), 'customNetworkInterfaceName', format('nic-{0}', variables('backendWebSiteResourceName')), 'privateDnsZoneGroup', createObject('privateDnsZoneGroupConfigs', createArray(createObject('privateDnsZoneResourceId', reference(format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').webApp)).outputs.resourceId.value))), 'service', 'sites', 'subnetResourceId', reference('virtualNetwork').outputs.pepsSubnetResourceId.value))), createObject('value', createArray()))]" }, "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", @@ -55735,8 +53765,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "15946348041145518691" + "version": "0.43.8.12551", + "templateHash": "6926177947269338708" } }, "definitions": { @@ -56748,8 +54778,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "1185169597469996118" + "version": "0.43.8.12551", + "templateHash": "1585721918321980475" }, "name": "Site App Settings", "description": "This module deploys a Site App Setting." @@ -57613,6 +55643,7 @@ "dependsOn": [ "aiFoundryAiServices", "applicationInsights", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').webApp)]", "backendUserAssignedIdentity", "cosmosDb", "logAnalyticsWorkspace", @@ -57646,6 +55677,11 @@ "serverFarmResourceId": { "value": "[reference('webServerFarm').outputs.resourceId.value]" }, + "managedIdentities": { + "value": { + "systemAssigned": true + } + }, "siteConfig": { "value": { "linuxFxVersion": "[format('DOCKER|{0}/{1}:{2}', parameters('frontendContainerRegistryHostname'), parameters('frontendContainerImageName'), parameters('frontendContainerImageTag'))]", @@ -57657,12 +55693,16 @@ { "name": "appsettings", "properties": { - "APP_API_BASE_URL": "[format('https://api-{0}.azurewebsites.net', variables('solutionSuffix'))]" + "APP_API_BASE_URL": "[if(parameters('enablePrivateNetworking'), '', format('https://api-{0}.azurewebsites.net', variables('solutionSuffix')))]", + "BACKEND_API_HOST": "[if(parameters('enablePrivateNetworking'), format('api-{0}.azurewebsites.net', variables('solutionSuffix')), '')]" }, "applicationInsightResourceId": "[if(parameters('enableMonitoring'), reference('applicationInsights').outputs.resourceId.value, null())]" } ] }, + "e2eEncryptionEnabled": { + "value": true + }, "vnetRouteAllEnabled": "[if(parameters('enablePrivateNetworking'), createObject('value', true()), createObject('value', false()))]", "vnetImagePullEnabled": "[if(parameters('enablePrivateNetworking'), createObject('value', true()), createObject('value', false()))]", "virtualNetworkSubnetId": "[if(parameters('enablePrivateNetworking'), createObject('value', reference('virtualNetwork').outputs.webSubnetResourceId.value), createObject('value', null()))]", @@ -57678,8 +55718,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "15946348041145518691" + "version": "0.43.8.12551", + "templateHash": "6926177947269338708" } }, "definitions": { @@ -58691,8 +56731,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "1185169597469996118" + "version": "0.43.8.12551", + "templateHash": "1585721918321980475" }, "name": "Site App Settings", "description": "This module deploys a Site App Setting." @@ -59583,13 +57623,6 @@ }, "value": "[parameters('location')]" }, - "AZURE_CONTENT_UNDERSTANDING_LOCATION": { - "type": "string", - "metadata": { - "description": "Contains Azure Content Understanding Location." - }, - "value": "[parameters('contentUnderstandingLocation')]" - }, "APPINSIGHTS_INSTRUMENTATIONKEY": { "type": "string", "metadata": { @@ -59681,14 +57714,14 @@ }, "value": "True" }, - "AZURE_OPENAI_DEPLOYMENT_MODEL": { + "AZURE_ENV_GPT_MODEL_NAME": { "type": "string", "metadata": { "description": "Contains Azure OpenAI deployment model name." }, "value": "[parameters('gptModelName')]" }, - "AZURE_OPENAI_DEPLOYMENT_MODEL_CAPACITY": { + "AZURE_ENV_GPT_MODEL_CAPACITY": { "type": "int", "metadata": { "description": "Contains Azure OpenAI deployment model capacity." @@ -59702,34 +57735,27 @@ }, "value": "[format('https://{0}.openai.azure.com/', reference('aiFoundryAiServices').outputs.name.value)]" }, - "AZURE_OPENAI_MODEL_DEPLOYMENT_TYPE": { + "AZURE_ENV_MODEL_DEPLOYMENT_TYPE": { "type": "string", "metadata": { "description": "Contains Azure OpenAI model deployment type." }, "value": "[parameters('deploymentType')]" }, - "AZURE_OPENAI_EMBEDDING_MODEL": { + "AZURE_ENV_EMBEDDING_MODEL_NAME": { "type": "string", "metadata": { "description": "Contains Azure OpenAI embedding model name." }, "value": "[parameters('embeddingModel')]" }, - "AZURE_OPENAI_EMBEDDING_MODEL_CAPACITY": { + "AZURE_ENV_EMBEDDING_DEPLOYMENT_CAPACITY": { "type": "int", "metadata": { "description": "Contains Azure OpenAI embedding model capacity." }, "value": "[parameters('embeddingDeploymentCapacity')]" }, - "AZURE_OPENAI_API_VERSION": { - "type": "string", - "metadata": { - "description": "Contains Azure OpenAI API version." - }, - "value": "[parameters('azureOpenAIApiVersion')]" - }, "AZURE_CONTENT_UNDERSTANDING_API_VERSION": { "type": "string", "metadata": { @@ -59821,20 +57847,13 @@ }, "value": "[variables('acrName')]" }, - "AZURE_ENV_IMAGETAG": { + "AZURE_ENV_IMAGE_TAG": { "type": "string", "metadata": { "description": "Contains Azure environment image tag." }, "value": "[parameters('backendContainerImageTag')]" }, - "AZURE_EXISTING_AI_PROJECT_RESOURCE_ID": { - "type": "string", - "metadata": { - "description": "Contains existing AI project resource ID." - }, - "value": "[parameters('existingAiFoundryAiProjectResourceId')]" - }, "APPLICATIONINSIGHTS_CONNECTION_STRING": { "type": "string", "metadata": { @@ -59877,19 +57896,12 @@ }, "value": "[reference('aiFoundryAiServices').outputs.resourceId.value]" }, - "CU_FOUNDRY_RESOURCE_ID": { - "type": "string", - "metadata": { - "description": "Resource ID of the Content Understanding AI Foundry." - }, - "value": "[reference('cognitiveServicesCu').outputs.resourceId.value]" - }, "AZURE_OPENAI_CU_ENDPOINT": { "type": "string", "metadata": { "description": "Azure OpenAI Content Understanding endpoint URL." }, - "value": "[reference('cognitiveServicesCu').outputs.endpoint.value]" + "value": "[reference('aiFoundryAiServices').outputs.endpoints.value['Content Understanding']]" }, "API_APP_NAME": { "type": "string", diff --git a/infra/main.parameters.json b/infra/main.parameters.json index 2923b0cd6..3ed53c369 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -8,50 +8,44 @@ "location": { "value": "${AZURE_LOCATION}" }, - "contentUnderstandingLocation" :{ - "value": "${AZURE_CONTENT_UNDERSTANDING_LOCATION}" - }, "secondaryLocation": { - "value": "${AZURE_SECONDARY_LOCATION}" + "value": "${AZURE_ENV_SECONDARY_LOCATION}" }, "aiServiceLocation": { - "value": "${AZURE_ENV_OPENAI_LOCATION}" + "value": "${AZURE_ENV_AI_SERVICE_LOCATION}" }, "deploymentType": { - "value": "${AZURE_OPENAI_MODEL_DEPLOYMENT_TYPE}" + "value": "${AZURE_ENV_MODEL_DEPLOYMENT_TYPE}" }, "gptModelName": { - "value": "${AZURE_OPENAI_DEPLOYMENT_MODEL}" + "value": "${AZURE_ENV_GPT_MODEL_NAME}" }, "gptModelVersion": { - "value": "${AZURE_ENV_MODEL_VERSION}" - }, - "azureOpenAIApiVersion":{ - "value": "${AZURE_OPENAI_API_VERSION}" + "value": "${AZURE_ENV_GPT_MODEL_VERSION}" }, "gptDeploymentCapacity": { - "value": "${AZURE_OPENAI_DEPLOYMENT_MODEL_CAPACITY}" + "value": "${AZURE_ENV_GPT_MODEL_CAPACITY}" }, "embeddingModel": { - "value": "${AZURE_OPENAI_EMBEDDING_MODEL}" + "value": "${AZURE_ENV_EMBEDDING_MODEL_NAME}" }, "embeddingDeploymentCapacity": { - "value": "${AZURE_OPENAI_EMBEDDING_MODEL_CAPACITY}" + "value": "${AZURE_ENV_EMBEDDING_DEPLOYMENT_CAPACITY}" }, "backendContainerImageTag": { - "value": "${AZURE_ENV_IMAGETAG=latest_afv2}" + "value": "${AZURE_ENV_IMAGE_TAG=latest_afv2}" }, "frontendContainerImageTag": { - "value": "${AZURE_ENV_IMAGETAG=latest_afv2}" + "value": "${AZURE_ENV_IMAGE_TAG=latest_afv2}" }, "enableTelemetry": { "value": "${AZURE_ENV_ENABLE_TELEMETRY}" }, "existingLogAnalyticsWorkspaceId": { - "value": "${AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID}" + "value": "${AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID}" }, "existingAiFoundryAiProjectResourceId": { - "value": "${AZURE_EXISTING_AI_PROJECT_RESOURCE_ID}" + "value": "${AZURE_EXISTING_AIPROJECT_RESOURCE_ID}" }, "backendContainerRegistryHostname": { "value": "${AZURE_ENV_CONTAINER_REGISTRY_ENDPOINT}" @@ -60,7 +54,7 @@ "value": "${AZURE_ENV_CONTAINER_REGISTRY_ENDPOINT}" }, "usecase":{ - "value": "${AZURE_ENV_USE_CASE}" + "value": "${USE_CASE}" } } } diff --git a/infra/main.waf.parameters.json b/infra/main.waf.parameters.json index 40bbcb1c6..574122e12 100644 --- a/infra/main.waf.parameters.json +++ b/infra/main.waf.parameters.json @@ -8,50 +8,44 @@ "location": { "value": "${AZURE_LOCATION}" }, - "contentUnderstandingLocation" :{ - "value": "${AZURE_CONTENT_UNDERSTANDING_LOCATION}" - }, "secondaryLocation": { - "value": "${AZURE_SECONDARY_LOCATION}" + "value": "${AZURE_ENV_SECONDARY_LOCATION}" }, "aiServiceLocation": { - "value": "${AZURE_ENV_OPENAI_LOCATION}" + "value": "${AZURE_ENV_AI_SERVICE_LOCATION}" }, "deploymentType": { - "value": "${AZURE_OPENAI_MODEL_DEPLOYMENT_TYPE}" + "value": "${AZURE_ENV_MODEL_DEPLOYMENT_TYPE}" }, "gptModelName": { - "value": "${AZURE_OPENAI_DEPLOYMENT_MODEL}" + "value": "${AZURE_ENV_GPT_MODEL_NAME}" }, "gptModelVersion": { - "value": "${AZURE_ENV_MODEL_VERSION}" - }, - "azureOpenAIApiVersion":{ - "value": "${AZURE_OPENAI_API_VERSION}" + "value": "${AZURE_ENV_GPT_MODEL_VERSION}" }, "gptDeploymentCapacity": { - "value": "${AZURE_OPENAI_DEPLOYMENT_MODEL_CAPACITY}" + "value": "${AZURE_ENV_GPT_MODEL_CAPACITY}" }, "embeddingModel": { - "value": "${AZURE_OPENAI_EMBEDDING_MODEL}" + "value": "${AZURE_ENV_EMBEDDING_MODEL_NAME}" }, "embeddingDeploymentCapacity": { - "value": "${AZURE_OPENAI_EMBEDDING_MODEL_CAPACITY}" + "value": "${AZURE_ENV_EMBEDDING_DEPLOYMENT_CAPACITY}" }, "backendContainerImageTag": { - "value": "${AZURE_ENV_IMAGETAG=latest_afv2}" + "value": "${AZURE_ENV_IMAGE_TAG=latest_afv2}" }, "frontendContainerImageTag": { - "value": "${AZURE_ENV_IMAGETAG=latest_afv2}" + "value": "${AZURE_ENV_IMAGE_TAG=latest_afv2}" }, "enableTelemetry": { "value": "${AZURE_ENV_ENABLE_TELEMETRY}" }, "existingLogAnalyticsWorkspaceId": { - "value": "${AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID}" + "value": "${AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID}" }, "existingAiFoundryAiProjectResourceId": { - "value": "${AZURE_EXISTING_AI_PROJECT_RESOURCE_ID}" + "value": "${AZURE_EXISTING_AIPROJECT_RESOURCE_ID}" }, "backendContainerRegistryHostname": { "value": "${AZURE_ENV_CONTAINER_REGISTRY_ENDPOINT}" @@ -78,7 +72,7 @@ "value": "${AZURE_ENV_VM_SIZE}" }, "usecase":{ - "value": "${AZURE_ENV_USE_CASE}" + "value": "${USE_CASE}" } } } diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index c00cde641..59a957f07 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -24,10 +24,11 @@ param location string 'australiaeast' 'eastus' 'eastus2' - 'francecentral' 'japaneast' + 'southcentralus' 'swedencentral' 'uksouth' + 'westeurope' 'westus' 'westus3' ]) @@ -36,7 +37,7 @@ param location string type: 'location' usageName: [ 'OpenAI.GlobalStandard.gpt-4o-mini,150' - 'OpenAI.GlobalStandard.text-embedding-ada-002,80' + 'OpenAI.GlobalStandard.text-embedding-3-small,80' ] } }) @@ -51,16 +52,6 @@ param aiServiceLocation string ]) param usecase string -@minLength(1) -@description('Optional. Location for the Content Understanding service deployment.') -@allowed(['swedencentral', 'australiaeast']) -@metadata({ - azd: { - type: 'location' - } -}) -param contentUnderstandingLocation string = 'swedencentral' - @minLength(1) @description('Optional. Secondary location for databases creation (example: eastus2).') param secondaryLocation string = 'eastus2' @@ -79,14 +70,11 @@ param gptModelName string = 'gpt-4o-mini' @description('Optional. Version of the GPT model to deploy.') param gptModelVersion string = '2024-07-18' -@description('Optional. Version of the Azure OpenAI API.') -param azureOpenAIApiVersion string = '2025-01-01-preview' - @description('Optional. Version of AI Agent API.') param azureAiAgentApiVersion string = '2025-05-01' @description('Optional. Version of Content Understanding API.') -param azureContentUnderstandingApiVersion string = '2024-12-01-preview' +param azureContentUnderstandingApiVersion string = '2025-11-01' // You can increase this, but capacity is limited per model/region, so you will get errors if you go over // https://learn.microsoft.com/en-us/azure/ai-services/openai/quotas-limits @@ -97,32 +85,14 @@ param gptDeploymentCapacity int = 150 @minLength(1) @description('Optional. Name of the Text Embedding model to deploy.') @allowed([ - 'text-embedding-ada-002' + 'text-embedding-3-small' ]) -param embeddingModel string = 'text-embedding-ada-002' +param embeddingModel string = 'text-embedding-3-small' @minValue(10) @description('Optional. Capacity of the Embedding Model deployment.') param embeddingDeploymentCapacity int = 80 -@description('Optional. The Container Registry hostname where the docker images for the backend are located.') -param backendContainerRegistryHostname string = 'kmcontainerreg.azurecr.io' - -@description('Optional. The Container Image Name to deploy on the backend.') -param backendContainerImageName string = 'km-api' - -@description('Optional. The Container Image Tag to deploy on the backend.') -param backendContainerImageTag string = 'latest_waf_2025-12-02_1084' - -@description('Optional. The Container Registry hostname where the docker images for the frontend are located.') -param frontendContainerRegistryHostname string = 'kmcontainerreg.azurecr.io' - -@description('Optional. The Container Image Name to deploy on the frontend.') -param frontendContainerImageName string = 'km-app' - -@description('Optional. The Container Image Tag to deploy on the frontend.') -param frontendContainerImageTag string = 'latest_waf_2025-12-02_1084' - @description('Optional. The tags to apply to all deployed Azure resources.') param tags resourceInput<'Microsoft.Resources/resourceGroups@2025-04-01'>.tags = {} @@ -212,6 +182,16 @@ var useExistingLogAnalytics = !empty(existingLogAnalyticsWorkspaceId) var logAnalyticsWorkspaceResourceId = useExistingLogAnalytics ? existingLogAnalyticsWorkspaceId : logAnalyticsWorkspace!.outputs.resourceId + +var existingLawSubscription = useExistingLogAnalytics ? split(existingLogAnalyticsWorkspaceId, '/')[2] : '' +var existingLawResourceGroup = useExistingLogAnalytics ? split(existingLogAnalyticsWorkspaceId, '/')[4] : '' +var existingLawName = useExistingLogAnalytics ? split(existingLogAnalyticsWorkspaceId, '/')[8] : '' + +resource existingLogAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2025-07-01' existing = if (useExistingLogAnalytics) { + name: existingLawName + scope: resourceGroup(existingLawSubscription, existingLawResourceGroup) +} + var existingTags = resourceGroup().tags ?? {} // ========== Resource Group Tag ========== // @@ -378,6 +358,125 @@ module bastionHost 'br/public:avm/res/network/bastion-host:0.8.2' = if (enablePr } } + +var dataCollectionRulesResourceName = 'dcr-${solutionSuffix}' +var dataCollectionRulesLocation = useExistingLogAnalytics + ? existingLogAnalyticsWorkspace!.location + : logAnalyticsWorkspace!.outputs.location +module windowsVmDataCollectionRules 'br/public:avm/res/insights/data-collection-rule:0.11.0' = if (enablePrivateNetworking && enableMonitoring) { + name: take('avm.res.insights.data-collection-rule.${dataCollectionRulesResourceName}', 64) + params: { + name: dataCollectionRulesResourceName + tags: tags + enableTelemetry: enableTelemetry + location: dataCollectionRulesLocation + dataCollectionRuleProperties: { + kind: 'Windows' + dataSources: { + performanceCounters: [ + { + streams: [ + 'Microsoft-Perf' + ] + samplingFrequencyInSeconds: 60 + counterSpecifiers: [ + '\\Processor Information(_Total)\\% Processor Time' + '\\Processor Information(_Total)\\% Privileged Time' + '\\Processor Information(_Total)\\% User Time' + '\\Processor Information(_Total)\\Processor Frequency' + '\\System\\Processes' + '\\Process(_Total)\\Thread Count' + '\\Process(_Total)\\Handle Count' + '\\System\\System Up Time' + '\\System\\Context Switches/sec' + '\\System\\Processor Queue Length' + '\\Memory\\% Committed Bytes In Use' + '\\Memory\\Available Bytes' + '\\Memory\\Committed Bytes' + '\\Memory\\Cache Bytes' + '\\Memory\\Pool Paged Bytes' + '\\Memory\\Pool Nonpaged Bytes' + '\\Memory\\Pages/sec' + '\\Memory\\Page Faults/sec' + '\\Process(_Total)\\Working Set' + '\\Process(_Total)\\Working Set - Private' + '\\LogicalDisk(_Total)\\% Disk Time' + '\\LogicalDisk(_Total)\\% Disk Read Time' + '\\LogicalDisk(_Total)\\% Disk Write Time' + '\\LogicalDisk(_Total)\\% Idle Time' + '\\LogicalDisk(_Total)\\Disk Bytes/sec' + '\\LogicalDisk(_Total)\\Disk Read Bytes/sec' + '\\LogicalDisk(_Total)\\Disk Write Bytes/sec' + '\\LogicalDisk(_Total)\\Disk Transfers/sec' + '\\LogicalDisk(_Total)\\Disk Reads/sec' + '\\LogicalDisk(_Total)\\Disk Writes/sec' + '\\LogicalDisk(_Total)\\Avg. Disk sec/Transfer' + '\\LogicalDisk(_Total)\\Avg. Disk sec/Read' + '\\LogicalDisk(_Total)\\Avg. Disk sec/Write' + '\\LogicalDisk(_Total)\\Avg. Disk Queue Length' + '\\LogicalDisk(_Total)\\Avg. Disk Read Queue Length' + '\\LogicalDisk(_Total)\\Avg. Disk Write Queue Length' + '\\LogicalDisk(_Total)\\% Free Space' + '\\LogicalDisk(_Total)\\Free Megabytes' + '\\Network Interface(*)\\Bytes Total/sec' + '\\Network Interface(*)\\Bytes Sent/sec' + '\\Network Interface(*)\\Bytes Received/sec' + '\\Network Interface(*)\\Packets/sec' + '\\Network Interface(*)\\Packets Sent/sec' + '\\Network Interface(*)\\Packets Received/sec' + '\\Network Interface(*)\\Packets Outbound Errors' + '\\Network Interface(*)\\Packets Received Errors' + ] + name: 'perfCounterDataSource60' + } + ] + windowsEventLogs: [ + { + name: 'SecurityAuditEvents' + streams: [ + 'Microsoft-Event' + ] + xPathQueries: [ + 'Security!*[System[(band(Keywords,13510798882111488)) and (EventID != 4624)]]' + ] + } + ] + } + destinations: { + logAnalytics: [ + { + workspaceResourceId: logAnalyticsWorkspaceResourceId + name: 'la--1264800308' + } + ] + } + dataFlows: [ + { + streams: [ + 'Microsoft-Perf' + ] + destinations: [ + 'la--1264800308' + ] + transformKql: 'source' + outputStream: 'Microsoft-Perf' + } + { + streams: [ + 'Microsoft-Event' + ] + destinations: [ + 'la--1264800308' + ] + transformKql: 'source' + outputStream: 'Microsoft-Event' + } + ] + } + } +} + + // Jumpbox Virtual Machine var jumpboxVmName = take('vm-jumpbox-${solutionSuffix}', 15) module jumpboxVM 'br/public:avm/res/compute/virtual-machine:0.21.0' = if (enablePrivateNetworking) { @@ -434,6 +533,18 @@ module jumpboxVM 'br/public:avm/res/compute/virtual-machine:0.21.0' = if (enable } ] enableTelemetry: enableTelemetry + extensionMonitoringAgentConfig: enableMonitoring + ? { + dataCollectionRuleAssociations: [ + { + dataCollectionRuleResourceId: windowsVmDataCollectionRules!.outputs.resourceId + name: 'send-${logAnalyticsWorkspaceResourceName}' + } + ] + enabled: true + tags: tags + } + : null } } @@ -449,6 +560,7 @@ var privateDnsZones = [ 'privatelink.documents.azure.com' 'privatelink${environment().suffixes.sqlServerHostname}' 'privatelink.search.windows.net' + 'privatelink.azurewebsites.net' ] // DNS Zone Index Constants @@ -463,6 +575,7 @@ var dnsZoneIndex = { cosmosDB: 7 sqlServer: 8 search: 9 + webApp: 10 } // =================================================== @@ -520,7 +633,6 @@ module backendUserAssignedIdentity 'br/public:avm/res/managed-identity/user-assi // ========== AI Foundry: AI Services ========== // // WAF best practices for Open AI: https://learn.microsoft.com/en-us/azure/well-architected/service-guides/azure-openai -var existingOpenAIEndpoint = !empty(existingAiFoundryAiProjectResourceId) ? format('https://{0}.openai.azure.com/', split(existingAiFoundryAiProjectResourceId, '/')[8]) : '' var existingProjEndpoint = !empty(existingAiFoundryAiProjectResourceId) ? format('https://{0}.services.ai.azure.com/api/projects/{1}', split(existingAiFoundryAiProjectResourceId, '/')[8], split(existingAiFoundryAiProjectResourceId, '/')[10]) : '' var existingAIServicesName = !empty(existingAiFoundryAiProjectResourceId) ? split(existingAiFoundryAiProjectResourceId, '/')[8] : '' var existingAIProjectName = !empty(existingAiFoundryAiProjectResourceId) ? split(existingAiFoundryAiProjectResourceId, '/')[10] : '' @@ -540,9 +652,7 @@ var aiFoundryAiProjectResourceName = useExistingAiFoundryAiProject : 'proj-${solutionSuffix}' // NOTE: Required version 'Microsoft.CognitiveServices/accounts@2024-04-01-preview' not available in AVM -// var aiFoundryAiServicesResourceName = 'aif-${solutionSuffix}' var aiFoundryAiServicesAiProjectResourceName = 'proj-${solutionSuffix}' -var aiFoundryAIservicesEnabled = true var aiModelDeployments = [ { name: gptModelName @@ -563,7 +673,7 @@ var aiModelDeployments = [ name: 'GlobalStandard' capacity: embeddingDeploymentCapacity } - version: '2' + version: '1' raiPolicyName: 'Microsoft.Default' } ] @@ -578,7 +688,7 @@ resource existingAiFoundryAiServicesProject 'Microsoft.CognitiveServices/account parent: existingAiFoundryAiServices } -module aiFoundryAiServices 'modules/ai-services.bicep' = if (aiFoundryAIservicesEnabled) { +module aiFoundryAiServices 'modules/ai-services.bicep' = { name: take('avm.res.cognitive-services.account.${aiFoundryAiServicesResourceName}', 64) params: { name: aiFoundryAiServicesResourceName @@ -692,79 +802,6 @@ module aiFoundryPrivateEndpoint 'br/public:avm/res/network/private-endpoint:0.8. } } -// AI Foundry: AI Services Content Understanding -var aiFoundryAiServicesCUResourceName = 'aif-${solutionSuffix}-cu' -var aiServicesNameCu = 'aisa-${solutionSuffix}-cu' -module cognitiveServicesCu 'br/public:avm/res/cognitive-services/account:0.14.1' = { - name: take('avm.res.cognitive-services.account.${aiFoundryAiServicesCUResourceName}', 64) - params: { - name: aiServicesNameCu - location: contentUnderstandingLocation - tags: tags - enableTelemetry: enableTelemetry - diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null - sku: 'S0' - kind: 'AIServices' - networkAcls: { - defaultAction: 'Allow' - virtualNetworkRules: [] - ipRules: [] - } - managedIdentities: { userAssignedResourceIds: [userAssignedIdentity!.outputs.resourceId] } //To create accounts or projects, you must enable a managed identity on your resource - disableLocalAuth: true - customSubDomainName: aiServicesNameCu - apiProperties: { - // staticsEnabled: false - } - publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' - privateEndpoints: [] - roleAssignments: [ - { - roleDefinitionIdOrName: '53ca6127-db72-4b80-b1b0-d745d6d5456d' // Azure AI User - principalId: userAssignedIdentity.outputs.principalId - principalType: 'ServicePrincipal' - } - ] - } -} - -// ========== AI Services CU: Separate Private Endpoint ========== // -module cognitiveServicesCuPrivateEndpoint 'br/public:avm/res/network/private-endpoint:0.8.1' = if (enablePrivateNetworking) { - name: take('pep-${aiFoundryAiServicesCUResourceName}-deployment', 64) - params: { - name: 'pep-${aiFoundryAiServicesCUResourceName}' - customNetworkInterfaceName: 'nic-${aiFoundryAiServicesCUResourceName}' - location: location - tags: tags - privateLinkServiceConnections: [ - { - name: 'pep-${aiFoundryAiServicesCUResourceName}-connection' - properties: { - privateLinkServiceId: cognitiveServicesCu.outputs.resourceId - groupIds: ['account'] - } - } - ] - privateDnsZoneGroup: { - privateDnsZoneGroupConfigs: [ - { - name: 'ai-services-cu-dns-zone-cognitiveservices' - privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.cognitiveServices]!.outputs.resourceId - } - { - name: 'ai-services-cu-dns-zone-openai' - privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.openAI]!.outputs.resourceId - } - { - name: 'ai-services-cu-dns-zone-aiservices' - privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.aiServices]!.outputs.resourceId - } - ] - } - subnetResourceId: virtualNetwork!.outputs.pepsSubnetResourceId - } -} - // ========== AVM WAF ========== // // ========== AI Foundry: AI Search ========== // var aiSearchName = 'srch-${solutionSuffix}' @@ -784,6 +821,7 @@ module searchServiceUpdate 'br/public:avm/res/search/search-service:0.12.0' = { params: { // Required parameters name: aiSearchName + location: location enableTelemetry: enableTelemetry diagnosticSettings: enableMonitoring ? [ { @@ -859,6 +897,9 @@ module searchServiceUpdate 'br/public:avm/res/search/search-service:0.12.0' = { ] : [] } + dependsOn: [ + searchService + ] } // ========== Search Service to AI Services Role Assignment ========== // @@ -955,6 +996,7 @@ module storageAccount 'br/public:avm/res/storage/storage-account:0.31.0' = { allowSharedKeyAccess: true allowBlobPublicAccess: false publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' + requireInfrastructureEncryption: true privateEndpoints: enablePrivateNetworking ? [ { @@ -1328,10 +1370,6 @@ module webSiteBackend 'modules/web-sites.bicep' = { ENABLE_ORYX_BUILD: 'true' PYTHONUNBUFFERED: '1' REACT_APP_LAYOUT_CONFIG: reactAppLayoutConfig - AZURE_OPENAI_DEPLOYMENT_MODEL: gptModelName - AZURE_OPENAI_ENDPOINT: !empty(existingOpenAIEndpoint) ? existingOpenAIEndpoint : 'https://${aiFoundryAiServices.outputs.name}.openai.azure.com/' - AZURE_OPENAI_API_VERSION: azureOpenAIApiVersion - AZURE_OPENAI_RESOURCE: aiFoundryAiServices.outputs.name AZURE_AI_AGENT_ENDPOINT: !empty(existingProjEndpoint) ? existingProjEndpoint : aiFoundryAiServices.outputs.aiProjectInfo.apiEndpoint AZURE_AI_AGENT_API_VERSION: azureAiAgentApiVersion AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME: gptModelName @@ -1361,12 +1399,28 @@ module webSiteBackend 'modules/web-sites.bicep' = { applicationInsightResourceId: enableMonitoring ? applicationInsights!.outputs.resourceId : null } ] + e2eEncryptionEnabled: true diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null // WAF aligned configuration for Private Networking vnetRouteAllEnabled: enablePrivateNetworking ? true : false vnetImagePullEnabled: enablePrivateNetworking ? true : false virtualNetworkSubnetId: enablePrivateNetworking ? virtualNetwork!.outputs.webSubnetResourceId : null - publicNetworkAccess: 'Enabled' + publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' + privateEndpoints: enablePrivateNetworking + ? [ + { + name: 'pep-${backendWebSiteResourceName}' + customNetworkInterfaceName: 'nic-${backendWebSiteResourceName}' + privateDnsZoneGroup: { + privateDnsZoneGroupConfigs: [ + { privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.webApp]!.outputs.resourceId } + ] + } + service: 'sites' + subnetResourceId: virtualNetwork!.outputs.pepsSubnetResourceId + } + ] + : [] } } @@ -1394,13 +1448,15 @@ module webSiteFrontend 'modules/web-sites.bicep' = { properties: { SCM_DO_BUILD_DURING_DEPLOYMENT: 'true' ENABLE_ORYX_BUILD: 'true' - REACT_APP_API_BASE_URL: 'https://api-${solutionSuffix}.azurewebsites.net' + REACT_APP_API_BASE_URL: enablePrivateNetworking ? '' : 'https://api-${solutionSuffix}.azurewebsites.net' WEBSITE_NODE_DEFAULT_VERSION: '~20' - APP_API_BASE_URL: 'https://api-${solutionSuffix}.azurewebsites.net' + APP_API_BASE_URL: enablePrivateNetworking ? '' : 'https://api-${solutionSuffix}.azurewebsites.net' + BACKEND_API_HOST: enablePrivateNetworking ? 'api-${solutionSuffix}.azurewebsites.net' : '' } applicationInsightResourceId: enableMonitoring ? applicationInsights!.outputs.resourceId : null } ] + e2eEncryptionEnabled: true vnetRouteAllEnabled: enablePrivateNetworking ? true : false vnetImagePullEnabled: enablePrivateNetworking ? true : false virtualNetworkSubnetId: enablePrivateNetworking ? virtualNetwork!.outputs.webSubnetResourceId : null @@ -1419,9 +1475,6 @@ output RESOURCE_GROUP_NAME string = resourceGroup().name @description('Contains Resource Group Location.') output RESOURCE_GROUP_LOCATION string = location -@description('Contains Azure Content Understanding Location.') -output AZURE_CONTENT_UNDERSTANDING_LOCATION string = contentUnderstandingLocation - // @description('Contains Azure Secondary Location.') // output AZURE_SECONDARY_LOCATION string = secondaryLocation @@ -1465,25 +1518,22 @@ output AZURE_COSMOSDB_DATABASE string = 'db_conversation_history' output AZURE_COSMOSDB_ENABLE_FEEDBACK string = 'True' @description('Contains Azure OpenAI deployment model name.') -output AZURE_OPENAI_DEPLOYMENT_MODEL string = gptModelName +output AZURE_ENV_GPT_MODEL_NAME string = gptModelName @description('Contains Azure OpenAI deployment model capacity.') -output AZURE_OPENAI_DEPLOYMENT_MODEL_CAPACITY int = gptDeploymentCapacity +output AZURE_ENV_GPT_MODEL_CAPACITY int = gptDeploymentCapacity @description('Contains Azure OpenAI endpoint URL.') output AZURE_OPENAI_ENDPOINT string = 'https://${aiFoundryAiServices.outputs.name}.openai.azure.com/' @description('Contains Azure OpenAI model deployment type.') -output AZURE_OPENAI_MODEL_DEPLOYMENT_TYPE string = deploymentType +output AZURE_ENV_MODEL_DEPLOYMENT_TYPE string = deploymentType @description('Contains Azure OpenAI embedding model name.') -output AZURE_OPENAI_EMBEDDING_MODEL string = embeddingModel +output AZURE_ENV_EMBEDDING_MODEL_NAME string = embeddingModel @description('Contains Azure OpenAI embedding model capacity.') -output AZURE_OPENAI_EMBEDDING_MODEL_CAPACITY int = embeddingDeploymentCapacity - -@description('Contains Azure OpenAI API version.') -output AZURE_OPENAI_API_VERSION string = azureOpenAIApiVersion +output AZURE_ENV_EMBEDDING_DEPLOYMENT_CAPACITY int = embeddingDeploymentCapacity @description('Contains Content Understanding API version.') output AZURE_CONTENT_UNDERSTANDING_API_VERSION string = azureContentUnderstandingApiVersion @@ -1527,11 +1577,11 @@ output AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME string = gptModelName @description('Contains Azure Container Registry name.') output ACR_NAME string = acrName -@description('Contains Azure environment image tag.') -output AZURE_ENV_IMAGETAG string = backendContainerImageTag +@description('Contains Azure environment image tag. Not used in custom deployment (uses source code, not containers).') +output AZURE_ENV_IMAGE_TAG string = 'not-applicable' @description('Contains existing AI project resource ID.') -output AZURE_EXISTING_AI_PROJECT_RESOURCE_ID string = existingAiFoundryAiProjectResourceId +output AZURE_EXISTING_AIPROJECT_RESOURCE_ID string = existingAiFoundryAiProjectResourceId @description('Contains Application Insights connection string.') output APPLICATIONINSIGHTS_CONNECTION_STRING string = enableMonitoring ? applicationInsights!.outputs.connectionString : '' @@ -1551,11 +1601,8 @@ output STORAGE_CONTAINER_NAME string = 'data' @description('Resource ID of the AI Foundry.') output AI_FOUNDRY_RESOURCE_ID string = aiFoundryAiServices.outputs.resourceId -@description('Resource ID of the Content Understanding AI Foundry.') -output CU_FOUNDRY_RESOURCE_ID string = cognitiveServicesCu.outputs.resourceId - @description('Azure OpenAI Content Understanding endpoint URL.') -output AZURE_OPENAI_CU_ENDPOINT string = cognitiveServicesCu.outputs.endpoint +output AZURE_OPENAI_CU_ENDPOINT string = aiFoundryAiServices.outputs.endpoints['Content Understanding'] @description('Contains API application name.') output API_APP_NAME string = 'api-${solutionSuffix}' diff --git a/infra/modules/fetch-container-image.bicep b/infra/modules/fetch-container-image.bicep deleted file mode 100644 index 78d1e7eeb..000000000 --- a/infra/modules/fetch-container-image.bicep +++ /dev/null @@ -1,8 +0,0 @@ -param exists bool -param name string - -resource existingApp 'Microsoft.App/containerApps@2023-05-02-preview' existing = if (exists) { - name: name -} - -output containers array = exists ? existingApp.properties.template.containers : [] diff --git a/infra/resources.bicep b/infra/resources.bicep deleted file mode 100644 index b327c4f3c..000000000 --- a/infra/resources.bicep +++ /dev/null @@ -1,685 +0,0 @@ -@description('The location used for all deployed resources') -param location string = resourceGroup().location - -@description('Tags that will be applied to all resources') -param tags object = {} - - -param appExists bool -@secure() -param appDefinition object -param kmChartsFunctionExists bool -@secure() -param kmChartsFunctionDefinition object -param kmRagFunctionExists bool -@secure() -param kmRagFunctionDefinition object -param addUserScriptsExists bool -@secure() -param addUserScriptsDefinition object -param fabricScriptsExists bool -@secure() -param fabricScriptsDefinition object -param indexScriptsExists bool -@secure() -param indexScriptsDefinition object - -@description('Id of the user or app to assign application roles') -param principalId string - -var abbrs = loadJsonContent('./abbreviations.json') -var resourceToken = uniqueString(subscription().id, resourceGroup().id, location) - -// Monitor application with Azure Monitor -module monitoring 'br/public:avm/ptn/azd/monitoring:0.1.0' = { - name: 'monitoring' - params: { - logAnalyticsName: '${abbrs.operationalInsightsWorkspaces}${resourceToken}' - applicationInsightsName: '${abbrs.insightsComponents}${resourceToken}' - applicationInsightsDashboardName: '${abbrs.portalDashboards}${resourceToken}' - location: location - tags: tags - } -} - -// Container registry -module containerRegistry 'br/public:avm/res/container-registry/registry:0.1.1' = { - name: 'registry' - params: { - name: '${abbrs.containerRegistryRegistries}${resourceToken}' - location: location - acrAdminUserEnabled: true - tags: tags - publicNetworkAccess: 'Enabled' - roleAssignments:[ - { - principalId: appIdentity.outputs.principalId - principalType: 'ServicePrincipal' - roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') - } - { - principalId: kmChartsFunctionIdentity.outputs.principalId - principalType: 'ServicePrincipal' - roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') - } - { - principalId: kmRagFunctionIdentity.outputs.principalId - principalType: 'ServicePrincipal' - roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') - } - { - principalId: addUserScriptsIdentity.outputs.principalId - principalType: 'ServicePrincipal' - roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') - } - { - principalId: fabricScriptsIdentity.outputs.principalId - principalType: 'ServicePrincipal' - roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') - } - { - principalId: indexScriptsIdentity.outputs.principalId - principalType: 'ServicePrincipal' - roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') - } - ] - } -} - -// Container apps environment -module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.4.5' = { - name: 'container-apps-environment' - params: { - logAnalyticsWorkspaceResourceId: monitoring.outputs.logAnalyticsWorkspaceResourceId - name: '${abbrs.appManagedEnvironments}${resourceToken}' - location: location - zoneRedundant: false - } -} - -module appIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { - name: 'appidentity' - params: { - name: '${abbrs.managedIdentityUserAssignedIdentities}app-${resourceToken}' - location: location - } -} - -module appFetchLatestImage './modules/fetch-container-image.bicep' = { - name: 'app-fetch-image' - params: { - exists: appExists - name: 'app' - } -} - -var appAppSettingsArray = filter(array(appDefinition.settings), i => i.name != '') -var appSecrets = map(filter(appAppSettingsArray, i => i.?secret != null), i => { - name: i.name - value: i.value - secretRef: i.?secretRef ?? take(replace(replace(toLower(i.name), '_', '-'), '.', '-'), 32) -}) -var appEnv = map(filter(appAppSettingsArray, i => i.?secret == null), i => { - name: i.name - value: i.value -}) - -module app 'br/public:avm/res/app/container-app:0.8.0' = { - name: 'app' - params: { - name: 'app' - ingressTargetPort: 80 - scaleMinReplicas: 1 - scaleMaxReplicas: 10 - secrets: { - secureList: union([ - ], - map(appSecrets, secret => { - name: secret.secretRef - value: secret.value - })) - } - containers: [ - { - image: appFetchLatestImage.outputs.?containers[?0].?image ?? 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' - name: 'main' - resources: { - cpu: json('0.5') - memory: '1.0Gi' - } - env: union([ - { - name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' - value: monitoring.outputs.applicationInsightsConnectionString - } - { - name: 'AZURE_CLIENT_ID' - value: appIdentity.outputs.clientId - } - { - name: 'PORT' - value: '80' - } - ], - appEnv, - map(appSecrets, secret => { - name: secret.name - secretRef: secret.secretRef - })) - } - ] - managedIdentities:{ - systemAssigned: false - userAssignedResourceIds: [appIdentity.outputs.resourceId] - } - registries:[ - { - server: containerRegistry.outputs.loginServer - identity: appIdentity.outputs.resourceId - } - ] - environmentResourceId: containerAppsEnvironment.outputs.resourceId - location: location - tags: union(tags, { 'azd-service-name': 'app' }) - } -} - -module kmChartsFunctionIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { - name: 'kmChartsFunctionidentity' - params: { - name: '${abbrs.managedIdentityUserAssignedIdentities}kmChartsFunction-${resourceToken}' - location: location - } -} - -module kmChartsFunctionFetchLatestImage './modules/fetch-container-image.bicep' = { - name: 'kmChartsFunction-fetch-image' - params: { - exists: kmChartsFunctionExists - name: 'km-charts-function' - } -} - -var kmChartsFunctionAppSettingsArray = filter(array(kmChartsFunctionDefinition.settings), i => i.name != '') -var kmChartsFunctionSecrets = map(filter(kmChartsFunctionAppSettingsArray, i => i.?secret != null), i => { - name: i.name - value: i.value - secretRef: i.?secretRef ?? take(replace(replace(toLower(i.name), '_', '-'), '.', '-'), 32) -}) -var kmChartsFunctionEnv = map(filter(kmChartsFunctionAppSettingsArray, i => i.?secret == null), i => { - name: i.name - value: i.value -}) - -module kmChartsFunction 'br/public:avm/res/app/container-app:0.8.0' = { - name: 'kmChartsFunction' - params: { - name: 'km-charts-function' - ingressTargetPort: 5000 - scaleMinReplicas: 1 - scaleMaxReplicas: 10 - secrets: { - secureList: union([ - ], - map(kmChartsFunctionSecrets, secret => { - name: secret.secretRef - value: secret.value - })) - } - containers: [ - { - image: kmChartsFunctionFetchLatestImage.outputs.?containers[?0].?image ?? 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' - name: 'main' - resources: { - cpu: json('0.5') - memory: '1.0Gi' - } - env: union([ - { - name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' - value: monitoring.outputs.applicationInsightsConnectionString - } - { - name: 'AZURE_CLIENT_ID' - value: kmChartsFunctionIdentity.outputs.clientId - } - { - name: 'PORT' - value: '5000' - } - ], - kmChartsFunctionEnv, - map(kmChartsFunctionSecrets, secret => { - name: secret.name - secretRef: secret.secretRef - })) - } - ] - managedIdentities:{ - systemAssigned: false - userAssignedResourceIds: [kmChartsFunctionIdentity.outputs.resourceId] - } - registries:[ - { - server: containerRegistry.outputs.loginServer - identity: kmChartsFunctionIdentity.outputs.resourceId - } - ] - environmentResourceId: containerAppsEnvironment.outputs.resourceId - location: location - tags: union(tags, { 'azd-service-name': 'km-charts-function' }) - } -} - -module kmRagFunctionIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { - name: 'kmRagFunctionidentity' - params: { - name: '${abbrs.managedIdentityUserAssignedIdentities}kmRagFunction-${resourceToken}' - location: location - } -} - -module kmRagFunctionFetchLatestImage './modules/fetch-container-image.bicep' = { - name: 'kmRagFunction-fetch-image' - params: { - exists: kmRagFunctionExists - name: 'km-rag-function' - } -} - -var kmRagFunctionAppSettingsArray = filter(array(kmRagFunctionDefinition.settings), i => i.name != '') -var kmRagFunctionSecrets = map(filter(kmRagFunctionAppSettingsArray, i => i.?secret != null), i => { - name: i.name - value: i.value - secretRef: i.?secretRef ?? take(replace(replace(toLower(i.name), '_', '-'), '.', '-'), 32) -}) -var kmRagFunctionEnv = map(filter(kmRagFunctionAppSettingsArray, i => i.?secret == null), i => { - name: i.name - value: i.value -}) - -module kmRagFunction 'br/public:avm/res/app/container-app:0.8.0' = { - name: 'kmRagFunction' - params: { - name: 'km-rag-function' - ingressTargetPort: 5000 - scaleMinReplicas: 1 - scaleMaxReplicas: 10 - secrets: { - secureList: union([ - ], - map(kmRagFunctionSecrets, secret => { - name: secret.secretRef - value: secret.value - })) - } - containers: [ - { - image: kmRagFunctionFetchLatestImage.outputs.?containers[?0].?image ?? 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' - name: 'main' - resources: { - cpu: json('0.5') - memory: '1.0Gi' - } - env: union([ - { - name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' - value: monitoring.outputs.applicationInsightsConnectionString - } - { - name: 'AZURE_CLIENT_ID' - value: kmRagFunctionIdentity.outputs.clientId - } - { - name: 'PORT' - value: '5000' - } - ], - kmRagFunctionEnv, - map(kmRagFunctionSecrets, secret => { - name: secret.name - secretRef: secret.secretRef - })) - } - ] - managedIdentities:{ - systemAssigned: false - userAssignedResourceIds: [kmRagFunctionIdentity.outputs.resourceId] - } - registries:[ - { - server: containerRegistry.outputs.loginServer - identity: kmRagFunctionIdentity.outputs.resourceId - } - ] - environmentResourceId: containerAppsEnvironment.outputs.resourceId - location: location - tags: union(tags, { 'azd-service-name': 'km-rag-function' }) - } -} - -module addUserScriptsIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { - name: 'addUserScriptsidentity' - params: { - name: '${abbrs.managedIdentityUserAssignedIdentities}addUserScripts-${resourceToken}' - location: location - } -} - -module addUserScriptsFetchLatestImage './modules/fetch-container-image.bicep' = { - name: 'addUserScripts-fetch-image' - params: { - exists: addUserScriptsExists - name: 'add-user-scripts' - } -} - -var addUserScriptsAppSettingsArray = filter(array(addUserScriptsDefinition.settings), i => i.name != '') -var addUserScriptsSecrets = map(filter(addUserScriptsAppSettingsArray, i => i.?secret != null), i => { - name: i.name - value: i.value - secretRef: i.?secretRef ?? take(replace(replace(toLower(i.name), '_', '-'), '.', '-'), 32) -}) -var addUserScriptsEnv = map(filter(addUserScriptsAppSettingsArray, i => i.?secret == null), i => { - name: i.name - value: i.value -}) - -module addUserScripts 'br/public:avm/res/app/container-app:0.8.0' = { - name: 'addUserScripts' - params: { - name: 'add-user-scripts' - ingressTargetPort: 80 - scaleMinReplicas: 1 - scaleMaxReplicas: 10 - secrets: { - secureList: union([ - ], - map(addUserScriptsSecrets, secret => { - name: secret.secretRef - value: secret.value - })) - } - containers: [ - { - image: addUserScriptsFetchLatestImage.outputs.?containers[?0].?image ?? 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' - name: 'main' - resources: { - cpu: json('0.5') - memory: '1.0Gi' - } - env: union([ - { - name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' - value: monitoring.outputs.applicationInsightsConnectionString - } - { - name: 'AZURE_CLIENT_ID' - value: addUserScriptsIdentity.outputs.clientId - } - { - name: 'PORT' - value: '80' - } - ], - addUserScriptsEnv, - map(addUserScriptsSecrets, secret => { - name: secret.name - secretRef: secret.secretRef - })) - } - ] - managedIdentities:{ - systemAssigned: false - userAssignedResourceIds: [addUserScriptsIdentity.outputs.resourceId] - } - registries:[ - { - server: containerRegistry.outputs.loginServer - identity: addUserScriptsIdentity.outputs.resourceId - } - ] - environmentResourceId: containerAppsEnvironment.outputs.resourceId - location: location - tags: union(tags, { 'azd-service-name': 'add-user-scripts' }) - } -} - -module fabricScriptsIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { - name: 'fabricScriptsidentity' - params: { - name: '${abbrs.managedIdentityUserAssignedIdentities}fabricScripts-${resourceToken}' - location: location - } -} - -module fabricScriptsFetchLatestImage './modules/fetch-container-image.bicep' = { - name: 'fabricScripts-fetch-image' - params: { - exists: fabricScriptsExists - name: 'fabric-scripts' - } -} - -var fabricScriptsAppSettingsArray = filter(array(fabricScriptsDefinition.settings), i => i.name != '') -var fabricScriptsSecrets = map(filter(fabricScriptsAppSettingsArray, i => i.?secret != null), i => { - name: i.name - value: i.value - secretRef: i.?secretRef ?? take(replace(replace(toLower(i.name), '_', '-'), '.', '-'), 32) -}) -var fabricScriptsEnv = map(filter(fabricScriptsAppSettingsArray, i => i.?secret == null), i => { - name: i.name - value: i.value -}) - -module fabricScripts 'br/public:avm/res/app/container-app:0.8.0' = { - name: 'fabricScripts' - params: { - name: 'fabric-scripts' - ingressTargetPort: 80 - scaleMinReplicas: 1 - scaleMaxReplicas: 10 - secrets: { - secureList: union([ - ], - map(fabricScriptsSecrets, secret => { - name: secret.secretRef - value: secret.value - })) - } - containers: [ - { - image: fabricScriptsFetchLatestImage.outputs.?containers[?0].?image ?? 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' - name: 'main' - resources: { - cpu: json('0.5') - memory: '1.0Gi' - } - env: union([ - { - name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' - value: monitoring.outputs.applicationInsightsConnectionString - } - { - name: 'AZURE_CLIENT_ID' - value: fabricScriptsIdentity.outputs.clientId - } - { - name: 'PORT' - value: '80' - } - ], - fabricScriptsEnv, - map(fabricScriptsSecrets, secret => { - name: secret.name - secretRef: secret.secretRef - })) - } - ] - managedIdentities:{ - systemAssigned: false - userAssignedResourceIds: [fabricScriptsIdentity.outputs.resourceId] - } - registries:[ - { - server: containerRegistry.outputs.loginServer - identity: fabricScriptsIdentity.outputs.resourceId - } - ] - environmentResourceId: containerAppsEnvironment.outputs.resourceId - location: location - tags: union(tags, { 'azd-service-name': 'fabric-scripts' }) - } -} - -module indexScriptsIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { - name: 'indexScriptsidentity' - params: { - name: '${abbrs.managedIdentityUserAssignedIdentities}indexScripts-${resourceToken}' - location: location - } -} - -module indexScriptsFetchLatestImage './modules/fetch-container-image.bicep' = { - name: 'indexScripts-fetch-image' - params: { - exists: indexScriptsExists - name: 'index-scripts' - } -} - -var indexScriptsAppSettingsArray = filter(array(indexScriptsDefinition.settings), i => i.name != '') -var indexScriptsSecrets = map(filter(indexScriptsAppSettingsArray, i => i.?secret != null), i => { - name: i.name - value: i.value - secretRef: i.?secretRef ?? take(replace(replace(toLower(i.name), '_', '-'), '.', '-'), 32) -}) -var indexScriptsEnv = map(filter(indexScriptsAppSettingsArray, i => i.?secret == null), i => { - name: i.name - value: i.value -}) - -module indexScripts 'br/public:avm/res/app/container-app:0.8.0' = { - name: 'indexScripts' - params: { - name: 'index-scripts' - ingressTargetPort: 80 - scaleMinReplicas: 1 - scaleMaxReplicas: 10 - secrets: { - secureList: union([ - ], - map(indexScriptsSecrets, secret => { - name: secret.secretRef - value: secret.value - })) - } - containers: [ - { - image: indexScriptsFetchLatestImage.outputs.?containers[?0].?image ?? 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' - name: 'main' - resources: { - cpu: json('0.5') - memory: '1.0Gi' - } - env: union([ - { - name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' - value: monitoring.outputs.applicationInsightsConnectionString - } - { - name: 'AZURE_CLIENT_ID' - value: indexScriptsIdentity.outputs.clientId - } - { - name: 'PORT' - value: '80' - } - ], - indexScriptsEnv, - map(indexScriptsSecrets, secret => { - name: secret.name - secretRef: secret.secretRef - })) - } - ] - managedIdentities:{ - systemAssigned: false - userAssignedResourceIds: [indexScriptsIdentity.outputs.resourceId] - } - registries:[ - { - server: containerRegistry.outputs.loginServer - identity: indexScriptsIdentity.outputs.resourceId - } - ] - environmentResourceId: containerAppsEnvironment.outputs.resourceId - location: location - tags: union(tags, { 'azd-service-name': 'index-scripts' }) - } -} -// Create a keyvault to store secrets -module keyVault 'br/public:avm/res/key-vault/vault:0.6.1' = { - name: 'keyvault' - params: { - name: '${abbrs.keyVaultVaults}${resourceToken}' - location: location - tags: tags - enableRbacAuthorization: false - accessPolicies: [ - { - objectId: principalId - permissions: { - secrets: [ 'get', 'list' ] - } - } - { - objectId: appIdentity.outputs.principalId - permissions: { - secrets: [ 'get', 'list' ] - } - } - { - objectId: kmChartsFunctionIdentity.outputs.principalId - permissions: { - secrets: [ 'get', 'list' ] - } - } - { - objectId: kmRagFunctionIdentity.outputs.principalId - permissions: { - secrets: [ 'get', 'list' ] - } - } - { - objectId: addUserScriptsIdentity.outputs.principalId - permissions: { - secrets: [ 'get', 'list' ] - } - } - { - objectId: fabricScriptsIdentity.outputs.principalId - permissions: { - secrets: [ 'get', 'list' ] - } - } - { - objectId: indexScriptsIdentity.outputs.principalId - permissions: { - secrets: [ 'get', 'list' ] - } - } - ] - secrets: [ - ] - } -} -output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer -output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.uri -output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name -output AZURE_RESOURCE_APP_ID string = app.outputs.resourceId -output AZURE_RESOURCE_KM_CHARTS_FUNCTION_ID string = kmChartsFunction.outputs.resourceId -output AZURE_RESOURCE_KM_RAG_FUNCTION_ID string = kmRagFunction.outputs.resourceId -output AZURE_RESOURCE_ADD_USER_SCRIPTS_ID string = addUserScripts.outputs.resourceId -output AZURE_RESOURCE_FABRIC_SCRIPTS_ID string = fabricScripts.outputs.resourceId -output AZURE_RESOURCE_INDEX_SCRIPTS_ID string = indexScripts.outputs.resourceId diff --git a/infra/scripts/agent_scripts/requirements.txt b/infra/scripts/agent_scripts/requirements.txt index 5c3d5e927..b1179006b 100644 --- a/infra/scripts/agent_scripts/requirements.txt +++ b/infra/scripts/agent_scripts/requirements.txt @@ -1,3 +1,3 @@ -aiohttp==3.13.3 +aiohttp==3.13.4 azure-identity==1.25.2 azure-ai-projects==2.0.0b3 diff --git a/infra/scripts/checkquota_km.sh b/infra/scripts/checkquota_km.sh index 3bae689ef..bce3a5d80 100644 --- a/infra/scripts/checkquota_km.sh +++ b/infra/scripts/checkquota_km.sh @@ -30,7 +30,7 @@ echo "✅ Azure subscription set successfully." # Define models and their minimum required capacities declare -A MIN_CAPACITY=( ["OpenAI.GlobalStandard.gpt-4o-mini"]=$GPT_MIN_CAPACITY #km generic - ["OpenAI.GlobalStandard.text-embedding-ada-002"]=$TEXT_EMBEDDING_MIN_CAPACITY #km generic + ["OpenAI.GlobalStandard.text-embedding-3-small"]=$TEXT_EMBEDDING_MIN_CAPACITY #km generic ) VALID_REGION="" diff --git a/infra/scripts/fabric_scripts/create_fabric_items.py b/infra/scripts/fabric_scripts/create_fabric_items.py index e032423cb..bc5f8c0e1 100644 --- a/infra/scripts/fabric_scripts/create_fabric_items.py +++ b/infra/scripts/fabric_scripts/create_fabric_items.py @@ -1,8 +1,6 @@ -from azure.identity import ManagedIdentityCredential import base64 import json import requests -import pandas as pd import os from glob import iglob import zipfile @@ -98,11 +96,11 @@ # upload extracted folder file_names = [f for f in iglob(os.path.join(local_path, "**", "*"), recursive=True) if os.path.isfile(f)] # print('file_names ex', file_names) - for file_name in file_names: - upload_file_name = os.path.basename(file_name) + for extracted_file in file_names: + upload_file_name = os.path.basename(extracted_file) file_client = directory_client.get_file_client("cu_audio_files_all/" + upload_file_name) - # with open(file=os.path.join(extract_dir, file_name), mode="rb") as data: - with open(file=file_name, mode="rb") as data: + # with open(file=os.path.join(extract_dir, extracted_file), mode="rb") as data: + with open(file=extracted_file, mode="rb") as data: # print('data', data) file_client.upload_data(data, overwrite=True) @@ -127,7 +125,7 @@ env_res = requests.get(fabric_env_url, headers=fabric_headers) env_res_id = env_res.json()['value'][0]['id'] # print(env_res.json()) -except: +except Exception: # Environments may not be provisioned yet env_res_id = '' #create notebook items @@ -150,14 +148,14 @@ notebook_json['metadata']['dependencies']['lakehouse']['default_lakehouse'] = lakehouse_res.json()['id'] notebook_json['metadata']['dependencies']['lakehouse']['default_lakehouse_name'] = lakehouse_res.json()['displayName'] notebook_json['metadata']['dependencies']['lakehouse']['default_lakehouse_workspace_id'] = lakehouse_res.json()['workspaceId'] - except: + except Exception: # Lakehouse metadata may not be available pass if env_res_id != '': try: notebook_json['metadata']['dependencies']['environment']['environmentId'] = env_res_id notebook_json['metadata']['dependencies']['environment']['workspaceId'] = lakehouse_res.json()['workspaceId'] - except: + except Exception: # Environment metadata may not be available pass @@ -178,8 +176,7 @@ } } - fabric_response = requests.post(fabric_items_url, headers=fabric_headers, json=notebook_data) - #print(fabric_response.json()) + requests.post(fabric_items_url, headers=fabric_headers, json=notebook_data) time.sleep(120) diff --git a/infra/scripts/fabric_scripts/notebooks/speech_to_text/process_data_stt.ipynb b/infra/scripts/fabric_scripts/notebooks/speech_to_text/process_data_stt.ipynb index 36a224ca5..3e64f8599 100644 --- a/infra/scripts/fabric_scripts/notebooks/speech_to_text/process_data_stt.ipynb +++ b/infra/scripts/fabric_scripts/notebooks/speech_to_text/process_data_stt.ipynb @@ -508,7 +508,7 @@ "\n", "# Function: Get Embeddings \n", "def get_embeddings(text: str,openai_api_base,openai_api_version,openai_api_key):\n", - " model_id = \"text-embedding-ada-002\"\n", + " model_id = \"text-embedding-3-small\"\n", " client = AzureOpenAI(\n", " api_version=openai_api_version,\n", " azure_endpoint=openai_api_base,\n", @@ -930,3 +930,4 @@ "nbformat": 4, "nbformat_minor": 5 } + diff --git a/infra/scripts/index_scripts/02_create_cu_template_audio.py b/infra/scripts/index_scripts/02_create_cu_template_audio.py index 72279c29d..4a3570f26 100644 --- a/infra/scripts/index_scripts/02_create_cu_template_audio.py +++ b/infra/scripts/index_scripts/02_create_cu_template_audio.py @@ -10,14 +10,16 @@ p = argparse.ArgumentParser() p.add_argument("--cu_endpoint", required=True) p.add_argument("--cu_api_version", required=True) +p.add_argument("--deployment_model", required=True) +p.add_argument("--embedding_model", required=True) args = p.parse_args() CU_ENDPOINT = args.cu_endpoint CU_API_VERSION = args.cu_api_version -ANALYZER_ID = "ckm-audio" +ANALYZER_ID = "ckm_analyzer_audio" -ANALYZER_TEMPLATE_FILE = 'infra/data/ckm-analyzer_config_audio.json' +ANALYZER_TEMPLATE_FILE = 'infra/data/ckm_analyzer_config_audio.json' # Add parent directory to path for imports sys.path.append(str(Path.cwd().parent)) @@ -31,14 +33,22 @@ token_provider=token_provider ) +# Set model defaults (mandatory for GA API) +client.set_defaults(args.deployment_model, args.embedding_model) + # Create Analyzer try: analyzer = client.get_analyzer_detail_by_id(ANALYZER_ID) if analyzer is not None: client.delete_analyzer(ANALYZER_ID) -except Exception: +except Exception: # Analyzer may not exist yet, safe to ignore pass -response = client.begin_create_analyzer(ANALYZER_ID, analyzer_template_path=ANALYZER_TEMPLATE_FILE) +response = client.begin_create_analyzer( + ANALYZER_ID, + analyzer_template_path=ANALYZER_TEMPLATE_FILE, + completion_model=args.deployment_model, + embedding_model=args.embedding_model +) result = client.poll_result(response) print(f"✓ Analyzer '{ANALYZER_ID}' created") diff --git a/infra/scripts/index_scripts/02_create_cu_template_text.py b/infra/scripts/index_scripts/02_create_cu_template_text.py index a9080f2ed..a90189c77 100644 --- a/infra/scripts/index_scripts/02_create_cu_template_text.py +++ b/infra/scripts/index_scripts/02_create_cu_template_text.py @@ -8,14 +8,16 @@ p = argparse.ArgumentParser() p.add_argument("--cu_endpoint", required=True) p.add_argument("--cu_api_version", required=True) +p.add_argument("--deployment_model", required=True) +p.add_argument("--embedding_model", required=True) args = p.parse_args() CU_ENDPOINT = args.cu_endpoint CU_API_VERSION = args.cu_api_version -ANALYZER_ID = "ckm-json" +ANALYZER_ID = "ckm_analyzer_json" -ANALYZER_TEMPLATE_FILE = 'infra/data/ckm-analyzer_config_text.json' +ANALYZER_TEMPLATE_FILE = 'infra/data/ckm_analyzer_config_json.json' credential = AzureCliCredential(process_timeout=30) # Initialize Content Understanding Client @@ -26,14 +28,22 @@ token_provider=token_provider ) +# Set model defaults (mandatory for GA API) +client.set_defaults(args.deployment_model, args.embedding_model) + # Create Analyzer try: analyzer = client.get_analyzer_detail_by_id(ANALYZER_ID) if analyzer is not None: client.delete_analyzer(ANALYZER_ID) -except Exception: +except Exception: # Analyzer may not exist yet, safe to ignore pass -response = client.begin_create_analyzer(ANALYZER_ID, analyzer_template_path=ANALYZER_TEMPLATE_FILE) +response = client.begin_create_analyzer( + ANALYZER_ID, + analyzer_template_path=ANALYZER_TEMPLATE_FILE, + completion_model=args.deployment_model, + embedding_model=args.embedding_model +) result = client.poll_result(response) print(f"✓ Analyzer '{ANALYZER_ID}' created") diff --git a/infra/scripts/index_scripts/03_cu_process_data_text.py b/infra/scripts/index_scripts/03_cu_process_data_text.py index 30aaa1970..2ba6cb9b4 100644 --- a/infra/scripts/index_scripts/03_cu_process_data_text.py +++ b/infra/scripts/index_scripts/03_cu_process_data_text.py @@ -20,7 +20,7 @@ import pandas as pd import pyodbc -from azure.ai.inference.aio import EmbeddingsClient +from openai import AsyncOpenAI from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import PromptAgentDefinition from azure.identity.aio import AzureCliCredential as AsyncAzureCliCredential @@ -201,17 +201,20 @@ def generate_sql_insert_script(df, table_name, columns, sql_file_name): api_version=CU_API_VERSION, token_provider=cu_token_provider ) -ANALYZER_ID = "ckm-json" +ANALYZER_ID = "ckm_analyzer_json" -# Azure AI Foundry (Inference) embeddings client (async) -inference_endpoint = f"https://{urlparse(AI_PROJECT_ENDPOINT).netloc}/models" +# Azure OpenAI embeddings endpoint (v1 API - no api-version needed) +embeddings_base_url = f"https://{urlparse(AI_PROJECT_ENDPOINT).netloc}/openai/v1/" +embeddings_token_provider = get_bearer_token_provider( + AzureCliCredential(process_timeout=30), "https://ai.azure.com/.default" +) # Utility functions async def get_embeddings_async(text: str, embeddings_client): - """Get embeddings using async EmbeddingsClient.""" + """Get embeddings using async OpenAI client.""" try: - resp = await embeddings_client.embed(model=EMBEDDING_MODEL, input=[text]) + resp = await embeddings_client.embeddings.create(model=EMBEDDING_MODEL, input=text) return resp.data[0].embedding except Exception as e: print(f"Error getting embeddings: {e}") @@ -324,14 +327,11 @@ async def process_files(): processed_records = [] # Collect all records for batch insert # Create embeddings client for entire processing session - async with ( - AsyncAzureCliCredential(process_timeout=30) as async_cred, - EmbeddingsClient( - endpoint=inference_endpoint, - credential=async_cred, - credential_scopes=["https://ai.azure.com/.default"], - ) as embeddings_client - ): + embeddings_client = AsyncOpenAI( + base_url=embeddings_base_url, + api_key=embeddings_token_provider(), + ) + try: for path in paths: file_client = file_system_client.get_file_client(path.name) data_file = file_client.download_file() @@ -340,10 +340,10 @@ async def process_files(): response = cu_client.begin_analyze(ANALYZER_ID, file_location="", file_data=data) result = cu_client.poll_result(response) file_name = path.name.split('/')[-1].replace("%3A", "_") - if USE_CASE == 'telecom': + if USE_CASE == 'telecom': start_time = file_name.replace(".json", "")[-19:] timestamp_format = "%Y-%m-%d %H_%M_%S" - else: + else: start_time = file_name.replace(".json", "")[-16:] timestamp_format = "%Y-%m-%d%H%M%S" start_timestamp = datetime.strptime(start_time, timestamp_format) @@ -381,19 +381,22 @@ async def process_files(): docs.extend(await prepare_search_doc(content, conversation_id, path.name, embeddings_client)) counter += 1 - except Exception: + except Exception: # Skip files that fail processing pass if docs != [] and counter % 10 == 0: - result = search_client.upload_documents(documents=docs) + search_client.upload_documents(documents=docs) docs = [] if docs: search_client.upload_documents(documents=docs) - # Batch insert all processed records using optimized SQL script - if processed_records: - df_processed = pd.DataFrame(processed_records) - columns = ['ConversationId', 'EndTime', 'StartTime', 'Content', 'summary', 'satisfied', 'sentiment', 'topic', 'key_phrases', 'complaint'] - generate_sql_insert_script(df_processed, 'processed_data', columns, 'processed_data_batch_insert.sql') + # Batch insert all processed records using optimized SQL script + if processed_records: + df_processed = pd.DataFrame(processed_records) + columns = ['ConversationId', 'EndTime', 'StartTime', 'Content', 'summary', 'satisfied', 'sentiment', 'topic', 'key_phrases', 'complaint'] + generate_sql_insert_script(df_processed, 'processed_data', columns, 'processed_data_batch_insert.sql') + finally: + # Close the embeddings client to release the underlying httpx connection pool. + await embeddings_client.close() return conversationIds, counter @@ -533,7 +536,6 @@ async def call_topic_mining_agent(topics_str1): column_names = [i[0] for i in cursor.description] df_topics = pd.DataFrame(rows, columns=column_names) mined_topics_list = df_topics['label'].tolist() - mined_topics = ", ".join(mined_topics_list) print(f"✓ Mined {len(mined_topics_list)} topics") async def call_topic_mapping_agent(agent, input_text, list_of_topics): diff --git a/infra/scripts/index_scripts/04_cu_process_custom_data.py b/infra/scripts/index_scripts/04_cu_process_custom_data.py index f751cf9dd..c7bc81a5e 100644 --- a/infra/scripts/index_scripts/04_cu_process_custom_data.py +++ b/infra/scripts/index_scripts/04_cu_process_custom_data.py @@ -20,7 +20,7 @@ import pandas as pd import pyodbc -from azure.ai.inference.aio import EmbeddingsClient +from openai import AsyncOpenAI from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import PromptAgentDefinition from azure.identity.aio import AzureCliCredential as AsyncAzureCliCredential @@ -86,8 +86,11 @@ TOPIC_MINING_AGENT_NAME = f"KM-TopicMiningAgent-{SOLUTION_NAME}" TOPIC_MAPPING_AGENT_NAME = f"KM-TopicMappingAgent-{SOLUTION_NAME}" -# Azure AI Foundry (Inference) endpoint -inference_endpoint = f"https://{urlparse(AI_PROJECT_ENDPOINT).netloc}/models" +# Azure OpenAI embeddings endpoint (v1 API - no api-version needed) +embeddings_base_url = f"https://{urlparse(AI_PROJECT_ENDPOINT).netloc}/openai/v1/" +embeddings_token_provider = get_bearer_token_provider( + AzureCliCredential(process_timeout=30), "https://ai.azure.com/.default" +) # Azure DataLake setup account_url = f"https://{STORAGE_ACCOUNT_NAME}.dfs.core.windows.net" @@ -190,7 +193,7 @@ def create_search_index(): connection_string = f"DRIVER={driver};SERVER={SQL_SERVER};DATABASE={SQL_DATABASE};" conn = pyodbc.connect(connection_string, attrs_before={SQL_COPT_SS_ACCESS_TOKEN: token_struct}) cursor = conn.cursor() -except: +except Exception: # Fall back to ODBC Driver 17 driver = "{ODBC Driver 17 for SQL Server}" token_bytes = credential.get_token("https://database.windows.net/.default").token.encode("utf-16-LE") token_struct = struct.pack(f"/dev/null; then - echo "⚠ Failed to restore CU Foundry access - please check Azure portal" - fi - fi - # Restore SQL Server public access if [ -n "$original_sql_public_access" ] && [ "$original_sql_public_access" != "Enabled" ]; then @@ -330,14 +284,13 @@ get_values_from_azd_env() { backendUserMidDisplayName=$(azd env get-value BACKEND_USER_MID_NAME 2>&1 | grep -E '^[a-zA-Z0-9._/-]+$') aiSearchName=$(azd env get-value AZURE_AI_SEARCH_NAME 2>&1 | grep -E '^[a-zA-Z0-9._/-]+$') aif_resource_id=$(azd env get-value AI_FOUNDRY_RESOURCE_ID 2>&1 | grep -E '^[a-zA-Z0-9._/-]+$') - cu_foundry_resource_id=$(azd env get-value CU_FOUNDRY_RESOURCE_ID 2>&1 | grep -E '^[a-zA-Z0-9._/-]+$') searchEndpoint=$(azd env get-value AZURE_AI_SEARCH_ENDPOINT 2>&1 | grep -E '^https?://[a-zA-Z0-9._/-]+$') openaiEndpoint=$(azd env get-value AZURE_OPENAI_ENDPOINT 2>&1 | grep -E '^https?://[a-zA-Z0-9._/-]+/?$') - embeddingModel=$(azd env get-value AZURE_OPENAI_EMBEDDING_MODEL 2>&1 | grep -E '^[a-zA-Z0-9._-]+$') + embeddingModel=$(azd env get-value AZURE_ENV_EMBEDDING_MODEL_NAME 2>&1 | grep -E '^[a-zA-Z0-9._-]+$') cuEndpoint=$(azd env get-value AZURE_OPENAI_CU_ENDPOINT 2>&1 | grep -E '^https?://[a-zA-Z0-9._/-]+$') aiAgentEndpoint=$(azd env get-value AZURE_AI_AGENT_ENDPOINT 2>&1 | grep -E '^https?://[a-zA-Z0-9._/:/-]+$') cuApiVersion=$(azd env get-value AZURE_CONTENT_UNDERSTANDING_API_VERSION 2>&1 | grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}(-preview)?$') - deploymentModel=$(azd env get-value AZURE_OPENAI_DEPLOYMENT_MODEL 2>&1 | grep -E '^[a-zA-Z0-9._-]+$') + deploymentModel=$(azd env get-value AZURE_ENV_GPT_MODEL_NAME 2>&1 | grep -E '^[a-zA-Z0-9._-]+$') solutionName=$(azd env get-value SOLUTION_NAME 2>&1 | grep -E '^[a-zA-Z0-9._-]+$') # Strip FQDN suffix from SQL server name if present (Azure CLI needs just the server name) @@ -388,13 +341,12 @@ get_values_from_az_deployment() { aiSearchName=$(extract_value "azureAISearchName" "AZURE_AI_SEARCH_NAME") searchEndpoint=$(extract_value "azureAISearchEndpoint" "AZURE_AI_SEARCH_ENDPOINT") aif_resource_id=$(extract_value "aiFoundryResourceId" "AI_FOUNDRY_RESOURCE_ID") - cu_foundry_resource_id=$(extract_value "cuFoundryResourceId" "CU_FOUNDRY_RESOURCE_ID") openaiEndpoint=$(extract_value "azureOpenAIEndpoint" "AZURE_OPENAI_ENDPOINT") - embeddingModel=$(extract_value "azureOpenAIEmbeddingModel" "AZURE_OPENAI_EMBEDDING_MODEL") + embeddingModel=$(extract_value "azureOpenAIEmbeddingModel" "AZURE_ENV_EMBEDDING_MODEL_NAME") cuEndpoint=$(extract_value "azureOpenAICuEndpoint" "AZURE_OPENAI_CU_ENDPOINT") aiAgentEndpoint=$(extract_value "azureAiAgentEndpoint" "AZURE_AI_AGENT_ENDPOINT") cuApiVersion=$(extract_value "azureContentUnderstandingApiVersion" "AZURE_CONTENT_UNDERSTANDING_API_VERSION") - deploymentModel=$(extract_value "azureOpenAIDeploymentModel" "AZURE_OPENAI_DEPLOYMENT_MODEL") + deploymentModel=$(extract_value "azureOpenAIDeploymentModel" "AZURE_ENV_GPT_MODEL_NAME") solutionName=$(extract_value "solutionName" "SOLUTION_NAME") # Strip FQDN suffix from SQL server name if present (Azure CLI needs just the server name) @@ -410,14 +362,13 @@ get_values_from_az_deployment() { ["backendUserMidDisplayName"]="BACKEND_USER_MID_NAME" ["aiSearchName"]="AZURE_AI_SEARCH_NAME" ["aif_resource_id"]="AI_FOUNDRY_RESOURCE_ID" - ["cu_foundry_resource_id"]="CU_FOUNDRY_RESOURCE_ID" ["searchEndpoint"]="AZURE_AI_SEARCH_ENDPOINT" ["openaiEndpoint"]="AZURE_OPENAI_ENDPOINT" - ["embeddingModel"]="AZURE_OPENAI_EMBEDDING_MODEL" + ["embeddingModel"]="AZURE_ENV_EMBEDDING_MODEL_NAME" ["cuEndpoint"]="AZURE_OPENAI_CU_ENDPOINT" ["aiAgentEndpoint"]="AZURE_AI_AGENT_ENDPOINT" ["cuApiVersion"]="AZURE_CONTENT_UNDERSTANDING_API_VERSION" - ["deploymentModel"]="AZURE_OPENAI_DEPLOYMENT_MODEL" + ["deploymentModel"]="AZURE_ENV_GPT_MODEL_NAME" ["solutionName"]="SOLUTION_NAME" ) @@ -498,7 +449,7 @@ echo "" echo "" # Check if all required parameters are provided -if [ -n "$resourceGroupName" ] && [ -n "$azSubscriptionId" ] && [ -n "$storageAccountName" ] && [ -n "$fileSystem" ] && [ -n "$sqlServerName" ] && [ -n "$SqlDatabaseName" ] && [ -n "$backendUserMidClientId" ] && [ -n "$backendUserMidDisplayName" ] && [ -n "$aiSearchName" ] && [ -n "$searchEndpoint" ] && [ -n "$aif_resource_id" ] && [ -n "$cu_foundry_resource_id" ] && [ -n "$openaiEndpoint" ] && [ -n "$embeddingModel" ] && [ -n "$deploymentModel" ] && [ -n "$cuEndpoint" ] && [ -n "$cuApiVersion" ] && [ -n "$aiAgentEndpoint" ] && [ -n "$solutionName" ]; then +if [ -n "$resourceGroupName" ] && [ -n "$azSubscriptionId" ] && [ -n "$storageAccountName" ] && [ -n "$fileSystem" ] && [ -n "$sqlServerName" ] && [ -n "$SqlDatabaseName" ] && [ -n "$backendUserMidClientId" ] && [ -n "$backendUserMidDisplayName" ] && [ -n "$aiSearchName" ] && [ -n "$searchEndpoint" ] && [ -n "$aif_resource_id" ] && [ -n "$openaiEndpoint" ] && [ -n "$embeddingModel" ] && [ -n "$deploymentModel" ] && [ -n "$cuEndpoint" ] && [ -n "$cuApiVersion" ] && [ -n "$aiAgentEndpoint" ] && [ -n "$solutionName" ]; then # All parameters provided - use them directly echo "All parameters provided via command line." # Strip FQDN suffix from SQL server name if present @@ -550,7 +501,6 @@ echo "Backend User-Assigned Managed Identity Display Name: $backendUserMidDispla echo "Backend User-Assigned Managed Identity Client ID: $backendUserMidClientId" echo "AI Search Service Name: $aiSearchName" echo "AI Foundry Resource ID: $aif_resource_id" -echo "CU Foundry Resource ID: $cu_foundry_resource_id" echo "Search Endpoint: $searchEndpoint" echo "OpenAI Endpoint: $openaiEndpoint" echo "Embedding Model: $embeddingModel" @@ -581,13 +531,13 @@ fi # Create Content Understanding analyzers echo "✓ Creating Content Understanding analyzer templates" -python "${pythonScriptPath}02_create_cu_template_text.py" --cu_endpoint="$cuEndpoint" --cu_api_version="$cuApiVersion" +python "${pythonScriptPath}02_create_cu_template_text.py" --cu_endpoint="$cuEndpoint" --cu_api_version="$cuApiVersion" --deployment_model="$deploymentModel" --embedding_model="$embeddingModel" if [ $? -ne 0 ]; then echo "Error: 02_create_cu_template_text.py failed." exit 1 fi -python "${pythonScriptPath}02_create_cu_template_audio.py" --cu_endpoint="$cuEndpoint" --cu_api_version="$cuApiVersion" +python "${pythonScriptPath}02_create_cu_template_audio.py" --cu_endpoint="$cuEndpoint" --cu_api_version="$cuApiVersion" --deployment_model="$deploymentModel" --embedding_model="$embeddingModel" if [ $? -ne 0 ]; then echo "Error: 02_create_cu_template_audio.py failed." exit 1 diff --git a/infra/scripts/process_sample_data.sh b/infra/scripts/process_sample_data.sh index 92edd4fc0..c7b129d84 100644 --- a/infra/scripts/process_sample_data.sh +++ b/infra/scripts/process_sample_data.sh @@ -24,30 +24,26 @@ searchEndpoint="${10}" # AI Foundry aif_resource_id="${11}" -cu_foundry_resource_id="${12}" # OpenAI -openaiEndpoint="${13}" -embeddingModel="${14}" -deploymentModel="${15}" +openaiEndpoint="${12}" +embeddingModel="${13}" +deploymentModel="${14}" # Content Understanding & AI Agent -cuEndpoint="${16}" -cuApiVersion="${17}" -aiAgentEndpoint="${18}" +cuEndpoint="${15}" +cuApiVersion="${16}" +aiAgentEndpoint="${17}" -usecase="${19}" -solutionName="${20}" +usecase="${18}" +solutionName="${19}" # Global variables to track original network access states original_storage_public_access="" original_storage_default_action="" original_foundry_public_access="" -original_cu_foundry_public_access="" aif_resource_group="" aif_account_resource_id="" -cu_resource_group="" -cu_account_resource_id="" # Add global variable for SQL Server public access original_sql_public_access="" created_sql_allow_all_firewall_rule="false" @@ -132,34 +128,6 @@ enable_public_access() { fi fi - # Enable public access for Content Understanding Foundry - if [ -n "$cu_foundry_resource_id" ] && [ "$cu_foundry_resource_id" != "null" ]; then - cu_account_resource_id="$cu_foundry_resource_id" - cu_resource_name=$(echo "$cu_foundry_resource_id" | sed -n 's|.*/providers/Microsoft.CognitiveServices/accounts/\([^/]*\).*|\1|p') - cu_resource_group=$(echo "$cu_foundry_resource_id" | sed -n 's|.*/resourceGroups/\([^/]*\)/.*|\1|p') - cu_subscription_id=$(echo "$cu_account_resource_id" | sed -n 's|.*/subscriptions/\([^/]*\)/.*|\1|p') - - original_cu_foundry_public_access=$(az cognitiveservices account show \ - --name "$cu_resource_name" \ - --resource-group "$cu_resource_group" \ - --subscription "$cu_subscription_id" \ - --query "properties.publicNetworkAccess" \ - --output tsv) - - if [ -z "$original_cu_foundry_public_access" ] || [ "$original_cu_foundry_public_access" = "null" ]; then - echo "⚠ Could not retrieve CU Foundry network access status" - elif [ "$original_cu_foundry_public_access" != "Enabled" ]; then - echo "✓ Enabling CU Foundry public access" - if ! MSYS_NO_PATHCONV=1 az resource update \ - --ids "$cu_account_resource_id" \ - --api-version 2024-10-01 \ - --set properties.publicNetworkAccess=Enabled properties.apiProperties="{}" \ - --output none; then - echo "⚠ Failed to enable CU Foundry public access" - fi - fi - fi - # Enable public access for SQL Server original_sql_public_access=$(az sql server show \ --name "$sqlServerName" \ @@ -270,20 +238,6 @@ restore_network_access() { fi fi - # Restore CU Foundry access - if [ -n "$original_cu_foundry_public_access" ] && [ "$original_cu_foundry_public_access" != "Enabled" ]; then - echo "✓ Restoring CU Foundry access" - if ! MSYS_NO_PATHCONV=1 az resource update \ - --ids "$cu_account_resource_id" \ - --api-version 2024-10-01 \ - --set properties.publicNetworkAccess="$original_cu_foundry_public_access" \ - --set properties.apiProperties.qnaAzureSearchEndpointKey="" \ - --set properties.networkAcls.bypass="AzureServices" \ - --output none 2>/dev/null; then - echo "⚠ Failed to restore CU Foundry access - please check Azure portal" - fi - fi - # Restore SQL Server public access if [ -n "$original_sql_public_access" ] && [ "$original_sql_public_access" != "Enabled" ]; then @@ -340,14 +294,13 @@ get_values_from_azd_env() { backendUserMidDisplayName=$(azd env get-value BACKEND_USER_MID_NAME 2>&1 | grep -E '^[a-zA-Z0-9._/-]+$') aiSearchName=$(azd env get-value AZURE_AI_SEARCH_NAME 2>&1 | grep -E '^[a-zA-Z0-9._/-]+$') aif_resource_id=$(azd env get-value AI_FOUNDRY_RESOURCE_ID 2>&1 | grep -E '^[a-zA-Z0-9._/-]+$') - cu_foundry_resource_id=$(azd env get-value CU_FOUNDRY_RESOURCE_ID 2>&1 | grep -E '^[a-zA-Z0-9._/-]+$') searchEndpoint=$(azd env get-value AZURE_AI_SEARCH_ENDPOINT 2>&1 | grep -E '^https?://[a-zA-Z0-9._/-]+$') openaiEndpoint=$(azd env get-value AZURE_OPENAI_ENDPOINT 2>&1 | grep -E '^https?://[a-zA-Z0-9._/-]+/?$') - embeddingModel=$(azd env get-value AZURE_OPENAI_EMBEDDING_MODEL 2>&1 | grep -E '^[a-zA-Z0-9._-]+$') + embeddingModel=$(azd env get-value AZURE_ENV_EMBEDDING_MODEL_NAME 2>&1 | grep -E '^[a-zA-Z0-9._-]+$') cuEndpoint=$(azd env get-value AZURE_OPENAI_CU_ENDPOINT 2>&1 | grep -E '^https?://[a-zA-Z0-9._/-]+$') aiAgentEndpoint=$(azd env get-value AZURE_AI_AGENT_ENDPOINT 2>&1 | grep -E '^https?://[a-zA-Z0-9._/:/-]+$') cuApiVersion=$(azd env get-value AZURE_CONTENT_UNDERSTANDING_API_VERSION 2>&1 | grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}(-preview)?$') - deploymentModel=$(azd env get-value AZURE_OPENAI_DEPLOYMENT_MODEL 2>&1 | grep -E '^[a-zA-Z0-9._-]+$') + deploymentModel=$(azd env get-value AZURE_ENV_GPT_MODEL_NAME 2>&1 | grep -E '^[a-zA-Z0-9._-]+$') usecase=$(azd env get-value USE_CASE 2>&1 | grep -E '^[a-zA-Z0-9._-]+$') solutionName=$(azd env get-value SOLUTION_NAME 2>&1 | grep -E '^[a-zA-Z0-9._-]+$') @@ -399,13 +352,12 @@ get_values_from_az_deployment() { aiSearchName=$(extract_value "azureAISearchName" "AZURE_AI_SEARCH_NAME") searchEndpoint=$(extract_value "azureAISearchEndpoint" "AZURE_AI_SEARCH_ENDPOINT") aif_resource_id=$(extract_value "aiFoundryResourceId" "AI_FOUNDRY_RESOURCE_ID") - cu_foundry_resource_id=$(extract_value "cuFoundryResourceId" "CU_FOUNDRY_RESOURCE_ID") openaiEndpoint=$(extract_value "azureOpenAIEndpoint" "AZURE_OPENAI_ENDPOINT") - embeddingModel=$(extract_value "azureOpenAIEmbeddingModel" "AZURE_OPENAI_EMBEDDING_MODEL") + embeddingModel=$(extract_value "azureOpenAIEmbeddingModel" "AZURE_ENV_EMBEDDING_MODEL_NAME") cuEndpoint=$(extract_value "azureOpenAICuEndpoint" "AZURE_OPENAI_CU_ENDPOINT") aiAgentEndpoint=$(extract_value "azureAiAgentEndpoint" "AZURE_AI_AGENT_ENDPOINT") cuApiVersion=$(extract_value "azureContentUnderstandingApiVersion" "AZURE_CONTENT_UNDERSTANDING_API_VERSION") - deploymentModel=$(extract_value "azureOpenAIDeploymentModel" "AZURE_OPENAI_DEPLOYMENT_MODEL") + deploymentModel=$(extract_value "azureOpenAIDeploymentModel" "AZURE_ENV_GPT_MODEL_NAME") usecase=$(extract_value "useCase" "USE_CASE") solutionName=$(extract_value "solutionName" "SOLUTION_NAME") @@ -422,14 +374,13 @@ get_values_from_az_deployment() { ["backendUserMidDisplayName"]="BACKEND_USER_MID_NAME" ["aiSearchName"]="AZURE_AI_SEARCH_NAME" ["aif_resource_id"]="AI_FOUNDRY_RESOURCE_ID" - ["cu_foundry_resource_id"]="CU_FOUNDRY_RESOURCE_ID" ["searchEndpoint"]="AZURE_AI_SEARCH_ENDPOINT" ["openaiEndpoint"]="AZURE_OPENAI_ENDPOINT" - ["embeddingModel"]="AZURE_OPENAI_EMBEDDING_MODEL" + ["embeddingModel"]="AZURE_ENV_EMBEDDING_MODEL_NAME" ["cuEndpoint"]="AZURE_OPENAI_CU_ENDPOINT" ["aiAgentEndpoint"]="AZURE_AI_AGENT_ENDPOINT" ["cuApiVersion"]="AZURE_CONTENT_UNDERSTANDING_API_VERSION" - ["deploymentModel"]="AZURE_OPENAI_DEPLOYMENT_MODEL" + ["deploymentModel"]="AZURE_ENV_GPT_MODEL_NAME" ["usecase"]="USE_CASE" ["solutionName"]="SOLUTION_NAME" ) @@ -524,7 +475,7 @@ echo "" echo "" # Check if all required parameters are provided -if [ -n "$resourceGroupName" ] && [ -n "$azSubscriptionId" ] && [ -n "$storageAccountName" ] && [ -n "$fileSystem" ] && [ -n "$sqlServerName" ] && [ -n "$SqlDatabaseName" ] && [ -n "$backendUserMidClientId" ] && [ -n "$backendUserMidDisplayName" ] && [ -n "$aiSearchName" ] && [ -n "$searchEndpoint" ] && [ -n "$aif_resource_id" ] && [ -n "$cu_foundry_resource_id" ] && [ -n "$openaiEndpoint" ] && [ -n "$embeddingModel" ] && [ -n "$deploymentModel" ] && [ -n "$cuEndpoint" ] && [ -n "$cuApiVersion" ] && [ -n "$aiAgentEndpoint" ] && [ -n "$usecase" ] && [ -n "$solutionName" ]; then +if [ -n "$resourceGroupName" ] && [ -n "$azSubscriptionId" ] && [ -n "$storageAccountName" ] && [ -n "$fileSystem" ] && [ -n "$sqlServerName" ] && [ -n "$SqlDatabaseName" ] && [ -n "$backendUserMidClientId" ] && [ -n "$backendUserMidDisplayName" ] && [ -n "$aiSearchName" ] && [ -n "$searchEndpoint" ] && [ -n "$aif_resource_id" ] && [ -n "$openaiEndpoint" ] && [ -n "$embeddingModel" ] && [ -n "$deploymentModel" ] && [ -n "$cuEndpoint" ] && [ -n "$cuApiVersion" ] && [ -n "$aiAgentEndpoint" ] && [ -n "$usecase" ] && [ -n "$solutionName" ]; then # All parameters provided - use them directly echo "All parameters provided via command line." # Strip FQDN suffix from SQL server name if present @@ -576,7 +527,6 @@ echo "Backend User-Assigned Managed Identity Display Name: $backendUserMidDispla echo "Backend User-Assigned Managed Identity Client ID: $backendUserMidClientId" echo "AI Search Service Name: $aiSearchName" echo "AI Foundry Resource ID: $aif_resource_id" -echo "CU Foundry Resource ID: $cu_foundry_resource_id" echo "Search Endpoint: $searchEndpoint" echo "OpenAI Endpoint: $openaiEndpoint" echo "Embedding Model: $embeddingModel" @@ -607,7 +557,7 @@ echo "copy_kb_files.sh completed successfully." # Call run_create_index_scripts.sh echo "Running run_create_index_scripts.sh" # Pass all required environment variables and backend managed identity info for role assignment -bash "$SCRIPT_DIR/run_create_index_scripts.sh" "$resourceGroupName" "$aiSearchName" "$searchEndpoint" "$sqlServerName" "$SqlDatabaseName" "$backendUserMidDisplayName" "$backendUserMidClientId" "$storageAccountName" "$openaiEndpoint" "$deploymentModel" "$embeddingModel" "$cuEndpoint" "$cuApiVersion" "$aif_resource_id" "$cu_foundry_resource_id" "$aiAgentEndpoint" "$usecase" "$solutionName" +bash "$SCRIPT_DIR/run_create_index_scripts.sh" "$resourceGroupName" "$aiSearchName" "$searchEndpoint" "$sqlServerName" "$SqlDatabaseName" "$backendUserMidDisplayName" "$backendUserMidClientId" "$storageAccountName" "$openaiEndpoint" "$deploymentModel" "$embeddingModel" "$cuEndpoint" "$cuApiVersion" "$aif_resource_id" "$aiAgentEndpoint" "$usecase" "$solutionName" if [ $? -ne 0 ]; then echo "Error: run_create_index_scripts.sh failed." exit 1 diff --git a/infra/scripts/quota_check_params.sh b/infra/scripts/quota_check_params.sh index 20e9473a2..26d8a22f9 100644 --- a/infra/scripts/quota_check_params.sh +++ b/infra/scripts/quota_check_params.sh @@ -47,7 +47,7 @@ log_verbose() { } # Default Models and Capacities (Comma-separated in "model:capacity" format) -DEFAULT_MODEL_CAPACITY="gpt-4o:150,gpt-4o-mini:150,gpt-4:150,text-embedding-ada-002:80" +DEFAULT_MODEL_CAPACITY="gpt-4o:150,gpt-4o-mini:150,gpt-4:150,text-embedding-3-small:80" # Convert the comma-separated string into an array IFS=',' read -r -a MODEL_CAPACITY_PAIRS <<< "$DEFAULT_MODEL_CAPACITY" @@ -93,7 +93,7 @@ az account set --subscription "$AZURE_SUBSCRIPTION_ID" echo "🎯 Active Subscription: $(az account show --query '[name, id]' --output tsv)" # Default Regions to check (Comma-separated, now configurable) -DEFAULT_REGIONS="eastus,eastus2,australiaeast,uksouth,francecentral,westus" +DEFAULT_REGIONS="eastus,eastus2,australiaeast,uksouth,swedencentral,westus,westus3,japaneast,southcentralus,westeurope" IFS=',' read -r -a DEFAULT_REGION_ARRAY <<< "$DEFAULT_REGIONS" # Read parameters (if any) @@ -199,7 +199,7 @@ for REGION in "${REGIONS[@]}"; do if [ "$AVAILABLE" -ge "$REQUIRED_CAPACITY" ]; then FOUND=true - if [ "$MODEL_NAME" = "text-embedding-ada-002" ]; then + if [ "$MODEL_NAME" = "text-embedding-3-small" ]; then TEXT_EMBEDDING_AVAILABLE=true fi AT_LEAST_ONE_MODEL_AVAILABLE=true diff --git a/infra/scripts/run_create_agents_scripts.sh b/infra/scripts/run_create_agents_scripts.sh index 42aa2bdb2..6f7914d4c 100644 --- a/infra/scripts/run_create_agents_scripts.sh +++ b/infra/scripts/run_create_agents_scripts.sh @@ -330,6 +330,8 @@ titleAgentName="" while IFS='=' read -r key value; do # Skip empty lines or lines without '=' [ -z "$key" ] && continue + # Strip trailing carriage return if present (Windows line endings) + value="${value%$'\r'}" case "$key" in conversationAgentName) conversationAgentName="$value" diff --git a/infra/scripts/run_create_index_scripts.sh b/infra/scripts/run_create_index_scripts.sh index 5403e795f..2e0794170 100644 --- a/infra/scripts/run_create_index_scripts.sh +++ b/infra/scripts/run_create_index_scripts.sh @@ -18,10 +18,9 @@ embedding_model="${11}" cu_endpoint="${12}" cu_api_version="${13}" aif_resource_id="${14}" -cu_foundry_resource_id="${15}" -ai_agent_endpoint="${16}" -usecase="${17}" -solution_name="${18}" +ai_agent_endpoint="${15}" +usecase="${16}" +solution_name="${17}" pythonScriptPath="$SCRIPT_DIR/index_scripts/" @@ -86,16 +85,14 @@ if [ -z "$role_assignment" ]; then fi fi -### Assign Azure AI User role to the signed in user for CU Foundry ### -if [ -n "$cu_foundry_resource_id" ] && [ "$cu_foundry_resource_id" != "null" ]; then - role_assignment=$(MSYS_NO_PATHCONV=1 az role assignment list --role 53ca6127-db72-4b80-b1b0-d745d6d5456d --scope $cu_foundry_resource_id --assignee $signed_user_id --query "[].roleDefinitionId" -o tsv) - if [ -z "$role_assignment" ]; then - echo "✓ Assigning Azure AI User role for CU Foundry" - MSYS_NO_PATHCONV=1 az role assignment create --assignee $signed_user_id --role 53ca6127-db72-4b80-b1b0-d745d6d5456d --scope $cu_foundry_resource_id --output none - if [ $? -ne 0 ]; then - echo "✗ Failed to assign Azure AI User role for CU Foundry" - exit 1 - fi +### Assign Cognitive Services OpenAI User role to the signed in user for AI Foundry ### +role_assignment=$(MSYS_NO_PATHCONV=1 az role assignment list --role 5e0bd9bd-7b93-4f28-af87-19fc36ad61bd --scope $aif_resource_id --assignee $signed_user_id --query "[].roleDefinitionId" -o tsv) +if [ -z "$role_assignment" ]; then + echo "✓ Assigning Cognitive Services OpenAI User role for AI Foundry" + MSYS_NO_PATHCONV=1 az role assignment create --assignee $signed_user_id --role 5e0bd9bd-7b93-4f28-af87-19fc36ad61bd --scope $aif_resource_id --output none + if [ $? -ne 0 ]; then + echo "✗ Failed to assign Cognitive Services OpenAI User role for AI Foundry" + exit 1 fi fi @@ -144,7 +141,7 @@ if [ $? -ne 0 ]; then fi echo "✓ Creating CU template for text" -python ${pythonScriptPath}02_create_cu_template_text.py --cu_endpoint="$cu_endpoint" --cu_api_version="$cu_api_version" +python ${pythonScriptPath}02_create_cu_template_text.py --cu_endpoint="$cu_endpoint" --cu_api_version="$cu_api_version" --deployment_model="$deployment_model" --embedding_model="$embedding_model" if [ $? -ne 0 ]; then echo "Error: 02_create_cu_template_text.py failed." error_flag=true @@ -152,7 +149,7 @@ fi if [ "$usecase" == "telecom" ]; then echo "✓ Creating CU template for audio" - python ${pythonScriptPath}02_create_cu_template_audio.py --cu_endpoint="$cu_endpoint" --cu_api_version="$cu_api_version" + python ${pythonScriptPath}02_create_cu_template_audio.py --cu_endpoint="$cu_endpoint" --cu_api_version="$cu_api_version" --deployment_model="$deployment_model" --embedding_model="$embedding_model" if [ $? -ne 0 ]; then echo "Error: 02_create_cu_template_audio.py failed." error_flag=true diff --git a/infra/scripts/validate_bicep_params.py b/infra/scripts/validate_bicep_params.py new file mode 100644 index 000000000..f1f0f371f --- /dev/null +++ b/infra/scripts/validate_bicep_params.py @@ -0,0 +1,691 @@ +""" +Bicep Parameter Mapping Validator +================================= +Validates that parameter names in *.parameters.json files exactly match +the param declarations in their corresponding Bicep templates. + +Checks performed: + 1. Whitespace – parameter names must have no leading/trailing spaces. + 2. Existence – every JSON parameter must map to a `param` in the Bicep file. + 3. Casing – names must match exactly (case-sensitive). + 4. Orphaned – required Bicep params (no default) missing from the JSON file. + 5. Env vars – parameter values bound to environment variables must use the + AZURE_ENV_* naming convention, except for explicitly allowed + names (for example, AZURE_LOCATION, AZURE_EXISTING_AIPROJECT_RESOURCE_ID). + +Usage: + # Validate a specific pair + python validate_bicep_params.py --bicep main.bicep --params main.parameters.json + + # Auto-discover all *.parameters.json files under infra/ + python validate_bicep_params.py --dir infra + + # CI mode – exit code 1 on any error + python validate_bicep_params.py --dir infra --strict + +Returns exit-code 0 when no errors are found, 1 when errors are found (in --strict mode). +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from dataclasses import dataclass, field +from pathlib import Path + +# Environment variables exempt from the AZURE_ENV_ naming convention. +_ENV_VAR_EXCEPTIONS = {"AZURE_LOCATION", "AZURE_EXISTING_AIPROJECT_RESOURCE_ID", "USE_CASE"} + +# --------------------------------------------------------------------------- +# Bicep param parser +# --------------------------------------------------------------------------- + +# Matches lines like: param environmentName string +# param tags resourceInput<...> +# param gptDeploymentCapacity int = 150 +# Ignores commented-out lines (// param ...). +# Captures the type token and the rest of the line so we can detect defaults. +_PARAM_RE = re.compile( + r"^(?!//)[ \t]*param\s+(?P[A-Za-z_]\w*)\s+(?P\S+)(?P.*)", + re.MULTILINE, +) + + +@dataclass +class BicepParam: + name: str + has_default: bool + + +def parse_bicep_params(bicep_path: Path) -> list[BicepParam]: + """Extract all `param` declarations from a Bicep file.""" + text = bicep_path.read_text(encoding="utf-8-sig") + params: list[BicepParam] = [] + for match in _PARAM_RE.finditer(text): + name = match.group("name") + param_type = match.group("type") + rest = match.group("rest") + # A param is optional if it has a default value (= ...) or is nullable (type ends with ?) + has_default = "=" in rest or param_type.endswith("?") + params.append(BicepParam(name=name, has_default=has_default)) + return params + + +# --------------------------------------------------------------------------- +# Parameters JSON parser +# --------------------------------------------------------------------------- + + +def parse_parameters_json(json_path: Path) -> list[str]: + """Return the raw parameter key names (preserving whitespace) from a + parameters JSON file.""" + text = json_path.read_text(encoding="utf-8-sig") + # azd parameter files may include ${VAR} or ${VAR=default} placeholders inside + # string values. These are valid JSON strings, but we sanitize them so that + # json.loads remains resilient to azd-specific placeholders and any unusual + # default formats. + sanitized = re.sub(r'"\$\{[^}]+\}"', '"__placeholder__"', text) + try: + data = json.loads(sanitized) + except json.JSONDecodeError: + # Fallback: extract keys with regex for resilience. + return _extract_keys_regex(text) + return list(data.get("parameters", {}).keys()) + + +def parse_parameters_env_vars(json_path: Path) -> dict[str, list[str]]: + """Return a mapping of parameter name → list of azd env var names + referenced in its value (e.g. ``${AZURE_ENV_NAME}``).""" + text = json_path.read_text(encoding="utf-8-sig") + result: dict[str, list[str]] = {} + params = {} + + # Parse the JSON to get the proper parameter structure. + sanitized = re.sub(r'"\$\{([^}]+)\}"', r'"__azd_\1__"', text) + try: + data = json.loads(sanitized) + params = data.get("parameters", {}) + except json.JSONDecodeError: # Parameters file may have azd variable placeholders + pass + + # Walk each top-level parameter and scan its entire serialized value + # for ${VAR} references from the original text. + for param_name, param_obj in params.items(): + # Find the raw text block for this parameter in the original file + # by scanning for all ${VAR} patterns in the original value section. + raw_value = json.dumps(param_obj) + # Restore original var references from the sanitized placeholders + for m in re.finditer(r'__azd_([^_].*?)__', raw_value): + var_ref = m.group(1) + # var_ref may contain "=default", extract just the var name + var_name = var_ref.split("=")[0].strip() + if re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', var_name): + result.setdefault(param_name, []).append(var_name) + + return result + + +def _extract_keys_regex(text: str) -> list[str]: + """Fallback key extraction via regex when JSON is non-standard.""" + # Matches the key inside "parameters": { "key": ... } + keys: list[str] = [] + in_params = False + for line in text.splitlines(): + if '"parameters"' in line: + in_params = True + continue + if in_params: + m = re.match(r'\s*"([^"]+)"\s*:', line) + if m: + keys.append(m.group(1)) + return keys + + +# --------------------------------------------------------------------------- +# Validation logic +# --------------------------------------------------------------------------- + +@dataclass +class ValidationIssue: + severity: str # "ERROR" or "WARNING" + param_file: str + bicep_file: str + param_name: str + message: str + + +@dataclass +class ValidationResult: + pair: str + issues: list[ValidationIssue] = field(default_factory=list) + + @property + def has_errors(self) -> bool: + return any(i.severity == "ERROR" for i in self.issues) + + +def validate_pair( + bicep_path: Path, + params_path: Path, +) -> ValidationResult: + """Validate a single (bicep, parameters.json) pair.""" + result = ValidationResult( + pair=f"{params_path.name} -> {bicep_path.name}" + ) + + bicep_params = parse_bicep_params(bicep_path) + bicep_names = {p.name for p in bicep_params} + bicep_names_lower = {p.name.lower(): p.name for p in bicep_params} + required_bicep = {p.name for p in bicep_params if not p.has_default} + + json_keys = parse_parameters_json(params_path) + + seen_json_keys: set[str] = set() + + for raw_key in json_keys: + stripped = raw_key.strip() + + # 1. Whitespace check + if raw_key != stripped: + result.issues.append(ValidationIssue( + severity="ERROR", + param_file=str(params_path), + bicep_file=str(bicep_path), + param_name=repr(raw_key), + message=( + f"Parameter name has leading/trailing whitespace. " + f"Raw key: {repr(raw_key)}, expected: {repr(stripped)}" + ), + )) + + # 2. Exact match check + if stripped not in bicep_names: + # 3. Case-insensitive near-match + suggestion = bicep_names_lower.get(stripped.lower()) + if suggestion: + result.issues.append(ValidationIssue( + severity="ERROR", + param_file=str(params_path), + bicep_file=str(bicep_path), + param_name=stripped, + message=( + f"Case mismatch: JSON has '{stripped}', " + f"Bicep declares '{suggestion}'." + ), + )) + else: + result.issues.append(ValidationIssue( + severity="ERROR", + param_file=str(params_path), + bicep_file=str(bicep_path), + param_name=stripped, + message=( + f"Parameter '{stripped}' exists in JSON but has no " + f"matching param in the Bicep template." + ), + )) + seen_json_keys.add(stripped) + + # 4. Required Bicep params missing from JSON + for req in sorted(required_bicep - seen_json_keys): + result.issues.append(ValidationIssue( + severity="WARNING", + param_file=str(params_path), + bicep_file=str(bicep_path), + param_name=req, + message=( + f"Required Bicep param '{req}' (no default value) is not " + f"supplied in the parameters file." + ), + )) + + # 5. Env var naming convention – all azd vars should start with AZURE_ENV_ + env_vars = parse_parameters_env_vars(params_path) + for param_name, var_names in sorted(env_vars.items()): + for var in var_names: + if not var.startswith("AZURE_ENV_") and var not in _ENV_VAR_EXCEPTIONS: + result.issues.append(ValidationIssue( + severity="WARNING", + param_file=str(params_path), + bicep_file=str(bicep_path), + param_name=param_name, + message=( + f"Env var '${{{var}}}' does not follow the " + f"AZURE_ENV_ naming convention." + ), + )) + + return result + + +# --------------------------------------------------------------------------- +# Discovery – find (bicep, params) pairs automatically +# --------------------------------------------------------------------------- + +def discover_pairs(infra_dir: Path) -> list[tuple[Path, Path]]: + """For each *.parameters.json, find the matching Bicep file. + + Naming convention: a file like ``main.waf.parameters.json`` is a + variant of ``main.parameters.json`` — the user copies its contents + into ``main.parameters.json`` before running ``azd up``. Both + files should therefore be validated against ``main.bicep``. + + Resolution order: + 1. Exact stem match (e.g. ``foo.parameters.json`` → ``foo.bicep``). + 2. Base-stem match (e.g. ``main.waf.parameters.json`` → ``main.bicep``). + """ + pairs: list[tuple[Path, Path]] = [] + for pf in sorted(infra_dir.rglob("*.parameters.json")): + stem = pf.name.replace(".parameters.json", "") + bicep_candidate = pf.parent / f"{stem}.bicep" + if bicep_candidate.exists(): + pairs.append((bicep_candidate, pf)) + else: + # Try the base stem (first segment before the first dot). + base_stem = stem.split(".")[0] + base_candidate = pf.parent / f"{base_stem}.bicep" + if base_candidate.exists(): + pairs.append((base_candidate, pf)) + else: + print(f" [SKIP] No matching Bicep file for {pf.name}") + return pairs + + +# --------------------------------------------------------------------------- +# Reporting +# --------------------------------------------------------------------------- + +_COLORS = { + "ERROR": "\033[91m", # red + "WARNING": "\033[93m", # yellow + "OK": "\033[92m", # green + "RESET": "\033[0m", +} + + +def print_report(results: list[ValidationResult], *, use_color: bool = True) -> None: + c = _COLORS if use_color else {k: "" for k in _COLORS} + total_errors = 0 + total_warnings = 0 + + for r in results: + errors = [i for i in r.issues if i.severity == "ERROR"] + warnings = [i for i in r.issues if i.severity == "WARNING"] + total_errors += len(errors) + total_warnings += len(warnings) + + if not r.issues: + print(f"\n{c['OK']}[PASS]{c['RESET']} {r.pair}") + elif errors: + print(f"\n{c['ERROR']}[FAIL]{c['RESET']} {r.pair}") + else: + print(f"\n{c['WARNING']}[WARN]{c['RESET']} {r.pair}") + + for issue in r.issues: + tag = ( + f"{c['ERROR']}ERROR{c['RESET']}" + if issue.severity == "ERROR" + else f"{c['WARNING']}WARN {c['RESET']}" + ) + print(f" {tag} {issue.param_name}: {issue.message}") + + print(f"\n{'='*60}") + print(f"Total: {total_errors} error(s), {total_warnings} warning(s)") + if total_errors == 0: + print(f"{c['OK']}All parameter mappings are valid.{c['RESET']}") + else: + print(f"{c['ERROR']}Parameter mapping issues detected!{c['RESET']}") + + +# --------------------------------------------------------------------------- +# HTML email report +# --------------------------------------------------------------------------- + +def _html_escape(text: str) -> str: + """Escape HTML special characters.""" + return ( + text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace('"', """) + ) + + +def generate_html_report( + results: list[ValidationResult], + *, + accelerator_name: str = "", + run_url: str = "", + scan_dir: str = "", +) -> str: + """Build a structured HTML email body from validation results.""" + total_errors = sum( + 1 for r in results for i in r.issues if i.severity == "ERROR" + ) + total_warnings = sum( + 1 for r in results for i in r.issues if i.severity == "WARNING" + ) + has_errors = total_errors > 0 + overall_status = "Issues Detected" if has_errors else "Passed" + status_color = "#D32F2F" if has_errors else "#2E7D32" + status_bg = "#FFEBEE" if has_errors else "#E8F5E9" + status_icon = "❌" if has_errors else "✅" + + parts: list[str] = [] + + # --- Document wrapper (Outlook-compatible, no gradient/border-radius/box-shadow) --- + parts.append( + '' + '' + '' + '
' + '' + ) + + # --- Header banner (solid color, Outlook-safe) --- + parts.append( + f'' + ) + + # --- Summary card --- + parts.append( + f'") + + # --- Per-pair detail sections --- + parts.append('") + + # --- Footer with run URL --- + footer_parts: list[str] = [] + if run_url: + footer_parts.append( + f'View Workflow Run' + ) + if has_errors: + footer_parts.append( + '

' + 'Please fix the parameter mapping issues at your earliest convenience.

' + ) + footer_parts.append( + '

' + 'Best regards,
Your Automation Team

' + ) + parts.append( + f'' + ) + + # --- Close wrapper --- + parts.append("
' + f'

' + f'Bicep Parameter Validation Report

' + f'

' + f'{_html_escape(accelerator_name) if accelerator_name else "Accelerator"}' + f' — Automated Check

' + f'
' + f'' + f'' + f'
' + f'' + f'{status_icon} Overall Status: {overall_status}' + f'
' + f'' + ) + # Accelerator name pill + if accelerator_name: + parts.append( + f'' + ) + # Scan directory pill + if scan_dir: + parts.append( + f'' + ) + # Error count pill + err_pill_color = "#D32F2F" if total_errors > 0 else "#2E7D32" + parts.append( + f'' + ) + # Warning count pill + warn_pill_color = "#F57C00" if total_warnings > 0 else "#2E7D32" + parts.append( + f'' + ) + parts.append("
' + f'Accelerator
' + f'{_html_escape(accelerator_name)}' + f'
' + f'Scan Directory
' + f'{_html_escape(scan_dir)}/' + f'
' + f'Errors
' + f'' + f'{total_errors}
' + f'Warnings
' + f'' + f'{total_warnings}
') + for r in results: + errors = [i for i in r.issues if i.severity == "ERROR"] + warnings = [i for i in r.issues if i.severity == "WARNING"] + + if not r.issues: + badge = ( + 'PASS' + ) + elif errors: + badge = ( + 'FAIL' + ) + else: + badge = ( + 'WARN' + ) + + parts.append( + f'' + f'' + ) + + if r.issues: + # --- Errors section --- + if errors: + parts.append( + '' + '") + + # --- Warnings section --- + if warnings: + parts.append( + '' + '") + else: + parts.append( + '' + ) + + parts.append("
' + f'{badge} ' + f'' + f'{_html_escape(r.pair)}' + f'' + f'{len(errors)} error(s), {len(warnings)} warning(s)' + f'
' + '' + '● Errors
' + '' + '' + '' + '' + ) + for idx, issue in enumerate(errors): + bg = "#ffffff" if idx % 2 == 0 else "#fff5f5" + parts.append( + f'' + f'' + f'' + f'' + ) + parts.append("
ParameterDetails
' + f'{_html_escape(issue.param_name)}{_html_escape(issue.message)}
' + '' + '● Warnings
' + '' + '' + '' + '' + ) + for idx, issue in enumerate(warnings): + bg = "#ffffff" if idx % 2 == 0 else "#fffaf0" + parts.append( + f'' + f'' + f'' + f'' + ) + parts.append("
ParameterDetails
' + f'{_html_escape(issue.param_name)}{_html_escape(issue.message)}
All parameters validated successfully.' + '
") + + parts.append("
' + f'{"".join(footer_parts)}
") + return "".join(parts) + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Bicep ↔ parameters.json parameter mappings.", + ) + parser.add_argument( + "--bicep", + type=Path, + help="Path to a specific Bicep template.", + ) + parser.add_argument( + "--params", + type=Path, + help="Path to a specific parameters JSON file.", + ) + parser.add_argument( + "--dir", + type=Path, + help="Directory to scan for *.parameters.json files (auto-discovers pairs).", + ) + parser.add_argument( + "--strict", + action="store_true", + help="Exit with code 1 if any errors are found.", + ) + parser.add_argument( + "--no-color", + action="store_true", + help="Disable colored output (useful for CI logs).", + ) + parser.add_argument( + "--json-output", + type=Path, + help="Write results as JSON to the given file path.", + ) + parser.add_argument( + "--html-output", + type=Path, + help="Write a structured HTML email report to the given file path.", + ) + parser.add_argument( + "--accelerator-name", + type=str, + default="", + help="Accelerator display name for the HTML report header.", + ) + parser.add_argument( + "--run-url", + type=str, + default="", + help="Workflow run URL to include in the HTML report footer.", + ) + args = parser.parse_args() + + results: list[ValidationResult] = [] + + if args.bicep and args.params: + results.append(validate_pair(args.bicep, args.params)) + elif args.dir: + pairs = discover_pairs(args.dir) + if not pairs: + print(f"No (bicep, parameters.json) pairs found under {args.dir}") + return 0 + for bicep_path, params_path in pairs: + results.append(validate_pair(bicep_path, params_path)) + else: + parser.error("Provide either --bicep/--params or --dir.") + + print_report(results, use_color=not args.no_color) + + # Optional JSON output for CI artifact consumption + if args.json_output: + json_data = [] + for r in results: + for issue in r.issues: + json_data.append({ + "severity": issue.severity, + "paramFile": issue.param_file, + "bicepFile": issue.bicep_file, + "paramName": issue.param_name, + "message": issue.message, + }) + args.json_output.parent.mkdir(parents=True, exist_ok=True) + args.json_output.write_text( + json.dumps(json_data, indent=2), encoding="utf-8" + ) + print(f"\nJSON report written to {args.json_output}") + + # Optional HTML email report + if args.html_output: + scan_dir = str(args.dir) if args.dir else "" + html = generate_html_report( + results, + accelerator_name=args.accelerator_name, + run_url=args.run_url, + scan_dir=scan_dir, + ) + args.html_output.parent.mkdir(parents=True, exist_ok=True) + args.html_output.write_text(html, encoding="utf-8") + print(f"HTML report written to {args.html_output}") + + has_errors = any(r.has_errors for r in results) + return 1 if args.strict and has_errors else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/next-steps.md b/next-steps.md index 80277b44b..773412673 100644 --- a/next-steps.md +++ b/next-steps.md @@ -35,21 +35,20 @@ To describe the infrastructure and application, `azure.yaml` along with Infrastr ```yaml - azure.yaml # azd project configuration - infra/ # Infrastructure-as-code Bicep files - - main.bicep # Subscription level resources - - resources.bicep # Primary resource group resources + - main.bicep # Main deployment template - modules/ # Library modules ``` -The resources declared in [resources.bicep](./infra/resources.bicep) are provisioned when running `azd up` or `azd provision`. +The resources declared in [main.bicep](./infra/main.bicep) are provisioned when running `azd up` or `azd provision`. This includes: - -- Azure Container App to host the 'app' service. -- Azure Container App to host the 'km-charts-function' service. -- Azure Container App to host the 'km-rag-function' service. -- Azure Container App to host the 'add-user-scripts' service. -- Azure Container App to host the 'fabric-scripts' service. -- Azure Container App to host the 'index-scripts' service. +- AI Foundry (AI Services and AI Project) +- AI Search +- Storage Account +- Cosmos DB +- SQL Database +- App Service Plan and Web Apps (backend API and frontend) +- Virtual Network and Private Endpoints (WAF deployment) More information about [Bicep](https://aka.ms/bicep) language. @@ -83,13 +82,13 @@ Q: I visited the service endpoint listed, and I'm seeing a blank page, a generic A: Your service may have failed to start, or it may be missing some configuration settings. To investigate further: 1. Run `azd show`. Click on the link under "View in Azure Portal" to open the resource group in Azure Portal. -2. Navigate to the specific Container App service that is failing to deploy. -3. Click on the failing revision under "Revisions with Issues". -4. Review "Status details" for more information about the type of failure. -5. Observe the log outputs from Console log stream and System log stream to identify any errors. -6. If logs are written to disk, use *Console* in the navigation to connect to a shell within the running container. +2. Navigate to the specific App Service that is failing to deploy. +3. Check the **Deployment Center** logs for deployment errors. +4. Review **Log stream** for application runtime errors. +5. Check **Diagnose and solve problems** for platform-level issues. +6. If you need access to the running container, use the App Service **SSH/Console** experience in Azure Portal or run `az webapp ssh`. Use *Advanced Tools (Kudu)* to inspect the SCM site, filesystem, and any logs written to disk. -For more troubleshooting information, visit [Container Apps troubleshooting](https://learn.microsoft.com/azure/container-apps/troubleshooting). +For more troubleshooting information, visit [App Service troubleshooting](https://learn.microsoft.com/azure/app-service/overview-diagnostics). ### Additional information diff --git a/src/App/WebApp.Dockerfile b/src/App/WebApp.Dockerfile index f45203611..69051f04b 100644 --- a/src/App/WebApp.Dockerfile +++ b/src/App/WebApp.Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine AS build +FROM node:24-alpine AS build WORKDIR /home/node/app COPY ./package*.json ./ @@ -17,6 +17,12 @@ COPY env.sh /docker-entrypoint.d/env.sh RUN chmod +x /docker-entrypoint.d/env.sh RUN sed -i 's/\r$//' /docker-entrypoint.d/env.sh +# Custom nginx config with API proxy support +COPY nginx.conf /etc/nginx/nginx.conf + +# Create empty api-proxy conf (overwritten at startup for WAF deployments) +RUN mkdir -p /etc/nginx/conf.d && touch /etc/nginx/conf.d/api-proxy.conf + # Expose the application port EXPOSE 3000 diff --git a/src/App/env.sh b/src/App/env.sh index 8346ce2bb..a2f5aab6d 100644 --- a/src/App/env.sh +++ b/src/App/env.sh @@ -1,10 +1,48 @@ #!/bin/sh + +# If BACKEND_API_HOST is set (WAF mode), configure nginx reverse proxy +if [ -n "$BACKEND_API_HOST" ]; then + # Write nginx proxy config for /api/ and /history/ routes + cat > /etc/nginx/conf.d/api-proxy.conf << PROXYEOF +location /api/ { + resolver 168.63.129.16 valid=30s; + set \$backend "https://$BACKEND_API_HOST"; + proxy_pass \$backend; + proxy_set_header Host $BACKEND_API_HOST; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + proxy_ssl_server_name on; + proxy_read_timeout 300s; + proxy_connect_timeout 60s; + proxy_buffering off; +} + +location /history/ { + resolver 168.63.129.16 valid=30s; + set \$backend "https://$BACKEND_API_HOST"; + proxy_pass \$backend; + proxy_set_header Host $BACKEND_API_HOST; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + proxy_ssl_server_name on; + proxy_read_timeout 300s; + proxy_connect_timeout 60s; + proxy_buffering off; +} +PROXYEOF +else + # Ensure empty file exists for non-WAF deployments + > /etc/nginx/conf.d/api-proxy.conf +fi + +# Replace APP_* env vars in built frontend files (existing behavior) for i in $(env | grep ^APP_) do key=$(echo $i | cut -d '=' -f 1) value=$(echo $i | cut -d '=' -f 2-) echo $key=$value - # Use sed to replace only the exact matches of the key find /usr/share/nginx/html -type f -exec sed -i "s|\b${key}\b|${value}|g" '{}' + done echo 'done' \ No newline at end of file diff --git a/src/App/nginx.conf b/src/App/nginx.conf new file mode 100644 index 000000000..58f13b345 --- /dev/null +++ b/src/App/nginx.conf @@ -0,0 +1,34 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Include API proxy config (generated at startup for WAF/private networking deployments) + include /etc/nginx/conf.d/api-proxy.conf; + + # Handle React Router + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + } +} diff --git a/src/App/package-lock.json b/src/App/package-lock.json index 5499050a5..6e4ccf05c 100644 --- a/src/App/package-lock.json +++ b/src/App/package-lock.json @@ -8,36 +8,39 @@ "name": "km-chart-visualization", "version": "0.1.0", "dependencies": { - "@azure/msal-browser": "^4.24.1", - "@azure/msal-react": "^3.0.23", - "@fluentui/react": "^8.125.4", - "@fluentui/react-components": "^9.72.11", - "@fluentui/react-icons": "^2.0.317", - "@testing-library/jest-dom": "^6.9.0", - "@testing-library/react": "^16.3.1", + "@azure/msal-browser": "^4.30.0", + "@azure/msal-react": "^3.0.29", + "@fluentui/react": "^8.125.5", + "@fluentui/react-components": "^9.73.7", + "@fluentui/react-icons": "^2.0.324", + "@reduxjs/toolkit": "^2.11.2", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", "@types/d3": "^7.4.3", "@types/jest": "^30.0.0", - "@types/node": "^25.1.0", - "@types/react": "^18.3.11", - "@types/react-dom": "^18.3.1", - "axios": "^1.13.5", - "chart.js": "^4.5.0", + "@types/node": "^25.6.0", + "@types/react": "^18.3.28", + "@types/react-dom": "^18.3.7", + "chart.js": "^4.5.1", "d3": "^7.9.0", - "d3-cloud": "^1.2.8", + "d3-cloud": "^1.2.9", "d3-color": "^3.1.0", - "lodash-es": "^4.17.22", + "lodash-es": "^4.18.1", "react": "^18.3.1", "react-chartjs-2": "^5.3.1", "react-d3-cloud": "^1.0.6", "react-dom": "^18.3.1", "react-markdown": "^10.1.0", + "react-redux": "^9.1.2", "react-scripts": "^5.0.1", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-supersub": "^1.0.0", + "scheduler": "^0.27.0", "typescript": "^4.9.5", - "web-vitals": "^5.1.0" + "web-vitals": "^5.2.0" }, "devDependencies": { "@types/chart.js": "^4.0.1", @@ -67,36 +70,36 @@ } }, "node_modules/@azure/msal-browser": { - "version": "4.29.0", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.29.0.tgz", - "integrity": "sha512-/f3eHkSNUTl6DLQHm+bKecjBKcRQxbd/XLx8lvSYp8Nl/HRyPuIPOijt9Dt0sH50/SxOwQ62RnFCmFlGK+bR/w==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.30.0.tgz", + "integrity": "sha512-HBBKfbZkMVzzF5bofvS1cXuNHFVc+gt4/HOnCmG/0hsHuZRJvJvDg/+7nTwIpoqvJc8BQp5o23rBUfisOLxR+w==", "license": "MIT", "dependencies": { - "@azure/msal-common": "15.15.0" + "@azure/msal-common": "15.17.0" }, "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-common": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.15.0.tgz", - "integrity": "sha512-/n+bN0AKlVa+AOcETkJSKj38+bvFs78BaP4rNtv3MJCmPH0YrHiskMRe74OhyZ5DZjGISlFyxqvf9/4QVEi2tw==", + "version": "15.17.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.17.0.tgz", + "integrity": "sha512-VQ5/gTLFADkwue+FohVuCqlzFPUq4xSrX8jeZe+iwZuY6moliNC8xt86qPVNYdtbQfELDf2Nu6LI+demFPHGgw==", "license": "MIT", "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-react": { - "version": "3.0.27", - "resolved": "https://registry.npmjs.org/@azure/msal-react/-/msal-react-3.0.27.tgz", - "integrity": "sha512-EKXCyUM2Yye7w3D50FCD19YO7dVkoTJAeTRtMaPKlh5K9oH94ded27sxAgI177COLaN/ZaHHSm8fmvv3kIYH4w==", + "version": "3.0.29", + "resolved": "https://registry.npmjs.org/@azure/msal-react/-/msal-react-3.0.29.tgz", + "integrity": "sha512-RpFfq3aIpmKajcshbaJH7Q/1CesxQRAeKorMv+uMpDw98jvi+/L0RJkNnTRmeXrV3aM34kj2LFWBQrQ9DOXs1Q==", "license": "MIT", "engines": { "node": ">=10" }, "peerDependencies": { - "@azure/msal-browser": "^4.29.0", + "@azure/msal-browser": "^4.30.0", "react": "^16.8.0 || ^17 || ^18 || ^19.2.1" } }, @@ -308,9 +311,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.6.tgz", - "integrity": "sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==", + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", @@ -485,22 +488,22 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", "license": "MIT", "dependencies": { "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -1952,9 +1955,9 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.0.tgz", - "integrity": "sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.2.tgz", + "integrity": "sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==", "license": "MIT", "dependencies": { "@babel/compat-data": "^7.29.0", @@ -2098,9 +2101,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2684,17 +2687,17 @@ } }, "node_modules/@fluentui/react-accordion": { - "version": "9.9.2", - "resolved": "https://registry.npmjs.org/@fluentui/react-accordion/-/react-accordion-9.9.2.tgz", - "integrity": "sha512-Mmi5nVKfQrBiBiD1JPVtCmIMrR1CpCy8hsWZLwv/pHt+uHHyW9HyrPXwiOitj3ookA5ec1kXyl34BN8RUi7DGQ==", + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-accordion/-/react-accordion-9.10.0.tgz", + "integrity": "sha512-EwjRfBdC3esMEP++PddyF7bVMSv9+t2W8AY5GkNcwDsqAW3D4zhlvxXBAb3qmpgXy4qMxRWGL8cEaiWgMpH1sg==", "license": "MIT", "dependencies": { "@fluentui/react-aria": "^9.17.10", "@fluentui/react-context-selector": "^9.2.15", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-motion": "^9.13.0", - "@fluentui/react-motion-components-preview": "^0.15.2", + "@fluentui/react-motion": "^9.14.0", + "@fluentui/react-motion-components-preview": "^0.15.3", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", @@ -2710,13 +2713,13 @@ } }, "node_modules/@fluentui/react-alert": { - "version": "9.0.0-beta.135", - "resolved": "https://registry.npmjs.org/@fluentui/react-alert/-/react-alert-9.0.0-beta.135.tgz", - "integrity": "sha512-Qkr89e6tl4q0fhzfx9Wzb3ltiqbFtZj7AhT+CHZdW0I6KtpfGmJnvzaqvz0KXMdrKROTgvkA1Ny3Epf9ortc0Q==", + "version": "9.0.0-beta.138", + "resolved": "https://registry.npmjs.org/@fluentui/react-alert/-/react-alert-9.0.0-beta.138.tgz", + "integrity": "sha512-mE3nMx1ngevvmFcp/2sePyJrdE8nme7eqCv1ppUT+mTIA1RYkR8hzBld1+DV1qJYc+F6DCeg4gImuQuu1OXiGA==", "license": "MIT", "dependencies": { - "@fluentui/react-avatar": "^9.10.2", - "@fluentui/react-button": "^9.8.2", + "@fluentui/react-avatar": "^9.11.0", + "@fluentui/react-button": "^9.9.0", "@fluentui/react-icons": "^2.0.239", "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-tabster": "^9.26.13", @@ -2753,20 +2756,20 @@ } }, "node_modules/@fluentui/react-avatar": { - "version": "9.10.2", - "resolved": "https://registry.npmjs.org/@fluentui/react-avatar/-/react-avatar-9.10.2.tgz", - "integrity": "sha512-0qy3U1S80c2Z0A8O/3Ko8XmG4d/NCof1XZ1jclbneKLDT0PeoX3BUlDDgCalOEwb0s1x6TjLabam5FtY4E30cg==", + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-avatar/-/react-avatar-9.11.0.tgz", + "integrity": "sha512-3MogJIiOGilKh9y/sWy0Cali1tpvWQNwcs2ryL7EVXi5xwTfKQM/WEgEnW2z+KtumDQUsRqlCHCSoi+x+BF8Qg==", "license": "MIT", "dependencies": { - "@fluentui/react-badge": "^9.4.15", + "@fluentui/react-badge": "^9.5.1", "@fluentui/react-context-selector": "^9.2.15", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-popover": "^9.14.0", + "@fluentui/react-popover": "^9.14.1", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", - "@fluentui/react-tooltip": "^9.9.3", + "@fluentui/react-tooltip": "^9.10.0", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" @@ -2779,9 +2782,9 @@ } }, "node_modules/@fluentui/react-badge": { - "version": "9.4.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-badge/-/react-badge-9.4.15.tgz", - "integrity": "sha512-KgFUJHBHP76vE3EDuPg/ml7lGqxs9zJ634e+vtxn8D7ghCZ6h9P6A0WbmgsPcN6MZoBZYLzzYT3OJ6Vmu3BM8g==", + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-badge/-/react-badge-9.5.1.tgz", + "integrity": "sha512-OHS15ovGFPShrAA9U+hCyloJEyffC9gdif0a27AOIB9aVlF/hTzG7toxxulcg4ar4F9X3xXk/uccCCa2kzK0Gw==", "license": "MIT", "dependencies": { "@fluentui/react-icons": "^2.0.245", @@ -2800,16 +2803,16 @@ } }, "node_modules/@fluentui/react-breadcrumb": { - "version": "9.3.17", - "resolved": "https://registry.npmjs.org/@fluentui/react-breadcrumb/-/react-breadcrumb-9.3.17.tgz", - "integrity": "sha512-POnwCFyvXabq7lNtJRslASNkrm0iRoXpnrWwh0LyBTFZRDiGDKaV18Bpk0UiuQNTUurVQiH513164XKHIP+d7Q==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-breadcrumb/-/react-breadcrumb-9.4.0.tgz", + "integrity": "sha512-QpCjYlM3JTMnNwh/sDehDbuAVjTcgSfjkPdSmFaPk2lPHpER32CBcJVhheP9en2U5NbW1e+Gtvq8y06RN8FCWw==", "license": "MIT", "dependencies": { "@fluentui/react-aria": "^9.17.10", - "@fluentui/react-button": "^9.8.2", + "@fluentui/react-button": "^9.9.0", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-link": "^9.7.4", + "@fluentui/react-link": "^9.8.0", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", @@ -2825,9 +2828,9 @@ } }, "node_modules/@fluentui/react-button": { - "version": "9.8.2", - "resolved": "https://registry.npmjs.org/@fluentui/react-button/-/react-button-9.8.2.tgz", - "integrity": "sha512-T2xBn6s6DRNH17Y+kLO+uEOaRe89Q20WP1Rs6OzC45cSpOGc+q9ogbPbYBqU7Tr1fur+Xd8LRHxdQJ3j5ufbdw==", + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-button/-/react-button-9.9.0.tgz", + "integrity": "sha512-aH3aSjKyxIiNb9jJOUaaIq47w7jP5ESFSRzvMjcWOETvlWo4QgNqEOOsYqpcltM1OrQZ0sTy/isxppRcyMDlcQ==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", @@ -2849,9 +2852,9 @@ } }, "node_modules/@fluentui/react-card": { - "version": "9.5.11", - "resolved": "https://registry.npmjs.org/@fluentui/react-card/-/react-card-9.5.11.tgz", - "integrity": "sha512-0W3BmDER/aKx+7+ttGy+M6LO09DW7DkJlO8F0x13L1ssOVxJ0OhyhSGiCF0cJliOK1tiGPveYf6+X2xMq2MT6g==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-card/-/react-card-9.6.0.tgz", + "integrity": "sha512-vgBvhtSzQDa01aOP9zdhJXFLsZAiDVslRfX3HmlIo1pAMt8w+PBq+ypDp1wxM7HPFpj9+RYcERRKtf4MSNP9Nw==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", @@ -2872,20 +2875,20 @@ } }, "node_modules/@fluentui/react-carousel": { - "version": "9.9.4", - "resolved": "https://registry.npmjs.org/@fluentui/react-carousel/-/react-carousel-9.9.4.tgz", - "integrity": "sha512-mzGZUOe3tB+86/WPsQTgppYRoqeM1vl8LswISl7FVrxk7PREnzZLW4BEZnFOKuP29dThcjJNzF0mM/5kq1lKug==", + "version": "9.9.6", + "resolved": "https://registry.npmjs.org/@fluentui/react-carousel/-/react-carousel-9.9.6.tgz", + "integrity": "sha512-Ae7DKwQsidRBjUQeiXffRUi8i/26jMgJd24rDVLeQUvoUhs+z/SA9iZN/QMuNl02E291MAEruENKzzkshvfYfg==", "license": "MIT", "dependencies": { "@fluentui/react-aria": "^9.17.10", - "@fluentui/react-button": "^9.8.2", + "@fluentui/react-button": "^9.9.0", "@fluentui/react-context-selector": "^9.2.15", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", - "@fluentui/react-tooltip": "^9.9.3", + "@fluentui/react-tooltip": "^9.10.0", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1", @@ -2901,15 +2904,15 @@ } }, "node_modules/@fluentui/react-checkbox": { - "version": "9.5.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-checkbox/-/react-checkbox-9.5.15.tgz", - "integrity": "sha512-ZXvuZo8HvBLvsd74foI/p/YkxKRmruQLhleeQRMqyNKMbytFcYZ8rHmAN492tNMjmWxGIfZHv5Oh7Ds6poNmJg==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-checkbox/-/react-checkbox-9.6.0.tgz", + "integrity": "sha512-GMgB1Yx2WP6cISIZoRTyXp2VkJBR8t1+wRyY63RRcofL/ziqqBhz++kl317lbVv7QxnXZh6KlVuoPROWFDQuaw==", "license": "MIT", "dependencies": { - "@fluentui/react-field": "^9.4.15", + "@fluentui/react-field": "^9.5.0", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-label": "^9.3.15", + "@fluentui/react-label": "^9.4.0", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", @@ -2948,15 +2951,15 @@ } }, "node_modules/@fluentui/react-combobox": { - "version": "9.16.17", - "resolved": "https://registry.npmjs.org/@fluentui/react-combobox/-/react-combobox-9.16.17.tgz", - "integrity": "sha512-/Q2incmVrKF4sKqtrkEntGvjkuddr5mHfV9K5ziM+aR9ZczMwFuFVUFbBTcJlmtnsYf8CLm4E+r7oBWgXy2TVA==", + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-combobox/-/react-combobox-9.17.0.tgz", + "integrity": "sha512-04JTIrXCAbG8HnczFVzJsUJO+NJQ2d/JPynXlmTq7KCMw0BssiF//7IAPFnTiMYmS7jcwc9Uh4ZeFrw+czA79g==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-aria": "^9.17.10", "@fluentui/react-context-selector": "^9.2.15", - "@fluentui/react-field": "^9.4.15", + "@fluentui/react-field": "^9.5.0", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-portal": "^9.8.11", @@ -2976,69 +2979,69 @@ } }, "node_modules/@fluentui/react-components": { - "version": "9.73.2", - "resolved": "https://registry.npmjs.org/@fluentui/react-components/-/react-components-9.73.2.tgz", - "integrity": "sha512-PZ9y66NLgUowuaZs9U75WtaxPXUTvjSUf/PHYABSV1Hl4DPVRM3koCQCPPxQEPlPhzHnbNqAK//5WZjPlmxBdA==", + "version": "9.73.7", + "resolved": "https://registry.npmjs.org/@fluentui/react-components/-/react-components-9.73.7.tgz", + "integrity": "sha512-hLxXEAiiMEMmFR3jEYgFPOV5lnNzu6SJU0NtyMCn1Tf4HXgCfy4h700e+GzuAsL1RlQAYC35HplcZHcEffwTIQ==", "license": "MIT", "dependencies": { - "@fluentui/react-accordion": "^9.9.2", - "@fluentui/react-alert": "9.0.0-beta.135", + "@fluentui/react-accordion": "^9.10.0", + "@fluentui/react-alert": "9.0.0-beta.138", "@fluentui/react-aria": "^9.17.10", - "@fluentui/react-avatar": "^9.10.2", - "@fluentui/react-badge": "^9.4.15", - "@fluentui/react-breadcrumb": "^9.3.17", - "@fluentui/react-button": "^9.8.2", - "@fluentui/react-card": "^9.5.11", - "@fluentui/react-carousel": "^9.9.4", - "@fluentui/react-checkbox": "^9.5.15", + "@fluentui/react-avatar": "^9.11.0", + "@fluentui/react-badge": "^9.5.1", + "@fluentui/react-breadcrumb": "^9.4.0", + "@fluentui/react-button": "^9.9.0", + "@fluentui/react-card": "^9.6.0", + "@fluentui/react-carousel": "^9.9.6", + "@fluentui/react-checkbox": "^9.6.0", "@fluentui/react-color-picker": "^9.2.15", - "@fluentui/react-combobox": "^9.16.17", - "@fluentui/react-dialog": "^9.17.2", - "@fluentui/react-divider": "^9.6.2", - "@fluentui/react-drawer": "^9.11.5", - "@fluentui/react-field": "^9.4.15", - "@fluentui/react-image": "^9.3.15", - "@fluentui/react-infobutton": "9.0.0-beta.112", - "@fluentui/react-infolabel": "^9.4.17", - "@fluentui/react-input": "^9.7.15", - "@fluentui/react-label": "^9.3.15", - "@fluentui/react-link": "^9.7.4", - "@fluentui/react-list": "^9.6.10", - "@fluentui/react-menu": "^9.22.0", - "@fluentui/react-message-bar": "^9.6.20", - "@fluentui/react-motion": "^9.13.0", - "@fluentui/react-nav": "^9.3.20", + "@fluentui/react-combobox": "^9.17.0", + "@fluentui/react-dialog": "^9.17.3", + "@fluentui/react-divider": "^9.7.0", + "@fluentui/react-drawer": "^9.11.6", + "@fluentui/react-field": "^9.5.0", + "@fluentui/react-image": "^9.4.0", + "@fluentui/react-infobutton": "9.0.0-beta.114", + "@fluentui/react-infolabel": "^9.4.19", + "@fluentui/react-input": "^9.8.1", + "@fluentui/react-label": "^9.4.0", + "@fluentui/react-link": "^9.8.0", + "@fluentui/react-list": "^9.6.13", + "@fluentui/react-menu": "^9.24.0", + "@fluentui/react-message-bar": "^9.6.23", + "@fluentui/react-motion": "^9.14.0", + "@fluentui/react-nav": "^9.3.23", "@fluentui/react-overflow": "^9.7.1", - "@fluentui/react-persona": "^9.6.2", - "@fluentui/react-popover": "^9.14.0", + "@fluentui/react-persona": "^9.7.2", + "@fluentui/react-popover": "^9.14.1", "@fluentui/react-portal": "^9.8.11", "@fluentui/react-positioning": "^9.22.0", - "@fluentui/react-progress": "^9.4.15", + "@fluentui/react-progress": "^9.5.0", "@fluentui/react-provider": "^9.22.15", - "@fluentui/react-radio": "^9.5.15", - "@fluentui/react-rating": "^9.3.15", - "@fluentui/react-search": "^9.3.15", - "@fluentui/react-select": "^9.4.15", + "@fluentui/react-radio": "^9.6.1", + "@fluentui/react-rating": "^9.4.0", + "@fluentui/react-search": "^9.4.1", + "@fluentui/react-select": "^9.5.0", "@fluentui/react-shared-contexts": "^9.26.2", - "@fluentui/react-skeleton": "^9.4.15", - "@fluentui/react-slider": "^9.5.15", - "@fluentui/react-spinbutton": "^9.5.15", - "@fluentui/react-spinner": "^9.7.15", - "@fluentui/react-swatch-picker": "^9.4.15", - "@fluentui/react-switch": "^9.6.0", - "@fluentui/react-table": "^9.19.10", - "@fluentui/react-tabs": "^9.11.2", + "@fluentui/react-skeleton": "^9.7.1", + "@fluentui/react-slider": "^9.6.1", + "@fluentui/react-spinbutton": "^9.6.1", + "@fluentui/react-spinner": "^9.8.1", + "@fluentui/react-swatch-picker": "^9.5.1", + "@fluentui/react-switch": "^9.7.1", + "@fluentui/react-table": "^9.19.14", + "@fluentui/react-tabs": "^9.12.0", "@fluentui/react-tabster": "^9.26.13", - "@fluentui/react-tag-picker": "^9.8.1", - "@fluentui/react-tags": "^9.7.17", - "@fluentui/react-teaching-popover": "^9.6.18", + "@fluentui/react-tag-picker": "^9.8.5", + "@fluentui/react-tags": "^9.8.0", + "@fluentui/react-teaching-popover": "^9.6.20", "@fluentui/react-text": "^9.6.15", - "@fluentui/react-textarea": "^9.6.15", + "@fluentui/react-textarea": "^9.7.1", "@fluentui/react-theme": "^9.2.1", - "@fluentui/react-toast": "^9.7.14", - "@fluentui/react-toolbar": "^9.7.3", - "@fluentui/react-tooltip": "^9.9.3", - "@fluentui/react-tree": "^9.15.12", + "@fluentui/react-toast": "^9.7.16", + "@fluentui/react-toolbar": "^9.7.7", + "@fluentui/react-tooltip": "^9.10.0", + "@fluentui/react-tree": "^9.15.16", "@fluentui/react-utilities": "^9.26.2", "@fluentui/react-virtualizer": "9.0.0-alpha.111", "@griffel/react": "^1.5.32", @@ -3069,9 +3072,9 @@ } }, "node_modules/@fluentui/react-dialog": { - "version": "9.17.2", - "resolved": "https://registry.npmjs.org/@fluentui/react-dialog/-/react-dialog-9.17.2.tgz", - "integrity": "sha512-mZdKylSvh2fRf0e3wMX3ZNccb9DahsOE7A5Y9LG97ghYvndMBVG2YwScIzUFVvLS206ari6HMOl0lC5JRB1bKA==", + "version": "9.17.3", + "resolved": "https://registry.npmjs.org/@fluentui/react-dialog/-/react-dialog-9.17.3.tgz", + "integrity": "sha512-rF5l8n5yhaB//ZHns0my3Tviir7R8NVyRgTtvV2gLhG58YM7qpm54oraG83uwlXCcZp0wlg2LuIe1cZ559ex1A==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", @@ -3079,8 +3082,8 @@ "@fluentui/react-context-selector": "^9.2.15", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-motion": "^9.13.0", - "@fluentui/react-motion-components-preview": "^0.15.2", + "@fluentui/react-motion": "^9.14.0", + "@fluentui/react-motion-components-preview": "^0.15.3", "@fluentui/react-portal": "^9.8.11", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", @@ -3097,9 +3100,9 @@ } }, "node_modules/@fluentui/react-divider": { - "version": "9.6.2", - "resolved": "https://registry.npmjs.org/@fluentui/react-divider/-/react-divider-9.6.2.tgz", - "integrity": "sha512-jfHlpSoJys78STe/SSjqdcn+W7QjEO1xCGiedWp/MdTBi3pH5vEeYbt2u8RU+zP32IF0Clta85KsUEEG0DYELQ==", + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-divider/-/react-divider-9.7.0.tgz", + "integrity": "sha512-U8Nhrghjeh+XCGM4B7aHYosd6fXaxHC3MpZi7DB0xQ20ljn5cSTpBt4Yvl+tB9ld2+/eM8wekx1GVKyI4yWa3g==", "license": "MIT", "dependencies": { "@fluentui/react-jsx-runtime": "^9.4.1", @@ -3117,15 +3120,15 @@ } }, "node_modules/@fluentui/react-drawer": { - "version": "9.11.5", - "resolved": "https://registry.npmjs.org/@fluentui/react-drawer/-/react-drawer-9.11.5.tgz", - "integrity": "sha512-eoZY+jKZwbJo1PUsb7Ico7u/8aObHL4BhPP6hd+HHNzB7seTpN7rLd0DpASLZsxJUy5yvch4QF2TrjOu6V8kRA==", + "version": "9.11.6", + "resolved": "https://registry.npmjs.org/@fluentui/react-drawer/-/react-drawer-9.11.6.tgz", + "integrity": "sha512-E+k3eKVb/xKPm2RH5Q1xBjL89NeB1GXtYHO6qRlhQ9auYVTlaBCR7f/ZfIIJJ2x8MzfntQljyl94VARtmZYnyA==", "license": "MIT", "dependencies": { - "@fluentui/react-dialog": "^9.17.2", + "@fluentui/react-dialog": "^9.17.3", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-motion": "^9.13.0", - "@fluentui/react-motion-components-preview": "^0.15.2", + "@fluentui/react-motion": "^9.14.0", + "@fluentui/react-motion-components-preview": "^0.15.3", "@fluentui/react-portal": "^9.8.11", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", @@ -3142,15 +3145,15 @@ } }, "node_modules/@fluentui/react-field": { - "version": "9.4.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-field/-/react-field-9.4.15.tgz", - "integrity": "sha512-hKdl+ncnT1C3vX8zQ4LqNGUk6TiatDOAW49dr18RkONcScg2staAaDme977Iozj6+AW7AJsDfkNxq/lwHhe/pg==", + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-field/-/react-field-9.5.0.tgz", + "integrity": "sha512-yGjB9RXqKrolkkjyAsKVdrH2Xeinj+vromrSCJelgMJ3Q3D6YkExHQzgtdzqo0fVPppnEA4oDKL3Vqqnz/G5Ug==", "license": "MIT", "dependencies": { "@fluentui/react-context-selector": "^9.2.15", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-label": "^9.3.15", + "@fluentui/react-label": "^9.4.0", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", @@ -3199,12 +3202,12 @@ } }, "node_modules/@fluentui/react-icons": { - "version": "2.0.320", - "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-2.0.320.tgz", - "integrity": "sha512-NU4gErPeaTD/T6Z9g3Uvp898lIFS6fDLr3++vpT8pcI4Ds0fZqQdrwNi3dF0R/SVws8DXQaRYiGlPHxszo4J4g==", + "version": "2.0.324", + "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-2.0.324.tgz", + "integrity": "sha512-wbtIQWwoTWNU6KyuF59zZ1viFv1i68iwVa1+so/QnfNKNHIXa2MEZ375Vg/pcubFBqlTxsKMrCBFtHEIzBHG/Q==", "license": "MIT", "dependencies": { - "@griffel/react": "^1.0.0", + "@griffel/react": "^1.6.1", "tslib": "^2.1.0" }, "peerDependencies": { @@ -3212,9 +3215,9 @@ } }, "node_modules/@fluentui/react-image": { - "version": "9.3.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-image/-/react-image-9.3.15.tgz", - "integrity": "sha512-k8ftGUc5G3Hj5W9nOFnWEKZ1oXmoZE3EvAEdyI6Cn9R8E6zW2PZ1+cug0p6rr01JCDG8kbry1LAITcObMrlPdw==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-image/-/react-image-9.4.0.tgz", + "integrity": "sha512-BpcBlmkukm7YYf6PTCbAIMkeCXc8+7aq2eMADsxF5gFD8j3d5lBY3cKByOWRM1NvXcMXmqXr/hQP+ovqNAHzEA==", "license": "MIT", "dependencies": { "@fluentui/react-jsx-runtime": "^9.4.1", @@ -3232,15 +3235,15 @@ } }, "node_modules/@fluentui/react-infobutton": { - "version": "9.0.0-beta.112", - "resolved": "https://registry.npmjs.org/@fluentui/react-infobutton/-/react-infobutton-9.0.0-beta.112.tgz", - "integrity": "sha512-Fhqoc6b1MQtHW+Mm5sBhfa5ZrRdOV4azuUa5WyBvwD4Ozq/z2pBOC/wi/A/WCjKMnGoMlQ2CggoLaMhQmenzAQ==", + "version": "9.0.0-beta.114", + "resolved": "https://registry.npmjs.org/@fluentui/react-infobutton/-/react-infobutton-9.0.0-beta.114.tgz", + "integrity": "sha512-3mqnlIcRc0PuW7rsxLFjzqnI/IITZIrHRt8Zwcm8NX7XZIK3wfODb9ytmQDYU/5IfwiSXC+xozqhI6kttaE3iw==", "license": "MIT", "dependencies": { "@fluentui/react-icons": "^2.0.237", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-label": "^9.3.15", - "@fluentui/react-popover": "^9.14.0", + "@fluentui/react-label": "^9.4.0", + "@fluentui/react-popover": "^9.14.1", "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", @@ -3255,15 +3258,15 @@ } }, "node_modules/@fluentui/react-infolabel": { - "version": "9.4.17", - "resolved": "https://registry.npmjs.org/@fluentui/react-infolabel/-/react-infolabel-9.4.17.tgz", - "integrity": "sha512-zLw52jn2wAuEKWFzaNj3aKhuB4BAEI8LqblryCg0LKPKHcv/z9d9RllCqcVz+ngdK1tQGtCIPH/wxNlZXx/I3Q==", + "version": "9.4.19", + "resolved": "https://registry.npmjs.org/@fluentui/react-infolabel/-/react-infolabel-9.4.19.tgz", + "integrity": "sha512-b/3ETF5DPgHcRUcj85iGyiEXUFozFq+IY6tPcyCiUcmIoKScD8McFaHozjpaVqngLbCz0uKNNA0JDy1x/T2ItQ==", "license": "MIT", "dependencies": { "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-label": "^9.3.15", - "@fluentui/react-popover": "^9.14.0", + "@fluentui/react-label": "^9.4.0", + "@fluentui/react-popover": "^9.14.1", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", @@ -3279,12 +3282,12 @@ } }, "node_modules/@fluentui/react-input": { - "version": "9.7.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-input/-/react-input-9.7.15.tgz", - "integrity": "sha512-pzGF1mOenV03RhIy+km8GrqCfahDSLm6YG7wxpE1m2q2fY73cyLZPuMbK7Kz27oaoyUI37v4Pa4612zl12228A==", + "version": "9.8.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-input/-/react-input-9.8.1.tgz", + "integrity": "sha512-ZlMeYBf1EQg4alI5+9gfx3Icmq3xibPiIYeARtFzOKJ2XzpnD4d/yswx3IDkzXCbqw9rSHtHV03vEeYLUPPTGw==", "license": "MIT", "dependencies": { - "@fluentui/react-field": "^9.4.15", + "@fluentui/react-field": "^9.5.0", "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-theme": "^9.2.1", @@ -3314,9 +3317,9 @@ } }, "node_modules/@fluentui/react-label": { - "version": "9.3.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-label/-/react-label-9.3.15.tgz", - "integrity": "sha512-ycmaQwC4tavA8WeDfgcay1Ywu/4goHq1NOeVxkyzWTPGA7rs+tdCgdZBQZLAsBK2XFaZiHs7l+KG9r1oIRKolA==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-label/-/react-label-9.4.0.tgz", + "integrity": "sha512-joQ7YNz2dgwDd134sc7e8/vxfFKBUT5AdWx0apT0ohWKgh7RBjB3AdXsaJ8FaMKMNZIGTxZVsP4hHcGsWMTAFw==", "license": "MIT", "dependencies": { "@fluentui/react-jsx-runtime": "^9.4.1", @@ -3334,9 +3337,9 @@ } }, "node_modules/@fluentui/react-link": { - "version": "9.7.4", - "resolved": "https://registry.npmjs.org/@fluentui/react-link/-/react-link-9.7.4.tgz", - "integrity": "sha512-ILKFpo/QH1SRsLN9gopAyZT/b/xsGcdO4JxthEeuTRvpLD6gImvRplum8ySIlbTskVVzog6038bHUSYLMdN7OA==", + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-link/-/react-link-9.8.0.tgz", + "integrity": "sha512-TH5LS4iuQ4jYzlR84A4n7lQTKaJuiuuGFHMIxoEqtKeMoL9F5AiabuBs6m7Q7clSdTrrcRMNzXLuEFarQrzGTQ==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", @@ -3356,13 +3359,13 @@ } }, "node_modules/@fluentui/react-list": { - "version": "9.6.10", - "resolved": "https://registry.npmjs.org/@fluentui/react-list/-/react-list-9.6.10.tgz", - "integrity": "sha512-NTAWYL8Z4h9N9N1b39H9xqfTyhfGkhlNTc3higpoIS/6jgEf6GMNF8iwvAyhB++hFdjBd27c+NbDl4MCwHhGiA==", + "version": "9.6.13", + "resolved": "https://registry.npmjs.org/@fluentui/react-list/-/react-list-9.6.13.tgz", + "integrity": "sha512-MIP0XKxU68m8VsBCyNBame46nnZ94FCNUArw9T2JuumyKMgV07C+sNhXCe9BCVpUr8e2Hfofo7CZjAsXWDZ0nw==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-checkbox": "^9.5.15", + "@fluentui/react-checkbox": "^9.6.0", "@fluentui/react-context-selector": "^9.2.15", "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-shared-contexts": "^9.26.2", @@ -3380,9 +3383,9 @@ } }, "node_modules/@fluentui/react-menu": { - "version": "9.22.0", - "resolved": "https://registry.npmjs.org/@fluentui/react-menu/-/react-menu-9.22.0.tgz", - "integrity": "sha512-RPZvqHsxMDEArsz80mJabs1fVGPlCrhMntzM/wt3Bga+fyPv4yEuDdN5FB8JqUpIAjRZneiW0RLC0Mr3WqmatA==", + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-menu/-/react-menu-9.24.0.tgz", + "integrity": "sha512-HqIwEM6lPropSHUnbPFufLYdkAIVca87XbNQHCTes4QSLeaF4oEjlBH60rIqQ52k78FwZuUFIciWkSChxJ9ekg==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", @@ -3390,8 +3393,8 @@ "@fluentui/react-context-selector": "^9.2.15", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-motion": "^9.13.0", - "@fluentui/react-motion-components-preview": "^0.15.2", + "@fluentui/react-motion": "^9.14.0", + "@fluentui/react-motion-components-preview": "^0.15.3", "@fluentui/react-portal": "^9.8.11", "@fluentui/react-positioning": "^9.22.0", "@fluentui/react-shared-contexts": "^9.26.2", @@ -3409,17 +3412,17 @@ } }, "node_modules/@fluentui/react-message-bar": { - "version": "9.6.20", - "resolved": "https://registry.npmjs.org/@fluentui/react-message-bar/-/react-message-bar-9.6.20.tgz", - "integrity": "sha512-d0u+ZPYhAvm+dQSyLECR0vk4Q5UwomI/3azNWduthqU9eQXrgaTDmJkJIeF/bu0jOci3AaMwImbmZqNMSQBmGQ==", + "version": "9.6.23", + "resolved": "https://registry.npmjs.org/@fluentui/react-message-bar/-/react-message-bar-9.6.23.tgz", + "integrity": "sha512-mGnFmYWx6tq36OMTdVtJmxyn3j0p+Shll3+w4W2fW8fcOVSeyrnZ++HLmpurUkVzwI2xR2lL842kxC3GtbwmNw==", "license": "MIT", "dependencies": { - "@fluentui/react-button": "^9.8.2", + "@fluentui/react-button": "^9.9.0", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-link": "^9.7.4", - "@fluentui/react-motion": "^9.13.0", - "@fluentui/react-motion-components-preview": "^0.15.2", + "@fluentui/react-link": "^9.8.0", + "@fluentui/react-motion": "^9.14.0", + "@fluentui/react-motion-components-preview": "^0.15.3", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", @@ -3434,9 +3437,9 @@ } }, "node_modules/@fluentui/react-motion": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@fluentui/react-motion/-/react-motion-9.13.0.tgz", - "integrity": "sha512-YdOpW6e7qfvzoWKcqh8hReCqwYEoiEmNBcCprGaupKjWOi9jBbF/JESM1AHI9nOjPd8aY90WUG2+ahvrqfL9LA==", + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-motion/-/react-motion-9.14.0.tgz", + "integrity": "sha512-gOy8+fUP1KQRM/J6mRhioCMmUrHW9jbLF0DZ9T8nKPQsLrLaSXHxnnI8DcKZjlYc2fKuZitBnbpximgff6HajQ==", "license": "MIT", "dependencies": { "@fluentui/react-shared-contexts": "^9.26.2", @@ -3451,9 +3454,9 @@ } }, "node_modules/@fluentui/react-motion-components-preview": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@fluentui/react-motion-components-preview/-/react-motion-components-preview-0.15.2.tgz", - "integrity": "sha512-KqHRV8lLmVwOWiHBdpUFA+TwMbuYu9cyzNvmhbMFLVKzZyr3MPgN+97Tf+6QYPf22o99SMT0BPySDv/HiNYanA==", + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/@fluentui/react-motion-components-preview/-/react-motion-components-preview-0.15.3.tgz", + "integrity": "sha512-dUH2+GmEWX9q2ojx70VfFLRqzA9fR4YISC6daXkz3iPx4PtesTDn7jwsuXXquaAhltJeBptJ8+K4jbtBrwCMYQ==", "license": "MIT", "dependencies": { "@fluentui/react-motion": "*", @@ -3468,24 +3471,24 @@ } }, "node_modules/@fluentui/react-nav": { - "version": "9.3.20", - "resolved": "https://registry.npmjs.org/@fluentui/react-nav/-/react-nav-9.3.20.tgz", - "integrity": "sha512-YIObOcR92Nz4OUePrDhRdLQ5m9ph0y+U7U9NYgE/XFrLtWl+uqUS7u36m3NJl9QGgZVpUHO4nbNjizGLkncCCA==", + "version": "9.3.23", + "resolved": "https://registry.npmjs.org/@fluentui/react-nav/-/react-nav-9.3.23.tgz", + "integrity": "sha512-Z9hA70n5i62sO9IJItkX5+v1F7Lo/539joPaHCLHHca+rySQQZKqy8zLRIfLbh/qF8Nm04ywY19Qt14XjI59cQ==", "license": "MIT", "dependencies": { "@fluentui/react-aria": "^9.17.10", - "@fluentui/react-button": "^9.8.2", + "@fluentui/react-button": "^9.9.0", "@fluentui/react-context-selector": "^9.2.15", - "@fluentui/react-divider": "^9.6.2", - "@fluentui/react-drawer": "^9.11.5", + "@fluentui/react-divider": "^9.7.0", + "@fluentui/react-drawer": "^9.11.6", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-motion": "^9.13.0", - "@fluentui/react-motion-components-preview": "^0.15.2", + "@fluentui/react-motion": "^9.14.0", + "@fluentui/react-motion-components-preview": "^0.15.3", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", - "@fluentui/react-tooltip": "^9.9.3", + "@fluentui/react-tooltip": "^9.10.0", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" @@ -3518,13 +3521,13 @@ } }, "node_modules/@fluentui/react-persona": { - "version": "9.6.2", - "resolved": "https://registry.npmjs.org/@fluentui/react-persona/-/react-persona-9.6.2.tgz", - "integrity": "sha512-60kOmljlYjUiySWDN1bZh1FB4C7jbJS2dobtBJQh5agnKg34p3egO+6MwsBHRcwaGhVMh4T8XcbE6t2hw+iqyQ==", + "version": "9.7.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-persona/-/react-persona-9.7.2.tgz", + "integrity": "sha512-u6buhC6Haf8YewBnZAzi49YCwiC8vt0O0YPADemk+4uJ8bhCnayzLxMYGuQ95XO4HFhvVnSPEYjMDdKrMO1hIw==", "license": "MIT", "dependencies": { - "@fluentui/react-avatar": "^9.10.2", - "@fluentui/react-badge": "^9.4.15", + "@fluentui/react-avatar": "^9.11.0", + "@fluentui/react-badge": "^9.5.1", "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-theme": "^9.2.1", @@ -3540,17 +3543,17 @@ } }, "node_modules/@fluentui/react-popover": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/@fluentui/react-popover/-/react-popover-9.14.0.tgz", - "integrity": "sha512-XrZlSfSYhA12j5bna4Sq8N/If2vul7gl8woVrN8U3iQUjdaHB6OAMZ/WMNUdMm35Z+4e4rHClAZxU2dUsbHrmw==", + "version": "9.14.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-popover/-/react-popover-9.14.1.tgz", + "integrity": "sha512-EODa5yWSfDLPDurjWoZXfkf2ccnbQQbk3s1XYRzxA6RDfdVqUI5W64RJzHWBiNhOLzQEhd6Qb4e6Mshj4FSbdQ==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-aria": "^9.17.10", "@fluentui/react-context-selector": "^9.2.15", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-motion": "^9.13.0", - "@fluentui/react-motion-components-preview": "^0.15.2", + "@fluentui/react-motion": "^9.14.0", + "@fluentui/react-motion-components-preview": "^0.15.3", "@fluentui/react-portal": "^9.8.11", "@fluentui/react-positioning": "^9.22.0", "@fluentui/react-shared-contexts": "^9.26.2", @@ -3622,13 +3625,14 @@ } }, "node_modules/@fluentui/react-progress": { - "version": "9.4.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-progress/-/react-progress-9.4.15.tgz", - "integrity": "sha512-U2dqtEtov7FoeIGSAEqdFV2O2pjx3gFzbCWpPkpuLCshOSGjCPPeLV3iiTGP1WFrGCcpwFoz5O2YmsnA3wf4oQ==", + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-progress/-/react-progress-9.5.0.tgz", + "integrity": "sha512-VcWXI6UJfBkrDuC/e9oR4YBlpnLUE+FqRRjMG4mVXV+AJzFiljF3mQkFAj94G6dsr54TcoDXC6oydgXLCOTW2A==", "license": "MIT", "dependencies": { - "@fluentui/react-field": "^9.4.15", + "@fluentui/react-field": "^9.5.0", "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-motion": "^9.14.0", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", @@ -3666,14 +3670,14 @@ } }, "node_modules/@fluentui/react-radio": { - "version": "9.5.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-radio/-/react-radio-9.5.15.tgz", - "integrity": "sha512-47Zhe1Ec02QXczoPNLTFwcvCQFGoXInEiXhsQYF0tD+XAX6Q675j/z6gsIItc8V+avvD0IITsDPpqQ09wfNYkQ==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-radio/-/react-radio-9.6.1.tgz", + "integrity": "sha512-QBoV6l8fVLP+H9Tigq/Y6boiEqMDRhhVMkIfUiWFbnsU/Uc7J5fxW8GoNqzMmoOmC7yvQ/g4jsoTQF27+PzK5w==", "license": "MIT", "dependencies": { - "@fluentui/react-field": "^9.4.15", + "@fluentui/react-field": "^9.5.0", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-label": "^9.3.15", + "@fluentui/react-label": "^9.4.0", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", @@ -3689,9 +3693,9 @@ } }, "node_modules/@fluentui/react-rating": { - "version": "9.3.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-rating/-/react-rating-9.3.15.tgz", - "integrity": "sha512-MH/Jgoco8p+haf1d5Gi+d5VCjwd0qE6y/uP0YJsB9m11+DFnDxgKhzJKIiIzs3yzB2M4bMM8z9SqEHzQGCQEPg==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-rating/-/react-rating-9.4.0.tgz", + "integrity": "sha512-qVesFNgQ7uuX8z9d8xqxIXn5ax06xffgBr/eAuZfqVYZG5aRrPHHRoiWf0HDrYD4Lb/HRBLPtbNihNxhXj/LEA==", "license": "MIT", "dependencies": { "@fluentui/react-icons": "^2.0.245", @@ -3711,13 +3715,13 @@ } }, "node_modules/@fluentui/react-search": { - "version": "9.3.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-search/-/react-search-9.3.15.tgz", - "integrity": "sha512-xm9YveJM4aXAn/XjG3GMHpXxLO53Nz2mmuJpc80WXaYqQwesGSS0YfMSTbjM04RkvMsjmQM/dwWcudV9JQ0//g==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-search/-/react-search-9.4.1.tgz", + "integrity": "sha512-Lv2zhPad7SDhMd5NeabXluw4y0Gov9YxDkJhjShMnkiN3yCOA5tlVviNvRXOXxy0gS//d8CiGJ5mBT1bzz2Rrw==", "license": "MIT", "dependencies": { "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-input": "^9.7.15", + "@fluentui/react-input": "^9.8.1", "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-theme": "^9.2.1", @@ -3733,12 +3737,12 @@ } }, "node_modules/@fluentui/react-select": { - "version": "9.4.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-select/-/react-select-9.4.15.tgz", - "integrity": "sha512-NWoDzf3H7mu8fXBCR3YIlumMb7lDElsbmcCSIlUz70n2cPTNXcNEQm4ERWiGAmxf8xoAfgfDWc5rYnRWAFi2fA==", + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-select/-/react-select-9.5.0.tgz", + "integrity": "sha512-pGOD6MBwQsiHKkEdNmVrTavcfC9pOjt4nz/DRlFD444j6iR1PALlus5cNOp7A0JOnGDDvW+1afIvgySCqN0oiA==", "license": "MIT", "dependencies": { - "@fluentui/react-field": "^9.4.15", + "@fluentui/react-field": "^9.5.0", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-shared-contexts": "^9.26.2", @@ -3769,12 +3773,12 @@ } }, "node_modules/@fluentui/react-skeleton": { - "version": "9.4.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-skeleton/-/react-skeleton-9.4.15.tgz", - "integrity": "sha512-QUVxZ5pYbIprCY1G5sJYDGvuvM1TNFl3vPkME8r/nD7pKXwxaZYJoob2L0DQ9OdnOeHgO8yTOgOgZEU+Km89dA==", + "version": "9.7.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-skeleton/-/react-skeleton-9.7.1.tgz", + "integrity": "sha512-9WniFEe6gbhkZuBurpQNFmMMhP/Ox84Xm9/iu6q8OmnRkFCyZrEuCFlWGDffnBREKIJqE0VJn5ZrUYWMMh45KA==", "license": "MIT", "dependencies": { - "@fluentui/react-field": "^9.4.15", + "@fluentui/react-field": "^9.5.0", "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-theme": "^9.2.1", @@ -3790,12 +3794,12 @@ } }, "node_modules/@fluentui/react-slider": { - "version": "9.5.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-slider/-/react-slider-9.5.15.tgz", - "integrity": "sha512-lFDkyYYAUUGwbg1UJqjsuQ2tQUBFjxzv2Bpyr1StyAoS91q8skTUDyZxamJTJ0K6Ox/nhkfg+Wzz2aVg9kkF4Q==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-slider/-/react-slider-9.6.1.tgz", + "integrity": "sha512-ytF1gOEho8DrI817H8WCBsck1RXOlW7JRXYtu9VwH3SnDRM2Jz1CNxbou80+BpvyR1KKkvCc/JSgREgUAnkRAQ==", "license": "MIT", "dependencies": { - "@fluentui/react-field": "^9.4.15", + "@fluentui/react-field": "^9.5.0", "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", @@ -3812,13 +3816,13 @@ } }, "node_modules/@fluentui/react-spinbutton": { - "version": "9.5.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-spinbutton/-/react-spinbutton-9.5.15.tgz", - "integrity": "sha512-0NNfaXm8TJWHlillg6FPgJ1Ph7iO9ez+Gz4TSFYm1u+zF8RNsSGoplCf40U6gcKX8GkAHBwQ5vBZUbBK7syDng==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-spinbutton/-/react-spinbutton-9.6.1.tgz", + "integrity": "sha512-szqGlEfeJYkBzszEWBjj7ux522ckw9YtKAH0CS0Npd0xcY1GFkdywPwJMOoRUhsO08BOhv6P70Wlx0eYqURgIA==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-field": "^9.4.15", + "@fluentui/react-field": "^9.5.0", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-shared-contexts": "^9.26.2", @@ -3835,13 +3839,13 @@ } }, "node_modules/@fluentui/react-spinner": { - "version": "9.7.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-spinner/-/react-spinner-9.7.15.tgz", - "integrity": "sha512-ZMJ7y08yvVXL9HuiMLLCy1cRn8plR9A4mL57CM2/otaXVWQbOwRaFD0/+Dx3u9A8sEtdYLo6O9gJIjU8fZGaYw==", + "version": "9.8.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-spinner/-/react-spinner-9.8.1.tgz", + "integrity": "sha512-vSM5FwjASEor8NBOJx/1MLp8VCw7+pOJqZSvMn29LrUmMbgSZ6CifZFx0GfiX+1fM0EZ2/pqJzFFHpoQQubAyw==", "license": "MIT", "dependencies": { "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-label": "^9.3.15", + "@fluentui/react-label": "^9.4.0", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", @@ -3856,13 +3860,13 @@ } }, "node_modules/@fluentui/react-swatch-picker": { - "version": "9.4.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-swatch-picker/-/react-swatch-picker-9.4.15.tgz", - "integrity": "sha512-jeYSEDwLbQAW/UoTP15EZpVm2Z+UpPSjkgJaKk73UxX1+rD/JIzpxrN3FfEfkn3/uTZUQkd/SE4NQrilu1OMZQ==", + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-swatch-picker/-/react-swatch-picker-9.5.1.tgz", + "integrity": "sha512-7rs4dgnFMV2m/2A1tkevrVfThVEJs9crnVWCiSE4XADb9hFp7mqVyN8dKbQCJJMXODLF/Bc90nTCtLV8WaEj4Q==", "license": "MIT", "dependencies": { "@fluentui/react-context-selector": "^9.2.15", - "@fluentui/react-field": "^9.4.15", + "@fluentui/react-field": "^9.5.0", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-shared-contexts": "^9.26.2", @@ -3880,15 +3884,15 @@ } }, "node_modules/@fluentui/react-switch": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/@fluentui/react-switch/-/react-switch-9.6.0.tgz", - "integrity": "sha512-fqFj7PPSeGKIKI6OZ8JTwGKf5TSDZDhoUmXig03kUloX1w+rsGih92oUanZgnucEreIbkNwcgAKijRNbb1P0JQ==", + "version": "9.7.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-switch/-/react-switch-9.7.1.tgz", + "integrity": "sha512-61zJhxG9UBcZ+5T/Dk9yzOJDCOc2ZMZef/ImgIMB4lVsyWs/3n/ec/PKPwjp9SNz2FhQvayhMytEbGzri00jGw==", "license": "MIT", "dependencies": { - "@fluentui/react-field": "^9.4.15", + "@fluentui/react-field": "^9.5.0", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-label": "^9.3.15", + "@fluentui/react-label": "^9.4.0", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", @@ -3904,19 +3908,19 @@ } }, "node_modules/@fluentui/react-table": { - "version": "9.19.10", - "resolved": "https://registry.npmjs.org/@fluentui/react-table/-/react-table-9.19.10.tgz", - "integrity": "sha512-FFMSgUlUsicVZxCoLoNvOMdpANIKa0Ys4bhiNhlObsayLPFLwKrM9aL1eOg5RZPE+NUIQ8DJSrFcws1zzo6Jpg==", + "version": "9.19.14", + "resolved": "https://registry.npmjs.org/@fluentui/react-table/-/react-table-9.19.14.tgz", + "integrity": "sha512-IZ3tDqlQDC+R6nzX4thU8A7Aw3BMhbBZ5tgMOHnW733Xfton7wqKiumjsGJBnef3I48mqnBHJZQEzWBgzLsdqg==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-aria": "^9.17.10", - "@fluentui/react-avatar": "^9.10.2", - "@fluentui/react-checkbox": "^9.5.15", + "@fluentui/react-avatar": "^9.11.0", + "@fluentui/react-checkbox": "^9.6.0", "@fluentui/react-context-selector": "^9.2.15", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-radio": "^9.5.15", + "@fluentui/react-radio": "^9.6.1", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", @@ -3932,9 +3936,9 @@ } }, "node_modules/@fluentui/react-tabs": { - "version": "9.11.2", - "resolved": "https://registry.npmjs.org/@fluentui/react-tabs/-/react-tabs-9.11.2.tgz", - "integrity": "sha512-zmWzySlPM9EwHJNW0/JhyxBCqBvmfZIj1OZLdRDpbPDsKjhO0aGZV6WjLHFYJmq58kbN0wHKUbxc7LfafHHUwA==", + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-tabs/-/react-tabs-9.12.0.tgz", + "integrity": "sha512-gKCi1XNDYRvF6R5wETeQptzQRVBlM7VETaQHS/ue1x7+Vo42MbWMtYOmvqeg5CPjqy2hAwch0IA9bzWEQAm2ZA==", "license": "MIT", "dependencies": { "@fluentui/react-context-selector": "^9.2.15", @@ -3975,23 +3979,23 @@ } }, "node_modules/@fluentui/react-tag-picker": { - "version": "9.8.1", - "resolved": "https://registry.npmjs.org/@fluentui/react-tag-picker/-/react-tag-picker-9.8.1.tgz", - "integrity": "sha512-DDCh4rrY6wcIjOCsSBCtC3d1zX9KgCLAIP7kGpd+LNYfaIc9AU/nUZIRSF1L/zTDqaODf0n60ba/lB5RufxdNA==", + "version": "9.8.5", + "resolved": "https://registry.npmjs.org/@fluentui/react-tag-picker/-/react-tag-picker-9.8.5.tgz", + "integrity": "sha512-uhZUWDdg7zmQNjb1/5YI3l6agSDg/yFFaYZDH4eQDOmKIm35jAT2GmEMZVomZZVW/dDhZpezfMWZA5r442cZYQ==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-aria": "^9.17.10", - "@fluentui/react-combobox": "^9.16.17", + "@fluentui/react-combobox": "^9.17.0", "@fluentui/react-context-selector": "^9.2.15", - "@fluentui/react-field": "^9.4.15", + "@fluentui/react-field": "^9.5.0", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-portal": "^9.8.11", "@fluentui/react-positioning": "^9.22.0", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", - "@fluentui/react-tags": "^9.7.17", + "@fluentui/react-tags": "^9.8.0", "@fluentui/react-theme": "^9.2.1", "@fluentui/react-utilities": "^9.26.2", "@griffel/react": "^1.5.32", @@ -4005,14 +4009,14 @@ } }, "node_modules/@fluentui/react-tags": { - "version": "9.7.17", - "resolved": "https://registry.npmjs.org/@fluentui/react-tags/-/react-tags-9.7.17.tgz", - "integrity": "sha512-LCJJqoXIiN+aNqFHC/5nddsQJqh56xzrywwpMbMrQYI/dbIk5UYlmZ6arIPhQ9HVKat3YzGKAvOGlhFhEHIwDg==", + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-tags/-/react-tags-9.8.0.tgz", + "integrity": "sha512-O/Kf8pFgS0/eguzDCPm8FmrPG64dU36xTI1uYKwgF6iVOpmWFjk+7aPQtkoFHQzVwl1iLUL4mQFSutR4A8s38Q==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-aria": "^9.17.10", - "@fluentui/react-avatar": "^9.10.2", + "@fluentui/react-avatar": "^9.11.0", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-shared-contexts": "^9.26.2", @@ -4030,17 +4034,17 @@ } }, "node_modules/@fluentui/react-teaching-popover": { - "version": "9.6.18", - "resolved": "https://registry.npmjs.org/@fluentui/react-teaching-popover/-/react-teaching-popover-9.6.18.tgz", - "integrity": "sha512-cf76vSRZs40geZEw/RChfQvu6ioMyFKR0qvPc52QstPDC/cgGkOg+45G7SZo11IpYwBdkpUVWasnWUWSxTMiHw==", + "version": "9.6.20", + "resolved": "https://registry.npmjs.org/@fluentui/react-teaching-popover/-/react-teaching-popover-9.6.20.tgz", + "integrity": "sha512-XB/SJXdJabulcDBp6z4NNSFOcAnaOoIUZdmzqpx09UxtQwU/eFnYvZw/k1SI8Nc7IpHBgjzId8gHy6jvaN8JHw==", "license": "MIT", "dependencies": { "@fluentui/react-aria": "^9.17.10", - "@fluentui/react-button": "^9.8.2", + "@fluentui/react-button": "^9.9.0", "@fluentui/react-context-selector": "^9.2.15", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-popover": "^9.14.0", + "@fluentui/react-popover": "^9.14.1", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", @@ -4077,12 +4081,12 @@ } }, "node_modules/@fluentui/react-textarea": { - "version": "9.6.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-textarea/-/react-textarea-9.6.15.tgz", - "integrity": "sha512-yGYW3d+t21qJXlVsbAHz07RR/YxVw5b56483nFAbqGP3RpPG8ert8q9Ci2mldI9LpjYTG5deXUHqfcVGJ7qDAg==", + "version": "9.7.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-textarea/-/react-textarea-9.7.1.tgz", + "integrity": "sha512-YG0j202PRLDLZZDn8QQgREd4Ery2fDYMYb2HUvFdfo6MuSXMvv0RCKEUBCgajIXsHwT31Hsg5+xzM40X4jlOBg==", "license": "MIT", "dependencies": { - "@fluentui/react-field": "^9.4.15", + "@fluentui/react-field": "^9.5.0", "@fluentui/react-jsx-runtime": "^9.4.1", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-theme": "^9.2.1", @@ -4108,17 +4112,17 @@ } }, "node_modules/@fluentui/react-toast": { - "version": "9.7.14", - "resolved": "https://registry.npmjs.org/@fluentui/react-toast/-/react-toast-9.7.14.tgz", - "integrity": "sha512-Hzdzq/3hBPSZUYAStDRQ1bP1fwCZnOOik4YyPFGsVvgS60SWgcgHtRlvYgmFVd29dOHOU6J8A9VPbCwiWqf56A==", + "version": "9.7.16", + "resolved": "https://registry.npmjs.org/@fluentui/react-toast/-/react-toast-9.7.16.tgz", + "integrity": "sha512-Yq4yJboYqtdL5pNJBIYlSdT/kR6m449O95taJCh/msXJyRgqQZ46EmpTcwsxu3D55LTHbqI6Vxu+AikDYH1W7w==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-aria": "^9.17.10", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-motion": "^9.13.0", - "@fluentui/react-motion-components-preview": "^0.15.2", + "@fluentui/react-motion": "^9.14.0", + "@fluentui/react-motion-components-preview": "^0.15.3", "@fluentui/react-portal": "^9.8.11", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", @@ -4135,16 +4139,16 @@ } }, "node_modules/@fluentui/react-toolbar": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@fluentui/react-toolbar/-/react-toolbar-9.7.3.tgz", - "integrity": "sha512-h9mXLrQ55SFd2YXJXQOtpC+MJ3SckyGB5lWqFkQxqExFZkkeCL1u1bRf2/YFjNj8gbivVMwKmozzWeccexPeyQ==", + "version": "9.7.7", + "resolved": "https://registry.npmjs.org/@fluentui/react-toolbar/-/react-toolbar-9.7.7.tgz", + "integrity": "sha512-49nrRvGqJfdXhwaKZfNIcTiZSqTbThNG8uCa0FvJ88cO11PRPGcr5s6u3plUVxDXUKXpZJ7PKr/TTA0MvP7yIg==", "license": "MIT", "dependencies": { - "@fluentui/react-button": "^9.8.2", + "@fluentui/react-button": "^9.9.0", "@fluentui/react-context-selector": "^9.2.15", - "@fluentui/react-divider": "^9.6.2", + "@fluentui/react-divider": "^9.7.0", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-radio": "^9.5.15", + "@fluentui/react-radio": "^9.6.1", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", @@ -4160,9 +4164,9 @@ } }, "node_modules/@fluentui/react-tooltip": { - "version": "9.9.3", - "resolved": "https://registry.npmjs.org/@fluentui/react-tooltip/-/react-tooltip-9.9.3.tgz", - "integrity": "sha512-a351JFoaBAOn0SnQ76tzuNv2ieHzAS+VO8Ncy4m9/emrIs5lvBBfKX8fvA4/efVxY+683XEQdoL1LuApuJuTWw==", + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-tooltip/-/react-tooltip-9.10.0.tgz", + "integrity": "sha512-+aM0S1mcXy8XKKWgU3TocqTxHjcai7fHns3KwONLJPTp3jXTjyqEoj/o4XX1ka2IM3gdOFfyUU0Gfvw708dn9w==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", @@ -4184,22 +4188,22 @@ } }, "node_modules/@fluentui/react-tree": { - "version": "9.15.12", - "resolved": "https://registry.npmjs.org/@fluentui/react-tree/-/react-tree-9.15.12.tgz", - "integrity": "sha512-xppRZ5lljdlrBS/FrTgxM7JHsbyjJ6PNK7kQvkFLUa6cSNac2nzbLExIDs9TAZZe+wNkAiJiX5RZY/9Sb87NJQ==", + "version": "9.15.16", + "resolved": "https://registry.npmjs.org/@fluentui/react-tree/-/react-tree-9.15.16.tgz", + "integrity": "sha512-WP4WjbF/UWCp0JKaZsMFtah/kXu+mxqN8/kghppRYfVHWzLiMgFAPB/OzrGejLNwx+ai3t2dHOIHxXHnR1jYHA==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-aria": "^9.17.10", - "@fluentui/react-avatar": "^9.10.2", - "@fluentui/react-button": "^9.8.2", - "@fluentui/react-checkbox": "^9.5.15", + "@fluentui/react-avatar": "^9.11.0", + "@fluentui/react-button": "^9.9.0", + "@fluentui/react-checkbox": "^9.6.0", "@fluentui/react-context-selector": "^9.2.15", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.4.1", - "@fluentui/react-motion": "^9.13.0", - "@fluentui/react-motion-components-preview": "^0.15.2", - "@fluentui/react-radio": "^9.5.15", + "@fluentui/react-motion": "^9.14.0", + "@fluentui/react-motion-components-preview": "^0.15.3", + "@fluentui/react-radio": "^9.6.1", "@fluentui/react-shared-contexts": "^9.26.2", "@fluentui/react-tabster": "^9.26.13", "@fluentui/react-theme": "^9.2.1", @@ -4328,13 +4332,13 @@ } }, "node_modules/@griffel/core": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/@griffel/core/-/core-1.20.0.tgz", - "integrity": "sha512-pTLh3ixLu9ND9+M8FjMb8vpgM/1ws56Haj6WUSKWCWOxGU6umexSqZ57ueEYHZHA6ch6G0jt2pot4AL6GPZsUg==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/@griffel/core/-/core-1.20.1.tgz", + "integrity": "sha512-ld1mX04zpmeHn8agx4slSEh8kJ+8or3Y0x9gsJNKSKn6GdCkZBSiGUh+oBXCBn8RKzz8l60TA9IhVSStnyKekA==", "license": "MIT", "dependencies": { "@emotion/hash": "^0.9.0", - "@griffel/style-types": "^1.3.0", + "@griffel/style-types": "^1.4.0", "csstype": "^3.1.3", "rtl-css-js": "^1.16.1", "stylis": "^4.2.0", @@ -4342,12 +4346,12 @@ } }, "node_modules/@griffel/react": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@griffel/react/-/react-1.6.0.tgz", - "integrity": "sha512-IVt6l2Vte1u4+Dtwlv1KtntLWNquYK0eCRgctG/e14E2P7HVf7ZRUFIUiC58md2uPKGToDmGwiU4YXC4gatNbw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@griffel/react/-/react-1.6.1.tgz", + "integrity": "sha512-mNM4/+dIXzqeHboWpVZ1/jiwTAYNc5/8y/V/HasnQ2QXnV6gSUYpeUk/0n6IFU3NJmVJly9JrLSfNo0hM/IFeA==", "license": "MIT", "dependencies": { - "@griffel/core": "^1.20.0", + "@griffel/core": "^1.20.1", "tslib": "^2.1.0" }, "peerDependencies": { @@ -4355,9 +4359,9 @@ } }, "node_modules/@griffel/style-types": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@griffel/style-types/-/style-types-1.3.0.tgz", - "integrity": "sha512-bHwD3sUE84Xwv4dH011gOKe1jul77M1S6ZFN9Tnq8pvZ48UMdY//vtES6fv7GRS5wXYT4iqxQPBluAiYAfkpmw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@griffel/style-types/-/style-types-1.4.0.tgz", + "integrity": "sha512-vNDfOGV7RN/XkA7vxgf7Z5HgW8eiBm5cHT9wQPhsKB4pxWom5u6eQ9CkYE5mCCTSPl9H6Nd1NBai04d4P6BD7Q==", "license": "MIT", "dependencies": { "csstype": "^3.1.3" @@ -4526,18 +4530,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/@jest/console/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/@jest/core": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.5.1.tgz", @@ -4671,22 +4663,10 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/@jest/core/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/@jest/diff-sequences": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", - "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", "license": "MIT", "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -4746,9 +4726,9 @@ } }, "node_modules/@jest/expect-utils": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", - "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", + "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0" @@ -4864,18 +4844,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/@jest/fake-timers/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/@jest/get-type": { "version": "30.1.0", "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", @@ -5103,18 +5071,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/@jest/reporters/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/@jest/reporters/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -5312,18 +5268,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/@jest/transform/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/@jest/transform/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -5334,9 +5278,9 @@ } }, "node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", "license": "MIT", "dependencies": { "@jest/pattern": "30.0.1", @@ -5538,6 +5482,42 @@ } } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -5617,18 +5597,6 @@ "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", "license": "MIT" }, - "node_modules/@rollup/pluginutils/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.53.3", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", @@ -5655,9 +5623,9 @@ "license": "MIT" }, "node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", "license": "MIT" }, "node_modules/@sinonjs/commons": { @@ -5678,6 +5646,18 @@ "@sinonjs/commons": "^1.7.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -5912,9 +5892,9 @@ } }, "node_modules/@swc/helpers": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz", - "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", + "version": "0.5.20", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.20.tgz", + "integrity": "sha512-2egEBHUMasdypIzrprsu8g+OEVd7Vp2MM3a2eVlM/cyFYto0nGz5BX5BTgh/ShZZI9ed+ozEq+Ngt+rgmUs8tw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" @@ -5925,7 +5905,6 @@ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -6018,8 +5997,7 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -6365,9 +6343,9 @@ } }, "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", "license": "MIT", "dependencies": { "@types/ms": "*" @@ -6536,9 +6514,9 @@ } }, "node_modules/@types/jest/node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "license": "MIT", "dependencies": { "@jest/schemas": "30.0.5", @@ -6606,12 +6584,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.3.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz", - "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "undici-types": "~7.19.0" } }, "node_modules/@types/node-forge": { @@ -6765,6 +6743,12 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -7467,18 +7451,6 @@ "node": ">= 8" } }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -7794,17 +7766,6 @@ "node": ">=4" } }, - "node_modules/axios": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -7954,13 +7915,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.15.tgz", - "integrity": "sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==", + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", "license": "MIT", "dependencies": { "@babel/compat-data": "^7.28.6", - "@babel/helper-define-polyfill-provider": "^0.6.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", "semver": "^6.3.1" }, "peerDependencies": { @@ -7977,12 +7938,12 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.0.tgz", - "integrity": "sha512-AvDcMxJ34W4Wgy4KBIIePQTAOP1Ie2WFwkQp3dB7FQ/f0lI5+nM96zUnYEOE1P9sEg0es5VCP0HxiWu5fUHZAQ==", + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", "core-js-compat": "^3.48.0" }, "peerDependencies": { @@ -7990,12 +7951,12 @@ } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.6.tgz", - "integrity": "sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A==", + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.6" + "@babel/helper-define-polyfill-provider": "^0.6.8" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -8110,9 +8071,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", - "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "version": "2.10.12", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.12.tgz", + "integrity": "sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -8230,9 +8191,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -8426,9 +8387,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001777", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", - "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", + "version": "1.0.30001782", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001782.tgz", + "integrity": "sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw==", "funding": [ { "type": "opencollective", @@ -8931,9 +8892,9 @@ "license": "MIT" }, "node_modules/core-js": { - "version": "3.48.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz", - "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==", + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -8942,9 +8903,9 @@ } }, "node_modules/core-js-compat": { - "version": "3.48.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz", - "integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==", + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", "license": "MIT", "dependencies": { "browserslist": "^4.28.1" @@ -8955,9 +8916,9 @@ } }, "node_modules/core-js-pure": { - "version": "3.48.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.48.0.tgz", - "integrity": "sha512-1slJgk89tWC51HQ1AEqG+s2VuwpTRr8ocu4n20QUcH1v9lAN0RXen0Q0AABa/DK1I7RrNWLucplOHMx8hfTGTw==", + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.49.0.tgz", + "integrity": "sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -10001,9 +9962,9 @@ } }, "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", "license": "ISC", "dependencies": { "robust-predicates": "^3.0.2" @@ -10167,8 +10128,7 @@ "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dom-converter": { "version": "0.2.0", @@ -10323,9 +10283,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.307", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", - "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "version": "1.5.329", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.329.tgz", + "integrity": "sha512-/4t+AS1l4S3ZC0Ja7PHFIWeBIxGA3QGqV8/yKsP36v7NcyUCl+bIcmw6s5zVuMIECWwBrAK/6QLzTmbJChBboQ==", "license": "ISC" }, "node_modules/embla-carousel": { @@ -10389,9 +10349,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", - "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -10521,9 +10481,9 @@ } }, "node_modules/es-iterator-helpers": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", - "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.1.tgz", + "integrity": "sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -10541,6 +10501,7 @@ "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0", "safe-array-concat": "^1.1.3" }, "engines": { @@ -11348,17 +11309,17 @@ } }, "node_modules/expect": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", - "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.2.0", + "@jest/expect-utils": "30.3.0", "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.2.0", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", - "jest-util": "30.2.0" + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -11600,9 +11561,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -11719,9 +11680,9 @@ } }, "node_modules/flatted": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", - "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "license": "ISC" }, "node_modules/follow-redirects": { @@ -11856,22 +11817,6 @@ "node": ">=6" } }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -13944,18 +13889,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-circus/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jest-cli": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz", @@ -14047,18 +13980,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-cli/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jest-config": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz", @@ -14168,28 +14089,16 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-config/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jest-diff": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", - "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", + "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", "license": "MIT", "dependencies": { - "@jest/diff-sequences": "30.0.1", + "@jest/diff-sequences": "30.3.0", "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "pretty-format": "30.2.0" + "pretty-format": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -14208,9 +14117,9 @@ } }, "node_modules/jest-diff/node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "license": "MIT", "dependencies": { "@jest/schemas": "30.0.5", @@ -14312,18 +14221,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-each/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jest-environment-jsdom": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz", @@ -14412,18 +14309,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-environment-jsdom/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jest-environment-node": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.5.1.tgz", @@ -14511,18 +14396,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-environment-node/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jest-get-type": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", @@ -14624,18 +14497,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-haste-map/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jest-jasmine2": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz", @@ -14786,18 +14647,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-jasmine2/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jest-leak-detector": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz", @@ -14812,15 +14661,15 @@ } }, "node_modules/jest-matcher-utils": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", - "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", + "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "jest-diff": "30.2.0", - "pretty-format": "30.2.0" + "jest-diff": "30.3.0", + "pretty-format": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -14839,9 +14688,9 @@ } }, "node_modules/jest-matcher-utils/node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "license": "MIT", "dependencies": { "@jest/schemas": "30.0.5", @@ -14859,18 +14708,18 @@ "license": "MIT" }, "node_modules/jest-message-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", - "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "micromatch": "^4.0.8", - "pretty-format": "30.2.0", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" }, @@ -14891,9 +14740,9 @@ } }, "node_modules/jest-message-util/node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "license": "MIT", "dependencies": { "@jest/schemas": "30.0.5", @@ -14911,14 +14760,14 @@ "license": "MIT" }, "node_modules/jest-mock": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", - "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "@types/node": "*", - "jest-util": "30.2.0" + "jest-util": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -15076,18 +14925,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-resolve/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jest-runner": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.5.1.tgz", @@ -15197,18 +15034,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-runner/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jest-runtime": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz", @@ -15341,18 +15166,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-runtime/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jest-serializer": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz", @@ -15521,30 +15334,18 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-snapshot/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jest-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" + "picomatch": "^4.0.3" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -15827,18 +15628,6 @@ "node": ">=8" } }, - "node_modules/jest-watch-typeahead/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jest-watch-typeahead/node_modules/pretty-format": { "version": "28.1.3", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", @@ -15999,18 +15788,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-watcher/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -16154,12 +15931,6 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "license": "MIT" }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "license": "(AFL-2.1 OR BSD-3-Clause)" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -16281,9 +16052,9 @@ } }, "node_modules/launch-editor": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.1.tgz", - "integrity": "sha512-lPSddlAAluRKJ7/cjRFoXUFzaX7q/YKI7yPHuEvSJVqoXvFnJov1/Ud87Aa4zULIbA9Nja4mSPK8l0z/7eV2wA==", + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.2.tgz", + "integrity": "sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg==", "license": "MIT", "dependencies": { "picocolors": "^1.1.1", @@ -16367,15 +16138,15 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", - "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, "node_modules/lodash.debounce": { @@ -16453,7 +16224,6 @@ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -17437,18 +17207,6 @@ "node": ">=8.6" } }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -17501,9 +17259,9 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.0.tgz", - "integrity": "sha512-540P2c5dYnJlyJxTaSloliZexv8rji6rY8FhQN+WF/82iHQfA23j/xtJx97L+mXOML27EqksSek/g4eK7jaL3g==", + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.2.tgz", + "integrity": "sha512-AOSS0IdEB95ayVkxn5oGzNQwqAi2J0Jb/kKm43t7H73s8+f5873g0yuj0PNvK4dO75mu5DHg4nlgp4k6Kga8eg==", "license": "MIT", "dependencies": { "schema-utils": "^4.0.0", @@ -17672,9 +17430,9 @@ } }, "node_modules/node-forge": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", - "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" @@ -18163,9 +17921,9 @@ "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, "node_modules/path-type": { @@ -18190,9 +17948,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" @@ -19495,9 +19253,9 @@ "license": "CC0-1.0" }, "node_modules/postcss-svgo/node_modules/sax": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", - "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -19688,12 +19446,6 @@ "node": ">= 0.10" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -20143,6 +19895,29 @@ "react": ">=18" } }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -20260,18 +20035,6 @@ "node": ">=8.10.0" } }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -20297,6 +20060,21 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -20588,6 +20366,12 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "license": "MIT" }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -20740,9 +20524,9 @@ } }, "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", "license": "Unlicense" }, "node_modules/rollup": { @@ -20972,8 +20756,7 @@ "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/schema-utils": { "version": "4.3.3", @@ -21099,9 +20882,9 @@ "license": "MIT" }, "node_modules/serialize-javascript": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.4.tgz", - "integrity": "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz", + "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==", "license": "BSD-3-Clause", "engines": { "node": ">=20.0.0" @@ -22196,27 +21979,10 @@ } } }, - "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", "license": "MIT", "engines": { "node": ">=6" @@ -22282,9 +22048,9 @@ } }, "node_modules/terser": { - "version": "5.46.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", - "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "version": "5.46.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", + "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -22300,9 +22066,9 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.17", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz", - "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", @@ -22723,9 +22489,9 @@ } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -23139,9 +22905,9 @@ } }, "node_modules/web-vitals": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.1.0.tgz", - "integrity": "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.2.0.tgz", + "integrity": "sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA==", "license": "Apache-2.0" }, "node_modules/webidl-conversions": { @@ -23284,9 +23050,9 @@ } }, "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -23621,13 +23387,12 @@ } }, "node_modules/workbox-build/node_modules/@apideck/better-ajv-errors": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", - "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.7.tgz", + "integrity": "sha512-TajUJwGWbDwkCx/CZi7tRE8PVB7simCvKJfHUsSdvps+aTM/PDPP4gkLmKnc+x3CE//y9i/nj74GqdL/hwk7Iw==", "license": "MIT", "dependencies": { - "json-schema": "^0.4.0", - "jsonpointer": "^5.0.0", + "jsonpointer": "^5.0.1", "leven": "^3.1.0" }, "engines": { @@ -23961,9 +23726,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "license": "ISC", "engines": { "node": ">= 6" diff --git a/src/App/package.json b/src/App/package.json index 72987be3b..cdf71b1f5 100644 --- a/src/App/package.json +++ b/src/App/package.json @@ -4,36 +4,39 @@ "private": true, "proxy": "http://localhost:5000", "dependencies": { - "@fluentui/react": "^8.125.4", - "@azure/msal-react": "^3.0.23", - "@azure/msal-browser": "^4.24.1", - "@testing-library/jest-dom": "^6.9.0", - "@fluentui/react-icons": "^2.0.317", - "@fluentui/react-components": "^9.72.11", - "@testing-library/react": "^16.3.1", + "@azure/msal-browser": "^4.30.0", + "@azure/msal-react": "^3.0.29", + "@fluentui/react": "^8.125.5", + "@fluentui/react-components": "^9.73.7", + "@fluentui/react-icons": "^2.0.324", + "@reduxjs/toolkit": "^2.11.2", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", "@types/d3": "^7.4.3", "@types/jest": "^30.0.0", - "@types/node": "^25.1.0", - "@types/react": "^18.3.11", - "@types/react-dom": "^18.3.1", - "axios": "^1.13.5", - "chart.js": "^4.5.0", + "@types/node": "^25.6.0", + "@types/react": "^18.3.28", + "@types/react-dom": "^18.3.7", + "chart.js": "^4.5.1", "d3": "^7.9.0", - "d3-cloud": "^1.2.8", + "d3-cloud": "^1.2.9", "d3-color": "^3.1.0", - "lodash-es": "^4.17.22", + "lodash-es": "^4.18.1", "react": "^18.3.1", "react-chartjs-2": "^5.3.1", "react-d3-cloud": "^1.0.6", "react-dom": "^18.3.1", "react-markdown": "^10.1.0", + "react-redux": "^9.1.2", "react-scripts": "^5.0.1", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-supersub": "^1.0.0", + "scheduler": "^0.27.0", "typescript": "^4.9.5", - "web-vitals": "^5.1.0" + "web-vitals": "^5.2.0" }, "scripts": { "start": "react-scripts start", @@ -70,7 +73,13 @@ "overrides": { "d3-color": "$d3-color", "nth-check": "$nth-check", + "flatted": "^3.4.2", + "lodash": "4.18.1", + "node-forge": "^1.4.0", + "react-scripts": { + "picomatch": "^4.0.4" + }, "serialize-javascript": "^7.0.3", "bfj": "^9.1.3" } -} \ No newline at end of file +} diff --git a/src/App/src/App.css b/src/App/src/App.css index 74b63ffee..265e41393 100644 --- a/src/App/src/App.css +++ b/src/App/src/App.css @@ -1,6 +1,7 @@ :root { --margin-base: 0.8%; --gap-base: 0.8%; + --header-height: 6vh; --button-height: 2rem; --header-btn-font-size: 1rem; --header-btn-icon-size: 1rem; @@ -9,10 +10,10 @@ .main-container { display: flex; flex-direction: row; - height: calc(100vh - 6vh); - /* height: 94vh; */ + height: calc(100vh - var(--header-height)); gap: var(--gap-base); margin-inline: var(--margin-base); + overflow: hidden; } .main-container > div { @@ -31,7 +32,6 @@ body { .header { display: flex; align-items: center; - /* height: 64px; */ height: 6vh; justify-content: space-between; background-color: #fff; @@ -44,20 +44,27 @@ body { } .header-title { - /* font-size: 24px; */ color: #242424; - /* font-size: 18px; - font-style: normal; - font-weight: 700; - font-family: "Segoe UI"; */ margin-left: 0.5rem; - /* font-size: 1.1rem; */ } .header-title span { font-weight: 400; } +.citation-panel-wrapper { + flex: 0 1 20%; + min-width: 200px; + max-width: 400px; + margin-block: var(--margin-base); + overflow: hidden; +} + +.citation-panel-wrapper .citationPanel { + height: 100%; + box-sizing: border-box; +} + .left-section { position: relative; margin-block-end: 0.1% !important; @@ -77,20 +84,6 @@ body { gap: 0.25rem; } -/* .header button { - height: var(--button-height) !important; - font-size: var(--header-btn-font-size) !important; -} */ - -/* .header button > span > svg { - width: var(--header-btn-icon-size); - height: var(--header-btn-icon-size); -} */ - -/* .answerDisclaimerContainer { - margin-top: 3px; -} */ - .answerDisclaimer { font-size: var(--answer-disclaimer-font, 12px); color: #a0a0a0; diff --git a/src/App/src/App.test.tsx b/src/App/src/App.test.tsx index 2a68616d9..ea44d87c5 100644 --- a/src/App/src/App.test.tsx +++ b/src/App/src/App.test.tsx @@ -1,9 +1,29 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import App from './App'; +import chatReducer, { + appendMessages, + setGeneratingResponse, +} from "./state/slices/chatSlice"; -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); +describe("chatSlice", () => { + it("stores chat messages with typed actions", () => { + const initialState = chatReducer(undefined, { type: "@@INIT" }); + const loadingState = chatReducer( + initialState, + setGeneratingResponse(true) + ); + const nextState = chatReducer( + loadingState, + appendMessages([ + { + id: "message-1", + role: "user", + content: "Hello world", + date: "2026-04-10T00:00:00.000Z", + }, + ]) + ); + + expect(nextState.generatingResponse).toBe(true); + expect(nextState.messages).toHaveLength(1); + expect(nextState.messages[0].content).toBe("Hello world"); + }); }); diff --git a/src/App/src/App.tsx b/src/App/src/App.tsx index 229db1442..0cb68c9d4 100644 --- a/src/App/src/App.tsx +++ b/src/App/src/App.tsx @@ -1,34 +1,36 @@ -import React, { useEffect, useState, useRef } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import Chart from "./components/Chart/Chart"; import Chat from "./components/Chat/Chat"; import { + Avatar, + Body2, Button, FluentProvider, Subtitle2, - Body2, webLightTheme, - Avatar, - Tag, } from "@fluentui/react-components"; import { SparkleRegular } from "@fluentui/react-icons"; import "./App.css"; import { ChatHistoryPanel } from "./components/ChatHistoryPanel/ChatHistoryPanel"; - +import { getUserInfo } from "./api/api"; +import { useAppDispatch, useAppSelector } from "./state/hooks"; import { - getUserInfo, - getLayoutConfig, - historyDelete, - historyDeleteAll, - historyList, - historyRead, -} from "./api/api"; - -import { useAppContext } from "./state/useAppContext"; -import { actionConstants } from "./state/ActionConstants"; -import { ChatMessage, Conversation } from "./types/AppTypes"; + fetchLayoutConfig, + setSelectedConversationId, + setShowAppSpinner, + startNewConversation, +} from "./state/slices/appSlice"; +import { + clearAllConversations, + fetchConversationMessages, + fetchConversations, +} from "./state/slices/chatHistorySlice"; +import { resetChatState, setMessages } from "./state/slices/chatSlice"; +import { hideCitation } from "./state/slices/citationSlice"; import { AppLogo } from "./components/Svg/Svg"; import CustomSpinner from "./components/CustomSpinner/CustomSpinner"; import CitationPanel from "./components/CitationPanel/CitationPanel"; + const panels = { DASHBOARD: "DASHBOARD", CHAT: "CHAT", @@ -53,8 +55,18 @@ const defaultPanelShowStates = { }; const Dashboard: React.FC = () => { - const { state, dispatch } = useAppContext(); - const { appConfig } = state.config; + const dispatch = useAppDispatch(); + const appConfig = useAppSelector((state) => state.app.config.appConfig); + const showAppSpinner = useAppSelector((state) => state.app.showAppSpinner); + const activeCitation = useAppSelector((state) => state.citation.activeCitation); + const showCitation = useAppSelector((state) => state.citation.showCitation); + const currentConversationIdForCitation = useAppSelector( + (state) => state.citation.currentConversationIdForCitation + ); + const isFetchingConversations = useAppSelector( + (state) => state.chatHistory.fetchingConversations + ); + const [panelShowStates, setPanelShowStates] = useState< Record >({ ...defaultPanelShowStates }); @@ -64,229 +76,186 @@ const Dashboard: React.FC = () => { const [layoutWidthUpdated, setLayoutWidthUpdated] = useState(false); const [showClearAllConfirmationDialog, setChowClearAllConfirmationDialog] = useState(false); - const [clearing, setClearing] = React.useState(false); - const [clearingError, setClearingError] = React.useState(false); - const [isInitialAPItriggered, setIsInitialAPItriggered] = useState(false); - const [showAuthMessage, setShowAuthMessage] = useState(); + const [clearing, setClearing] = useState(false); + const [clearingError, setClearingError] = useState(false); const [offset, setOffset] = useState(0); - const OFFSET_INCREMENT = 25; const [hasMoreRecords, setHasMoreRecords] = useState(true); const [name, setName] = useState(""); + const OFFSET_INCREMENT = 25; useEffect(() => { - try { - const fetchConfig = async () => { - const configData = await getLayoutConfig(); - console.log("configData", configData); - dispatch({ type: actionConstants.SAVE_CONFIG, payload: configData }); - }; - fetchConfig(); - } catch (error) { - console.error("Failed to fetch chart configuration:", error); - } - }, []); - - const getUserInfoList = async () => { - const userInfoList = await getUserInfo(); - if ( - userInfoList.length === 0 && - window.location.hostname !== "localhost" && - window.location.hostname !== "127.0.0.1" - ) { - setShowAuthMessage(true); - } else { - setShowAuthMessage(false); - } - }; + void dispatch(fetchLayoutConfig()); + }, [dispatch]); useEffect(() => { - getUserInfoList(); + const hydrateUser = async () => { + const userInfo = await getUserInfo(); + const displayName: string = + userInfo[0]?.user_claims?.find((claim: any) => claim.typ === "name") + ?.val ?? ""; + setName(displayName); + }; + + void hydrateUser(); }, []); - useEffect(() => { - getUserInfo().then((res) => { - const name: string = res[0]?.user_claims?.find((claim: any) => claim.typ === 'name')?.val ?? '' - setName(name) - }).catch((err) => { - console.error('Error fetching user info: ', err) - }) - }, []) - - const updateLayoutWidths = (newState: Record) => { - const noOfWidgetsOpen = Object.values(newState).filter((val) => val).length; - if (appConfig === null) { - return; - } + const updateLayoutWidths = useCallback( + (newState: Record) => { + const noOfWidgetsOpen = Object.values(newState).filter(Boolean).length; + if (appConfig === null) { + return; + } + + if ( + noOfWidgetsOpen === 1 || + (noOfWidgetsOpen === 2 && !newState[panels.CHAT]) + ) { + setPanelWidths(defaultSingleColumnConfig); + return; + } - if ( - noOfWidgetsOpen === 1 || - (noOfWidgetsOpen === 2 && !newState[panels.CHAT]) - ) { - setPanelWidths(defaultSingleColumnConfig); - } else if (noOfWidgetsOpen === 2 && newState[panels.CHAT]) { - const panelsInOpenState = Object.keys(newState).filter( - (key) => newState[key] - ); - const twoColLayouts = Object.keys(appConfig.TWO_COLUMN) as string[]; - for (let i = 0; i < twoColLayouts.length; i++) { - const key = twoColLayouts[i] as string; - const panelNames = key.split("_"); - const isMatched = panelsInOpenState.every((val) => - panelNames.includes(val) + if (noOfWidgetsOpen === 2 && newState[panels.CHAT]) { + const panelsInOpenState = Object.keys(newState).filter( + (key) => newState[key] ); - const TWO_COLUMN = appConfig.TWO_COLUMN as Record< - string, - Record - >; - if (isMatched) { - setPanelWidths({ ...TWO_COLUMN[key] }); - break; + const twoColumnLayouts = Object.keys(appConfig.TWO_COLUMN) as string[]; + + for (const layoutKey of twoColumnLayouts) { + const panelNames = layoutKey.split("_"); + const isMatched = panelsInOpenState.every((value) => + panelNames.includes(value) + ); + const twoColumnConfig = appConfig.TWO_COLUMN as Record< + string, + Record + >; + + if (isMatched) { + setPanelWidths({ ...twoColumnConfig[layoutKey] }); + return; + } } } - } else { - const threeColumn = appConfig.THREE_COLUMN as Record; + + const threeColumn = { + ...(appConfig.THREE_COLUMN as Record), + }; threeColumn.DASHBOARD = threeColumn.DASHBOARD > 55 ? threeColumn.DASHBOARD : 55; - setPanelWidths({ ...threeColumn }); - } - }; + setPanelWidths(threeColumn); + }, + [appConfig] + ); useEffect(() => { updateLayoutWidths(panelShowStates); - }, [state.config.appConfig]); - - const onHandlePanelStates = (panelName: string) => { - dispatch({ type: actionConstants.UPDATE_CITATION,payload: { activeCitation: null, showCitation: false }}) - setLayoutWidthUpdated((prevFlag) => !prevFlag); - const newState = { - ...panelShowStates, - [panelName]: !panelShowStates[panelName], - }; - const isHiddenBoth = !newState[panels.DASHBOARD] && !newState[panels.CHAT]; - if (isHiddenBoth && panelName === panels.CHAT) { - newState[panels.DASHBOARD] = true; - } else if (isHiddenBoth && panelName === panels.DASHBOARD) { - newState[panels.CHAT] = true; - } - updateLayoutWidths(newState); - setPanelShowStates(newState); - }; + }, [panelShowStates, updateLayoutWidths]); + + const onHandlePanelStates = useCallback( + (panelName: string) => { + setLayoutWidthUpdated((previousFlag) => !previousFlag); + const nextState = { + ...panelShowStates, + [panelName]: !panelShowStates[panelName], + }; + const isHiddenBoth = !nextState[panels.DASHBOARD] && !nextState[panels.CHAT]; + + if (isHiddenBoth && panelName === panels.CHAT) { + nextState[panels.DASHBOARD] = true; + } else if (isHiddenBoth && panelName === panels.DASHBOARD) { + nextState[panels.CHAT] = true; + } + + if (panelName === panels.CHAT && !nextState[panels.CHAT]) { + dispatch(hideCitation()); + } + + updateLayoutWidths(nextState); + setPanelShowStates(nextState); + }, + [dispatch, panelShowStates, updateLayoutWidths] + ); - const getHistoryListData = async () => { - if (!hasMoreRecords) { + const getHistoryListData = useCallback(async () => { + if (!hasMoreRecords || isFetchingConversations) { return; } - dispatch({ - type: actionConstants.UPDATE_CONVERSATIONS_FETCHING_FLAG, - payload: true, - }); - const convs = await historyList(offset); - if (convs !== null) { - if (convs.length === OFFSET_INCREMENT) { - setOffset((offset) => (offset += OFFSET_INCREMENT)); - // Stopping offset increment if there were no records - } else if (convs.length < OFFSET_INCREMENT) { + + const result = await dispatch(fetchConversations(offset)); + if (fetchConversations.fulfilled.match(result)) { + const conversations = result.payload; + if (conversations.length === OFFSET_INCREMENT) { + setOffset((currentOffset) => currentOffset + OFFSET_INCREMENT); + } else if (conversations.length < OFFSET_INCREMENT) { setHasMoreRecords(false); } - dispatch({ - type: actionConstants.ADD_CONVERSATIONS_TO_LIST, - payload: convs, - }); } - dispatch({ - type: actionConstants.UPDATE_CONVERSATIONS_FETCHING_FLAG, - payload: false, - }); - }; - - const onClearAllChatHistory = async () => { - dispatch({ - type: actionConstants.UPDATE_APP_SPINNER_STATUS, - payload: true, - }); - dispatch({ type: actionConstants.UPDATE_CITATION,payload: { activeCitation: null, showCitation: false }}) + }, [ + dispatch, + hasMoreRecords, + isFetchingConversations, + offset, + OFFSET_INCREMENT, + ]); + + useEffect(() => { + void getHistoryListData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const onClearAllChatHistory = useCallback(async () => { + dispatch(setShowAppSpinner(true)); + dispatch(hideCitation()); setClearing(true); - const response = await historyDeleteAll(); - if (!response.ok) { + setClearingError(false); + + const result = await dispatch(clearAllConversations()); + if (clearAllConversations.rejected.match(result)) { setClearingError(true); } else { setChowClearAllConfirmationDialog(false); - dispatch({ type: actionConstants.UPDATE_ON_CLEAR_ALL_CONVERSATIONS }); + dispatch(resetChatState()); + dispatch(startNewConversation()); } - setClearing(false); - dispatch({ - type: actionConstants.UPDATE_APP_SPINNER_STATUS, - payload: false, - }); - }; - useEffect(() => { - setIsInitialAPItriggered(true); - }, []); + setClearing(false); + dispatch(setShowAppSpinner(false)); + }, [dispatch]); - useEffect(() => { - if (isInitialAPItriggered) { - (async () => { - getHistoryListData(); - })(); - } - }, [isInitialAPItriggered]); + const onSelectConversation = useCallback( + async (id: string) => { + if (!id) { + return; + } - const [ASSISTANT, TOOL, ERROR, USER] = ["assistant", "tool", "error", "user"]; + dispatch(hideCitation()); + dispatch(setSelectedConversationId(id)); + const result = await dispatch(fetchConversationMessages(id)); - const onSelectConversation = async (id: string) => { - if (!id) { - console.error("No conversation ID found"); - return; - } - dispatch({ - type: actionConstants.UPDATE_CHATHISTORY_CONVERSATION_FLAG, - payload: true, - }); - dispatch({ - type: actionConstants.UPDATE_SELECTED_CONV_ID, - payload: id, - }); - try { - const responseMessages = await historyRead(id); - - if (responseMessages) { - dispatch({ - type: actionConstants.SHOW_CHATHISTORY_CONVERSATION, - payload: { - id, - messages: responseMessages, - }, - }); + if (fetchConversationMessages.fulfilled.match(result)) { + dispatch(setMessages(result.payload.messages)); } + }, + [dispatch] + ); - } catch (error) { - console.error("Error fetching conversation messages:", error); - } finally { - dispatch({ - type: actionConstants.UPDATE_CHATHISTORY_CONVERSATION_FLAG, - payload: false, - }); - } - }; - - const onClickClearAllOption = () => { - setChowClearAllConfirmationDialog((prevFlag) => !prevFlag); - }; + const onClickClearAllOption = useCallback(() => { + setChowClearAllConfirmationDialog((previousFlag) => !previousFlag); + }, []); - const onHideClearAllDialog = () => { - setChowClearAllConfirmationDialog((prevFlag) => !prevFlag); + const onHideClearAllDialog = useCallback(() => { + setChowClearAllConfirmationDialog((previousFlag) => !previousFlag); setTimeout(() => { setClearingError(false); }, 1000); - }; + }, []); return ( - +
@@ -300,7 +269,7 @@ const Dashboard: React.FC = () => { onClick={() => onHandlePanelStates(panels.DASHBOARD)} > {`${ - panelShowStates?.[panels.DASHBOARD] ? "Hide" : "Show" + panelShowStates[panels.DASHBOARD] ? "Hide" : "Show" } Dashboard`}
@@ -316,8 +285,7 @@ const Dashboard: React.FC = () => {
- {/* LEFT PANEL: DASHBOARD */} - {panelShowStates?.[panels.DASHBOARD] && ( + {panelShowStates[panels.DASHBOARD] && (
{
)} - {/* MIDDLE PANEL: CHAT */} - {panelShowStates?.[panels.CHAT] && ( + {panelShowStates[panels.CHAT] && (
{ />
)} - {state.citation.showCitation && state.citation.currentConversationIdForCitation !== "" && ( -
- - + {showCitation && currentConversationIdForCitation !== "" && ( +
+
)} - {/* RIGHT PANEL: CHAT HISTORY */} - {panelShowStates?.[panels.CHAT] && - panelShowStates?.[panels.CHATHISTORY] && ( + {panelShowStates[panels.CHAT] && + panelShowStates[panels.CHATHISTORY] && (
{ getHistoryListData()} + handleFetchHistory={getHistoryListData} onClearAllChatHistory={onClearAllChatHistory} onClickClearAllOption={onClickClearAllOption} onHideClearAllDialog={onHideClearAllDialog} onSelectConversation={onSelectConversation} showClearAllConfirmationDialog={showClearAllConfirmationDialog} /> - {/* {useAppContext?.state.isChatHistoryOpen && - useAppContext?.state.isCosmosDBAvailable?.status !== CosmosDBStatus.NotConfigured && } */}
)}
diff --git a/src/App/src/Assets/ContosoImg.png b/src/App/src/Assets/ContosoImg.png deleted file mode 100644 index 0a1ca0f8b..000000000 Binary files a/src/App/src/Assets/ContosoImg.png and /dev/null differ diff --git a/src/App/src/Assets/Reset-icon.svg b/src/App/src/Assets/Reset-icon.svg deleted file mode 100644 index a19f8611f..000000000 --- a/src/App/src/Assets/Reset-icon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/App/src/Assets/Sparkle.png b/src/App/src/Assets/Sparkle.png deleted file mode 100644 index 6b928f2c3..000000000 Binary files a/src/App/src/Assets/Sparkle.png and /dev/null differ diff --git a/src/App/src/Assets/Sparkle.svg b/src/App/src/Assets/Sparkle.svg deleted file mode 100644 index f8247c037..000000000 --- a/src/App/src/Assets/Sparkle.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/src/App/src/Assets/km_logo.png b/src/App/src/Assets/km_logo.png deleted file mode 100644 index 65dd92d85..000000000 Binary files a/src/App/src/Assets/km_logo.png and /dev/null differ diff --git a/src/App/src/api/api.ts b/src/App/src/api/api.ts index 8199dcda0..6aee0b834 100644 --- a/src/App/src/api/api.ts +++ b/src/App/src/api/api.ts @@ -3,62 +3,82 @@ import { historyReadResponse, } from "../configs/StaticData"; import { - AppConfig, - ChartConfigItem, - ChatMessage, - Conversation, - ConversationRequest, - CosmosDBHealth, + type AppConfig, + type ChartConfigItem, + type ChatMessage, + type Conversation, + type ConversationRequest, + type CosmosDBHealth, CosmosDBStatus, } from "../types/AppTypes"; -const baseURL = process.env.REACT_APP_API_BASE_URL;// base API URL +import httpClient from "./httpClient"; +import { + createErrorResponse, + RequestCache, + retryRequest, +} from "../utils/apiUtils"; -export const fetchChartData = async () => { +const layoutConfigCache = new RequestCache<{ + appConfig: AppConfig; + charts: ChartConfigItem[]; +}>(); + +const mapConversation = (conversation: any): Conversation => ({ + id: conversation.id, + title: conversation.title, + date: conversation.createdAt, + updatedAt: conversation?.updatedAt, + messages: Array.isArray(conversation.messages) ? conversation.messages : [], +}); + +const mapHistoryMessage = (message: any): ChatMessage => ({ + id: message.id, + role: message.role, + content: message.content?.content ?? message.content, + date: message.createdAt, + feedback: message.feedback ?? undefined, + context: message.context, + citations: message.content?.citations ?? message.citations, + contentType: message.contentType, +}); + +const parseResponseJson = async (response: Response): Promise => { try { - const response = await fetch(`${baseURL}/api/fetchChartData`); - if (!response.ok) { - throw new Error(`Error: ${response.status} ${response.statusText}`); - } - const data = await response.json(); - return data; - } catch (error) { - console.error("Failed to fetch chart data:", error); - throw error; // Rethrow the error so the calling function can handle it + return (await response.json()) as T; + } catch { + return null; + } +}; + +export const fetchChartData = async () => { + const response = await retryRequest(() => httpClient.get("/api/fetchChartData")); + if (!response.ok) { + throw new Error(`Error: ${response.status} ${response.statusText}`); } + + return response.json(); }; export const fetchChartDataWithFilters = async (bodyData: any) => { - try { - const response = await fetch(`${baseURL}/api/fetchChartDataWithFilters`, { - headers: { - "Content-Type": "application/json", - }, - method: "POST", - body: JSON.stringify(bodyData), - }); - if (!response.ok) { - throw new Error(`Error: ${response.status} ${response.statusText}`); - } - const data = await response.json(); - return data; - } catch (error) { - console.error("Failed to fetch filtered chart data:", error); - throw error; + const response = await httpClient.post( + "/api/fetchChartDataWithFilters", + bodyData + ); + + if (!response.ok) { + throw new Error(`Error: ${response.status} ${response.statusText}`); } + + return response.json(); }; export const fetchFilterData = async () => { - try { - const response = await fetch(`${baseURL}/api/fetchFilterData`); - if (!response.ok) { - throw new Error(`Error: ${response.status} ${response.statusText}`); - } - const data = await response.json(); - return data; - } catch (error) { - console.error("Failed to fetch filter data:", error); - throw error; + const response = await httpClient.get("/api/fetchFilterData"); + if (!response.ok) { + throw new Error(`Error: ${response.status} ${response.statusText}`); } + + return response.json(); }; export type UserInfo = { @@ -71,231 +91,174 @@ export type UserInfo = { }; export async function getUserInfo(): Promise { - const response = await fetch(`/.auth/me`); + const controller = new AbortController(); + const timeoutId = window.setTimeout(() => controller.abort(), 15000); + let response: Response; + + try { + response = await fetch(`${window.location.origin}/.auth/me`, { + signal: controller.signal, + }); + } catch { + return []; + } finally { + window.clearTimeout(timeoutId); + } + if (!response.ok) { - console.error("No identity provider found. Access to chat will be blocked."); return []; } - const payload = await response.json(); - const userClaims = payload[0]?.user_claims || []; + + const payload = await parseResponseJson(response); + const userClaims = payload?.[0]?.user_claims ?? []; const objectIdClaim = userClaims.find( (claim: any) => claim.typ === "http://schemas.microsoft.com/identity/claims/objectidentifier" ); const userId = objectIdClaim?.val; + if (userId) { localStorage.setItem("userId", userId); } - return payload; -} - -function getUserIdFromLocalStorage(): string | null { - return localStorage.getItem("userId"); + return payload ?? []; } export const historyRead = async (convId: string): Promise => { - const userId = getUserIdFromLocalStorage(); - const response = await fetch(`${baseURL}/history/read`, { - method: "POST", - body: JSON.stringify({ - conversation_id: convId, - }), - headers: { - "Content-Type": "application/json", - "X-Ms-Client-Principal-Id": userId || "", - }, - }) - .then(async (res) => { - if (!res.ok) { - return historyReadResponse.messages.map((msg: any) => ({ - id: msg.id, - role: msg.role, - content: msg.content.content, - date: msg.createdAt, - feedback: msg.feedback ?? undefined, - context: msg.context, - contentType: msg.contentType, - })); - } - const payload = await res.json(); - const messages: ChatMessage[] = []; - - if (Array.isArray(payload?.messages)) { - payload.messages.forEach((msg: any) => { - const message: ChatMessage = { - id: msg.id, - role: msg.role, - content: msg.content.content, - date: msg.createdAt, - feedback: msg.feedback ?? undefined, - context: msg.context, - citations: msg.content.citations, - contentType: msg.contentType, - }; - messages.push(message); - }); - } - return messages; - }) - .catch((_err) => { - console.error("There was an issue fetching your data."); - return []; - }); - return response; + try { + const response = await retryRequest(() => + httpClient.post("/history/read", { + conversation_id: convId, + }) + ); + + if (!response.ok) { + return historyReadResponse.messages.map(mapHistoryMessage); + } + + const payload = await parseResponseJson<{ messages?: any[] }>(response); + return Array.isArray(payload?.messages) + ? (payload?.messages ?? []).map(mapHistoryMessage) + : []; + } catch { + return []; + } }; export const historyList = async ( offset = 0 ): Promise => { - const userId = getUserIdFromLocalStorage(); - let response = await fetch(`${baseURL}/history/list?offset=${offset}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - "X-Ms-Client-Principal-Id": userId || "", - }, -}) - .then(async (res) => { - let payload = await res.json(); - if (!Array.isArray(payload)) { - console.error("There was an issue fetching your data."); - return null; - } - const conversations: Conversation[] = payload.map((conv: any) => { - const conversation: Conversation = { - id: conv.id, - title: conv.title, - date: conv.createdAt, - updatedAt: conv?.updatedAt, - messages: [], - }; - return conversation; - }); - return conversations; - }) - .catch((_err) => { - console.error("There was an issue fetching your data.", _err); - const conversations: Conversation[] = historyListResponse.map( - (conv: any) => { - const conversation: Conversation = { - id: conv.id, - title: conv.title, - date: conv.createdAt, - updatedAt: conv?.updatedAt, - messages: [], - }; - return conversation; - } - ); - return conversations; + try { + const response = await httpClient.get("/history/list", { + params: { offset }, }); - return response; + const payload = await parseResponseJson(response); + + if (!Array.isArray(payload)) { + return null; + } + + return payload.map(mapConversation); + } catch { + return historyListResponse.map(mapConversation); + } }; export const historyUpdate = async ( messages: ChatMessage[], convId: string ): Promise => { - const userId = getUserIdFromLocalStorage(); - const response = await fetch(`${baseURL}/history/update`, { - method: "POST", - body: JSON.stringify({ + try { + return await httpClient.post("/history/update", { conversation_id: convId, - messages: messages, - }), - headers: { - "Content-Type": "application/json", - "X-Ms-Client-Principal-Id": userId || "", - }, - }) - .then(async (res) => { - return res; - }) - .catch((_err) => { - console.error("There was an issue fetching your data."); - const errRes: Response = { - ...new Response(), - ok: false, - status: 500, - }; - return errRes; + messages, }); - return response; + } catch { + return createErrorResponse(500, "There was an issue updating chat history."); + } }; export async function getLayoutConfig(): Promise<{ appConfig: AppConfig; charts: ChartConfigItem[]; }> { - const userId = getUserIdFromLocalStorage(); - const response = await fetch(`${baseURL}/api/layout-config`, { - method: "GET", - headers: { - "Content-Type": "application/json", - "X-Ms-Client-Principal-Id": userId || "", - }, - }); try { - if (response.ok) { - const layoutConfigData = await response.json(); - return layoutConfigData; - } + return await layoutConfigCache.getOrCreate("layout-config", async () => { + const response = await httpClient.get("/api/layout-config"); + if (!response.ok) { + return { + appConfig: null, + charts: [], + }; + } + + const layoutConfigData = await parseResponseJson<{ + appConfig: AppConfig; + charts: ChartConfigItem[]; + }>(response); + + return ( + layoutConfigData ?? { + appConfig: null, + charts: [], + } + ); + }); } catch { - console.error("Failed to parse Layout config data"); + layoutConfigCache.clear("layout-config"); + return { + appConfig: null, + charts: [], + }; } - return { - appConfig: null, - charts: [], - }; } export async function getIsChartDisplayDefault(): Promise<{ isChartDisplayDefault: boolean; }> { - const userId = getUserIdFromLocalStorage(); - const response = await fetch(`${baseURL}/api/display-chart-default`, { - method: "GET", - headers: { - "Content-Type": "application/json", - "X-Ms-Client-Principal-Id": userId || "", - }, - }); try { - if (response.ok) { - const responseData = await response.json(); - const tempChartDisplayFlag = responseData.isChartDisplayDefault.toLowerCase() == 'true' ? true : false - return { isChartDisplayDefault: tempChartDisplayFlag } + const response = await httpClient.get("/api/display-chart-default"); + if (!response.ok) { + return { isChartDisplayDefault: true }; } + + const responseData = await parseResponseJson<{ + isChartDisplayDefault?: string | boolean; + }>(response); + const rawValue = responseData?.isChartDisplayDefault; + + return { + isChartDisplayDefault: + typeof rawValue === "string" + ? rawValue.toLowerCase() === "true" + : Boolean(rawValue), + }; } catch { - console.error("Failed to get chart config flag"); + return { + isChartDisplayDefault: true, + }; } - return { - isChartDisplayDefault: true - }; } export async function callConversationApi( options: ConversationRequest, abortSignal: AbortSignal ): Promise { - const userId = getUserIdFromLocalStorage(); - const response = await fetch(`${baseURL}/api/chat`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Ms-Client-Principal-Id": userId || "", - }, - body: JSON.stringify({ + const response = await httpClient.post( + "/api/chat", + { query: options.query, - conversation_id: options.id - }), - signal: abortSignal, - }); + conversation_id: options.id, + }, + { + signal: abortSignal, + timeout: 120000, + } + ); if (!response.ok) { - const errorData = await response.json(); - throw new Error(JSON.stringify(errorData.error)); + const errorData = await parseResponseJson<{ error?: unknown }>(response); + throw new Error(JSON.stringify(errorData?.error ?? "Chat request failed.")); } return response; @@ -305,148 +268,77 @@ export const historyRename = async ( convId: string, title: string ): Promise => { - const userId = getUserIdFromLocalStorage(); - const response = await fetch(`${baseURL}/history/rename`, { - method: "POST", - body: JSON.stringify({ + try { + return await httpClient.post("/history/rename", { conversation_id: convId, - title: title, - }), - headers: { - "Content-Type": "application/json", - "X-Ms-Client-Principal-Id": userId || "", - }, - }) - .then((res) => { - return res; - }) - .catch((_err) => { - console.error("There was an issue fetching your data."); - const errRes: Response = { - ...new Response(), - ok: false, - status: 500, - }; - return errRes; + title, }); - return response; + } catch { + return createErrorResponse(500, "There was an issue renaming the conversation."); + } }; export const historyDelete = async (convId: string): Promise => { - const userId = getUserIdFromLocalStorage(); - const response = await fetch(`${baseURL}/history/delete`, { - method: "DELETE", - body: JSON.stringify({ + try { + return await httpClient.delete("/history/delete", { conversation_id: convId, - }), - headers: { - "Content-Type": "application/json", - "X-Ms-Client-Principal-Id": userId || "", - }, - }) - .then((res) => { - return res; - }) - .catch((_err) => { - console.error("There was an issue fetching your data."); - const errRes: Response = { - ...new Response(), - ok: false, - status: 500, - }; - return errRes; }); - return response; + } catch { + return createErrorResponse(500, "There was an issue deleting the conversation."); + } }; export const historyDeleteAll = async (): Promise => { - const userId = getUserIdFromLocalStorage(); - const response = await fetch(`${baseURL}/history/delete_all`, { - method: "DELETE", - body: JSON.stringify({}), - headers: { - "Content-Type": "application/json", - "X-Ms-Client-Principal-Id": userId || "", - }, - }) - .then((res) => { - return res; - }) - .catch((_err) => { - console.error("There was an issue fetching your data."); - const errRes: Response = { - ...new Response(), - ok: false, - status: 500, - }; - return errRes; - }); - return response; + try { + return await httpClient.delete("/history/delete_all", {}); + } catch { + return createErrorResponse(500, "There was an issue clearing chat history."); + } }; export const historyEnsure = async (): Promise => { - const userId = getUserIdFromLocalStorage(); - const response = await fetch(`${baseURL}/history/ensure`, { - method: "GET", - headers: { - "Content-Type": "application/json", - "X-Ms-Client-Principal-Id": userId || "", - }, - }) - .then(async (res) => { - const respJson = await res.json(); - let formattedResponse; - if (respJson.message) { - formattedResponse = CosmosDBStatus.Working; - } else { - if (res.status === 500) { - formattedResponse = CosmosDBStatus.NotWorking; - } else if (res.status === 401) { - formattedResponse = CosmosDBStatus.InvalidCredentials; - } else if (res.status === 422) { - formattedResponse = respJson.error; - } else { - formattedResponse = CosmosDBStatus.NotConfigured; - } - } - if (!res.ok) { - return { - cosmosDB: false, - status: formattedResponse, - }; - } else { - return { - cosmosDB: true, - status: formattedResponse, - }; - } - }) - .catch((err) => { - console.error("There was an issue fetching your data."); + try { + const response = await httpClient.get("/history/ensure"); + if (response.status === 404) { return { cosmosDB: false, - status: err, + status: CosmosDBStatus.NotConfigured, }; - }); - return response; -}; + } -export const fetchCitationContent = async (body: any) => { - try { - const response = await fetch(`${baseURL}/api/fetch-azure-search-content`, { - headers: { - "Content-Type": "application/json", - }, - method: "POST", - body: JSON.stringify(body), - }); - if (!response.ok) { - throw new Error(`Error: ${response.status} ${response.statusText}`); + const responseJson = await parseResponseJson<{ + message?: string; + error?: string; + }>(response); + + let status: string = CosmosDBStatus.NotConfigured; + if (responseJson?.message) { + status = CosmosDBStatus.Working; + } else if (response.status === 500) { + status = CosmosDBStatus.NotWorking; + } else if (response.status === 401) { + status = CosmosDBStatus.InvalidCredentials; + } else if (response.status === 422) { + status = responseJson?.error ?? CosmosDBStatus.NotConfigured; } - const data = await response.json(); - return data; + + return { + cosmosDB: response.ok, + status, + }; } catch (error) { - console.error("Failed to fetch azure search content:", error); - throw error; + return { + cosmosDB: false, + status: error instanceof Error ? error.message : CosmosDBStatus.NotConfigured, + }; + } +}; + +export const fetchCitationContent = async (body: any) => { + const response = await httpClient.post("/api/fetch-azure-search-content", body); + if (!response.ok) { + throw new Error(`Error: ${response.status} ${response.statusText}`); } + + return response.json(); }; diff --git a/src/App/src/api/httpClient.ts b/src/App/src/api/httpClient.ts new file mode 100644 index 000000000..84f3c544a --- /dev/null +++ b/src/App/src/api/httpClient.ts @@ -0,0 +1,225 @@ +type QueryValue = string | number | boolean | null | undefined; +type QueryParams = Record; + +export type HttpRequestConfig = { + url: string; + method?: string; + headers?: HeadersInit; + params?: QueryParams; + body?: unknown; + signal?: AbortSignal; + timeout?: number; +}; + +type RequestInterceptor = + | ((config: HttpRequestConfig) => HttpRequestConfig) + | ((config: HttpRequestConfig) => Promise); +type ResponseInterceptor = + | ((response: Response) => Response) + | ((response: Response) => Promise); +type ErrorInterceptor = (error: unknown) => never; + +class HttpClient { + private readonly baseURL = process.env.REACT_APP_API_BASE_URL ?? ""; + private readonly defaultTimeout = 30000; + private readonly requestInterceptors: RequestInterceptor[] = []; + private readonly responseInterceptors: ResponseInterceptor[] = []; + private readonly errorInterceptors: ErrorInterceptor[] = []; + + constructor() { + this.addRequestInterceptor((config) => { + const headers = new Headers(config.headers ?? {}); + const userId = localStorage.getItem("userId") ?? ""; + + if (!headers.has("Content-Type") && !(config.body instanceof FormData)) { + headers.set("Content-Type", "application/json"); + } + + if (userId && !headers.has("X-Ms-Client-Principal-Id")) { + headers.set("X-Ms-Client-Principal-Id", userId); + } + + return { + ...config, + headers, + }; + }); + + this.addResponseInterceptor((response) => { + if (response.status === 401) { + throw new Error("Unauthorized request. Sign in and try again."); + } + + return response; + }); + + this.addErrorInterceptor((error) => { + throw error instanceof Error + ? error + : new Error("Network request failed."); + }); + } + + addRequestInterceptor(interceptor: RequestInterceptor) { + this.requestInterceptors.push(interceptor); + } + + addResponseInterceptor(interceptor: ResponseInterceptor) { + this.responseInterceptors.push(interceptor); + } + + addErrorInterceptor(interceptor: ErrorInterceptor) { + this.errorInterceptors.push(interceptor); + } + + async request(config: HttpRequestConfig): Promise { + const resolvedConfig = await this.runRequestInterceptors(config); + const controller = new AbortController(); + const timeoutId = setTimeout( + () => controller.abort(), + resolvedConfig.timeout ?? this.defaultTimeout + ); + + const abortHandler = () => controller.abort(resolvedConfig.signal?.reason); + if (resolvedConfig.signal?.aborted) { + controller.abort(resolvedConfig.signal.reason); + } + resolvedConfig.signal?.addEventListener("abort", abortHandler, { + once: true, + }); + + try { + const response = await fetch(this.buildUrl(resolvedConfig.url, resolvedConfig.params), { + method: resolvedConfig.method ?? "GET", + headers: resolvedConfig.headers, + body: this.serializeBody(resolvedConfig.body), + signal: controller.signal, + }); + + return await this.runResponseInterceptors(response); + } catch (error) { + return this.runErrorInterceptors(error); + } finally { + clearTimeout(timeoutId); + resolvedConfig.signal?.removeEventListener("abort", abortHandler); + } + } + + get(url: string, options: Omit = {}) { + return this.request({ + ...options, + url, + method: "GET", + }); + } + + post( + url: string, + body?: unknown, + options: Omit = {} + ) { + return this.request({ + ...options, + url, + method: "POST", + body, + }); + } + + delete( + url: string, + body?: unknown, + options: Omit = {} + ) { + return this.request({ + ...options, + url, + method: "DELETE", + body, + }); + } + + private async runRequestInterceptors(config: HttpRequestConfig) { + let nextConfig = config; + + for (const interceptor of this.requestInterceptors) { + nextConfig = await interceptor(nextConfig); + } + + return nextConfig; + } + + private async runResponseInterceptors(response: Response) { + let nextResponse = response; + + for (const interceptor of this.responseInterceptors) { + nextResponse = await interceptor(nextResponse); + } + + return nextResponse; + } + + private runErrorInterceptors(error: unknown): never { + let nextError = error; + + for (const interceptor of this.errorInterceptors) { + try { + interceptor(nextError); + } catch (handledError) { + nextError = handledError; + } + } + + throw nextError instanceof Error + ? nextError + : new Error("Network request failed."); + } + + private buildUrl(url: string, params?: QueryParams) { + const normalizedUrl = url.startsWith("http") + ? new URL(url) + : new URL(`${this.baseURL}${url}`, window.location.origin); + + if (params) { + Object.entries(params).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + + if (Array.isArray(value)) { + value.forEach((item) => { + if (item !== undefined && item !== null) { + normalizedUrl.searchParams.append(key, String(item)); + } + }); + return; + } + + normalizedUrl.searchParams.set(key, String(value)); + }); + } + + return normalizedUrl.toString(); + } + + private serializeBody(body: unknown): BodyInit | undefined { + if (body === undefined || body === null) { + return undefined; + } + + if ( + typeof body === "string" || + body instanceof FormData || + body instanceof URLSearchParams || + body instanceof Blob + ) { + return body; + } + + return JSON.stringify(body); + } +} + +const httpClient = new HttpClient(); + +export default httpClient; diff --git a/src/App/src/chartComponents/DonutChart.tsx b/src/App/src/chartComponents/DonutChart.tsx index 75e4943f4..8c42610a4 100644 --- a/src/App/src/chartComponents/DonutChart.tsx +++ b/src/App/src/chartComponents/DonutChart.tsx @@ -49,12 +49,13 @@ const DonutChart: React.FC = ({ const donutWidthAndHeight = (containerHeight > containerWidth ? containerWidth : containerHeight) / 1.7; - const width = donutWidthAndHeight; // Set width equal to containerHeight for a square layout + const width = donutWidthAndHeight; const radius = width / 2; const svgHeight = donutWidthAndHeight + 40; const svgWidth = width + 40; + const chartElement = chartRef.current; const svg = d3 - .select(chartRef.current) + .select(chartElement) .attr("width", svgWidth) .attr("height", svgHeight + 8) .append("g") @@ -88,7 +89,9 @@ const DonutChart: React.FC = ({ }); return () => { - d3.select(chartRef.current).selectAll("*").remove(); + if (chartElement) { + d3.select(chartElement).selectAll("*").remove(); + } }; }, [data, containerHeight, containerID]); diff --git a/src/App/src/chartComponents/HorizontalBarChart.tsx b/src/App/src/chartComponents/HorizontalBarChart.tsx index c1d286a45..3662d342a 100644 --- a/src/App/src/chartComponents/HorizontalBarChart.tsx +++ b/src/App/src/chartComponents/HorizontalBarChart.tsx @@ -142,7 +142,7 @@ const BarChart: React.FC = ({ .style("font-size", "12px") .text(yLabel); } - }, [data, title, yLabel, containerHeight]); + }, [containerHeight, containerID, data, title, yLabel]); return (
diff --git a/src/App/src/chartComponents/WordCloudChart.tsx b/src/App/src/chartComponents/WordCloudChart.tsx index 49ae879bd..692a2d813 100644 --- a/src/App/src/chartComponents/WordCloudChart.tsx +++ b/src/App/src/chartComponents/WordCloudChart.tsx @@ -29,10 +29,14 @@ const WordCloudChart: React.FC = ({ height: containerHeight, }); - const currentWidth = useRef(widthInPixels); - const [wordsUpdatedFlag, setWordsUpdatedFlag] = useState(true); + const wordsSignature = useMemo( + () => + data.words + .map((word) => `${word.text}-${word.size}-${word.average_sentiment}`) + .join(","), + [data.words] + ); - // Observe container size changes dynamically useEffect(() => { const resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { @@ -41,21 +45,18 @@ const WordCloudChart: React.FC = ({ } }); - if (containerRef.current) { - resizeObserver.observe(containerRef.current); + const containerElement = containerRef.current; + if (containerElement) { + resizeObserver.observe(containerElement); } return () => { - if (containerRef.current) { - resizeObserver.unobserve(containerRef.current); + if (containerElement) { + resizeObserver.unobserve(containerElement); } }; }, []); - useMemo(() => { - setWordsUpdatedFlag((prev) => !prev); - }, [data.words.map((o) => o.text).join(",")]); - useEffect(() => { const HEIGHT_OFFSET = 30; const margin = { top: 10, right: 10, bottom: 10, left: 10 }; @@ -136,7 +137,7 @@ const WordCloudChart: React.FC = ({ ) .text((d) => d.text); } - }, [wordsUpdatedFlag, dimensions]); + }, [data.words, dimensions, wordsSignature]); return (
diff --git a/src/App/src/components/Chart/Chart.tsx b/src/App/src/components/Chart/Chart.tsx index 2e440dd25..058c77f72 100644 --- a/src/App/src/components/Chart/Chart.tsx +++ b/src/App/src/components/Chart/Chart.tsx @@ -1,4 +1,4 @@ -import { useEffect, useLayoutEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { fetchChartData, fetchChartDataWithFilters, @@ -15,53 +15,62 @@ import ChartFilter from "../ChartFilter/ChartFilter"; import "./Chart.css"; import { type ChartConfigItem, - SelectedFilters, type FilterMetaData, + type SelectedFilters, } from "../../types/AppTypes"; -import { useAppContext } from "../../state/useAppContext"; -import { actionConstants } from "../../state/ActionConstants"; +import { useAppDispatch, useAppSelector } from "../../state/hooks"; +import { + setChartsData, + setFetchingCharts, + setFetchingFilters, + setFiltersMeta, + setFiltersMetaFetched, + setInitialChartsDataFetched, +} from "../../state/slices/dashboardSlice"; import { ACCEPT_FILTERS, defaultSelectedFilters, getGridStyles, } from "../../configs/Utils"; -// import { ChartsResponse } from "../../configs/StaticData"; import { Subtitle2, Tag } from "@fluentui/react-components"; import { Spinner, SpinnerSize } from "@fluentui/react"; -// import { ChartsResponse } from "../../configs/StaticData"; +import { getSentimentColor } from "../../utils/chartUtils"; type ChartProps = { layoutWidthUpdated: boolean; }; -const Chart = (props: ChartProps) => { - const { state, dispatch } = useAppContext(); - const { - charts, - fetchingCharts, - fetchingFilters, - filtersMetaFetched, - initialChartsDataFetched, - } = state.dashboards; - const { config: layoutConfig } = state; - const { layoutWidthUpdated } = props; +const Chart = ({ layoutWidthUpdated }: ChartProps) => { + const dispatch = useAppDispatch(); + const charts = useAppSelector((state) => state.dashboards.charts); + const fetchingCharts = useAppSelector( + (state) => state.dashboards.fetchingCharts + ); + const fetchingFilters = useAppSelector( + (state) => state.dashboards.fetchingFilters + ); + const filtersMetaFetched = useAppSelector( + (state) => state.dashboards.filtersMetaFetched + ); + const initialChartsDataFetched = useAppSelector( + (state) => state.dashboards.initialChartsDataFetched + ); + const configCharts = useAppSelector((state) => state.app.config.charts); - const [widths, setWidths] = useState>({}); const [appliedFetch, setAppliedFetch] = useState(false); - const [widgetsGapInPercentage, setWidgetsGapInPercentage] = - useState(1); - - const [windowSize, setWindowSize] = useState({ + const [widgetsGapInPercentage] = useState(1); + const fallbackChartWidthInPixels = 300; + const [, setWindowSize] = useState({ width: window.innerWidth, height: window.innerHeight, }); - const handleResize = () => { + const handleResize = useCallback(() => { setWindowSize({ width: window.innerWidth, height: window.innerHeight, }); - }; + }, []); useEffect(() => { requestAnimationFrame(() => { @@ -79,146 +88,121 @@ const Chart = (props: ChartProps) => { return () => { window.removeEventListener("resize", handleResize); }; - }, []); + }, [handleResize]); - const getChartData = async (reqBody: any) => { - dispatch({ - type: actionConstants.UPDATE_CHARTS_FETCHING_FLAG, - payload: true, - }); - if (String(reqBody?.Sentiment?.[0]).toLowerCase() === "all") { - reqBody.Sentiment = []; - } - try { - let chartData: any; - if (reqBody) { - chartData = await fetchChartDataWithFilters({ - selected_filters: reqBody, - }); - } else { - chartData = await fetchChartData(); + const getChartData = useCallback( + async (requestBody?: SelectedFilters) => { + dispatch(setFetchingCharts(true)); + const normalizedRequestBody = requestBody + ? { ...requestBody } + : undefined; + + if ( + String((normalizedRequestBody as any)?.Sentiment?.[0]).toLowerCase() === + "all" + ) { + (normalizedRequestBody as any).Sentiment = []; } - // Update charts with data - const updatedCharts: ChartConfigItem[] = layoutConfig.charts - .map((configChart: any) => { - if (!configChart || !configChart.id) { - console.warn( - "Config chart or config chart name is undefined:", - configChart + + try { + const chartData = normalizedRequestBody + ? await fetchChartDataWithFilters({ + selected_filters: normalizedRequestBody, + }) + : await fetchChartData(); + + const updatedCharts: ChartConfigItem[] = configCharts + .map((configChart: any) => { + if (!configChart?.id) { + return null; + } + + const apiData = chartData.find( + (apiChart: any) => + apiChart.id?.toLowerCase() === configChart.id?.toLowerCase() ); - return null; // Skip this chart if the name is missing - } - - const apiData = chartData.find( - (apiChart: any) => - apiChart.id?.toLowerCase() === configChart?.id?.toLowerCase() - ); - const configObj = { - id: configChart?.id, - domId: configChart?.id.replace(/\s+/g, "_").toUpperCase(), - type: configChart?.type, - title: apiData ? apiData?.chart_name : configChart.name || "", - data: apiData ? apiData?.chart_value : [], - layout: { - row: configChart?.layout?.row, - col: configChart?.layout?.column, - ...configChart?.layout, - }, - } as ChartConfigItem; - if (configChart?.layout?.width) { - configObj.layout.width = configChart?.layout?.width; - } - return configObj; - }) - .filter((chart): chart is ChartConfigItem => chart !== null); - dispatch({ - type: actionConstants.UPDATE_CHARTS_DATA, - payload: updatedCharts, - }); - dispatch({ - type: actionConstants.UPDATE_CHARTS_FETCHING_FLAG, - payload: false, - }); - } catch (e) { - dispatch({ - type: actionConstants.UPDATE_CHARTS_FETCHING_FLAG, - payload: false, - }); - console.log("Error while fetching charts data", e); - } - }; - // Fetch chart data and filters + const configObject: ChartConfigItem = { + id: configChart.id, + domId: configChart.id.replace(/\s+/g, "_").toUpperCase(), + type: configChart.type, + title: apiData ? apiData.chart_name : configChart.name || "", + data: apiData ? apiData.chart_value : [], + layout: { + row: configChart.layout?.row, + col: configChart.layout?.column, + ...configChart.layout, + }, + }; + + if (configChart.layout?.width) { + configObject.layout.width = configChart.layout.width; + } + + return configObject; + }) + .filter((chart): chart is ChartConfigItem => chart !== null); + + dispatch(setChartsData(updatedCharts)); + } catch { + dispatch(setChartsData([])); + } finally { + dispatch(setFetchingCharts(false)); + } + }, + [configCharts, dispatch] + ); + useEffect(() => { const loadData = async () => { try { if (!filtersMetaFetched) { - dispatch({ - type: actionConstants.UPDATE_FILTERS_FETCHING_FLAG, - payload: true, - }); + dispatch(setFetchingFilters(true)); const filterResponse = await fetchFilterData(); const acceptedFilters: FilterMetaData = {}; - filterResponse?.forEach((obj: any) => { - if (ACCEPT_FILTERS.includes(obj?.filter_name)) { - const { filter_name, filter_values } = obj; - acceptedFilters[filter_name] = filter_values; + + filterResponse?.forEach((filter: any) => { + if (ACCEPT_FILTERS.includes(filter?.filter_name)) { + acceptedFilters[filter.filter_name] = filter.filter_values; } }); - dispatch({ - type: actionConstants.SET_FILTERS, - payload: acceptedFilters, - }); - dispatch({ - type: actionConstants.UPDATE_FILTERS_FETCHED_FLAG, - payload: true, - }); - dispatch({ - type: actionConstants.UPDATE_FILTERS_FETCHING_FLAG, - payload: false, - }); + + dispatch(setFiltersMeta(acceptedFilters)); + dispatch(setFiltersMetaFetched(true)); + dispatch(setFetchingFilters(false)); } + if (!initialChartsDataFetched) { await getChartData({ ...defaultSelectedFilters }); - dispatch({ - type: actionConstants.UPDATE_INITIAL_CHARTS_FETCHED_FLAG, - payload: true, - }); + dispatch(setInitialChartsDataFetched(true)); } - } catch (error) { - console.error("Error loading data:", error); - dispatch({ type: actionConstants.UPDATE_CHARTS_DATA, payload: [] }); - dispatch({ - type: actionConstants.UPDATE_FILTERS_FETCHING_FLAG, - payload: false, - }); + } catch { + dispatch(setChartsData([])); + dispatch(setFetchingFilters(false)); } }; - if (state.config.charts.length > 0) { - loadData(); + + if (configCharts.length > 0) { + void loadData(); } - }, [state.config.charts]); + }, [ + configCharts.length, + dispatch, + filtersMetaFetched, + getChartData, + initialChartsDataFetched, + ]); - const applyFilters = async (updatedFilters: SelectedFilters) => { - setAppliedFetch(true); - await getChartData(updatedFilters); - setAppliedFetch(false); - }; + const applyFilters = useCallback( + async (updatedFilters: SelectedFilters) => { + setAppliedFetch(true); + await getChartData(updatedFilters); + setAppliedFetch(false); + }, + [getChartData] + ); const renderChart = (chart: ChartConfigItem, heightInPixels: number) => { - const getColorForLabel = (label: string): string => { - switch (label) { - case "positive": - return "#6576F9"; // Blue - case "neutral": - return "#B2BBFC"; // Light Blue - case "negative": - return "#FF749B"; // Red - default: - return "#ccc"; // Default color - } - }; - const hasData = chart.data && chart.data.length > 0; switch (chart.type) { @@ -240,11 +224,14 @@ const Chart = (props: ChartProps) => { data={chart.data.map((item) => ({ label: item.name, value: parseInt(item.value) || 0, - color: getColorForLabel(item.name.toLowerCase()), + color: getSentimentColor(item.name.toLowerCase()), }))} containerHeight={heightInPixels} - widthInPixels={document?.getElementById(chart?.domId)!?.clientWidth} - containerID={chart?.domId} + widthInPixels={ + document.getElementById(chart.domId)?.clientWidth ?? + fallbackChartWidthInPixels + } + containerID={chart.domId} /> ) : (
{ value: parseFloat(item.value), }))} containerHeight={heightInPixels} - containerID={chart?.domId} + containerID={chart.domId} /> ) : (
{ average_sentiment: item.average_sentiment, })), }} - widthInPixels={document?.getElementById(chart?.domId)!?.clientWidth} + widthInPixels={ + document.getElementById(chart.domId)?.clientWidth ?? + fallbackChartWidthInPixels + } containerHeight={heightInPixels} /> ) : ( @@ -324,41 +314,28 @@ const Chart = (props: ChartProps) => {
); default: - console.warn(`Unknown chart type: ${chart.type}`); return null; } }; - useLayoutEffect(() => { - const updateWidths = () => { - const newWidths: Record = {}; - charts.forEach((chartObj) => { - const element = document.getElementById(chartObj?.domId); - if (element) { - newWidths[chartObj?.domId] = element!?.clientWidth; - } - }); - setWidths(newWidths); - }; - return updateWidths(); - }, [charts, windowSize.height, windowSize.width, layoutWidthUpdated]); - const getHeightInPixels = (vh: number) => (vh / 100) * window.innerHeight; - const groupedByRows: any = {}; - charts.forEach((obj) => { - const rowValue = String(obj?.layout?.row); + const groupedByRows: Record = {}; + charts.forEach((chart) => { + const rowValue = String(chart.layout?.row); if (!groupedByRows[rowValue]) { groupedByRows[rowValue] = []; } - groupedByRows[rowValue].push(obj); + groupedByRows[rowValue].push(chart); }); + const showAIGeneratedContentMessage = (!fetchingCharts && !fetchingFilters) || appliedFetch; + return ( <> {fetchingCharts && !appliedFetch ? ( -
+
Loading Please wait...
@@ -366,23 +343,24 @@ const Chart = (props: ChartProps) => {
- {Object.values(groupedByRows).map((chartsList: any, index) => { + {Object.values(groupedByRows).map((chartsList, index) => { const gridStyles = getGridStyles( [...chartsList], widgetsGapInPercentage ); let heightInPixels = 240; - if (gridStyles.gridTemplateRows) { - if (!isNaN(parseInt(gridStyles.gridTemplateRows))) { - const heightInVH = parseInt(gridStyles.gridTemplateRows); - heightInPixels = getHeightInPixels(heightInVH); - } + + if ( + gridStyles.gridTemplateRows && + !Number.isNaN(parseInt(gridStyles.gridTemplateRows)) + ) { + const heightInVH = parseInt(gridStyles.gridTemplateRows); + heightInPixels = getHeightInPixels(heightInVH); } + return (
{ style={{ ...gridStyles, gridGap: `${widgetsGapInPercentage}%` }} > {chartsList - .sort( - (a: ChartConfigItem, b: ChartConfigItem) => - a?.layout.col - b?.layout?.col - ) - .map((chart: any) => ( + .sort((a, b) => a.layout.col - b.layout.col) + .map((chart) => (
- {/*
{chart.title}
*/} {chart.title} diff --git a/src/App/src/components/ChartFilter/ChartFilter.tsx b/src/App/src/components/ChartFilter/ChartFilter.tsx index 16c02402f..f1cfe2f32 100644 --- a/src/App/src/components/ChartFilter/ChartFilter.tsx +++ b/src/App/src/components/ChartFilter/ChartFilter.tsx @@ -1,8 +1,7 @@ -import React, { useMemo, useState } from "react"; +import React, { useCallback, useMemo, useState } from "react"; import { Stack, DefaultButton, - PrimaryButton, DirectionalHint, IContextualMenuListProps, IContextualMenuItem, @@ -14,8 +13,8 @@ import { import "./ChartFilter.css"; import { type SelectedFilters } from "../../types/AppTypes"; import { defaultSelectedFilters, sentimentIcons } from "../../configs/Utils"; -import { useAppContext } from "../../state/useAppContext"; -import { actionConstants } from "../../state/ActionConstants"; +import { useAppDispatch, useAppSelector } from "../../state/hooks"; +import { setSelectedFilters as setSelectedDashboardFilters } from "../../state/slices/dashboardSlice"; import { ArrowClockwise20Regular, CalendarLtr20Regular, @@ -32,10 +31,12 @@ interface FilterComponentProps { } const ChartFilter: React.FC = (props) => { - const { state, dispatch } = useAppContext(); - const { selectedFilters, filtersMeta } = state.dashboards; + const dispatch = useAppDispatch(); + const { selectedFilters, filtersMeta } = useAppSelector( + (state) => state.dashboards + ); const { applyFilters, fetchingCharts } = props; - const initialDateRange = typeof Array.isArray(selectedFilters.DateRange) + const initialDateRange = Array.isArray(selectedFilters.DateRange) ? selectedFilters.DateRange : [""]; @@ -91,10 +92,7 @@ const ChartFilter: React.FC = (props) => { updatedFilters.Sentiment = selectedCsat; updatedFilters.DateRange = startDate; applyFilters(updatedFilters); - dispatch({ - type: actionConstants.UPDATE_SELECTED_FILTERS, - payload: updatedFilters, - }); + dispatch(setSelectedDashboardFilters(updatedFilters)); }; const handleResetFilters = () => { @@ -116,7 +114,7 @@ const ChartFilter: React.FC = (props) => { }; const onTopicsMenuOpen = () => { - setSearchQuery(""); // Clear the search query when the menu opens + setSearchQuery(""); setTimeout(() => { const element = document.getElementById("SEARCH_TOPICS"); if (element) { @@ -124,48 +122,50 @@ const ChartFilter: React.FC = (props) => { } }, 100); }; - const renderMenuList: IRenderFunction = ( - menuListProps, - defaultRender - ) => ( -
- -
- {defaultRender ? defaultRender(menuListProps) : null} -
- { - setSearchQuery(""); - }} // Clear search query when the "X" is clicked - onKeyUp={(e) => { - onSearchChange(e, (e.target as HTMLInputElement).value); - }} - iconProps={{ iconName: "Search" }} - styles={{ root: { margin: "8px" } }} - id="SEARCH_TOPICS" - showIcon - autoComplete="off" - autoFocus - /> -
- ); - const handleDeselectAll = (ev: React.MouseEvent) => { + const handleDeselectAll = useCallback((ev: React.MouseEvent) => { ev.preventDefault(); - setSelectedTopics([]); // Deselect all items - }; + setSelectedTopics([]); + }, []); + + const renderMenuList: IRenderFunction = useCallback( + (menuListProps, defaultRender) => ( +
+ +
+ {defaultRender ? defaultRender(menuListProps) : null} +
+ { + setSearchQuery(""); + }} + onKeyUp={(e) => { + onSearchChange(e, (e.target as HTMLInputElement).value); + }} + iconProps={{ iconName: "Search" }} + styles={{ root: { margin: "8px" } }} + id="SEARCH_TOPICS" + showIcon + autoComplete="off" + autoFocus + /> +
+ ), + [handleDeselectAll, selectedTopics.length] + ); const topicMenuProps = useMemo( () => ({ @@ -193,7 +193,7 @@ const ChartFilter: React.FC = (props) => { directionalHint: DirectionalHint.bottomLeftEdge, onMenuOpened: () => onTopicsMenuOpen(), }), - [filteredTopics, selectedTopics] + [filteredTopics, renderMenuList, selectedTopics] ); return ( diff --git a/src/App/src/components/Chat/Chat.css b/src/App/src/components/Chat/Chat.css index c5196c0e5..875d758c0 100644 --- a/src/App/src/components/Chat/Chat.css +++ b/src/App/src/components/Chat/Chat.css @@ -22,8 +22,6 @@ #eef6fe 100% ); overflow-y: auto; - /* border-bottom-left-radius: 0px; - border-top-left-radius: 0px; */ } .textarea-field { @@ -147,8 +145,6 @@ } .chat-header { - /* font-size: 1rem; - font-weight: 600; */ height: 6vh; display: flex; align-items: center; @@ -169,18 +165,14 @@ row-gap: 8px; width: 73%; text-align: center; - /* font-size: 1rem; */ } .initial-msg > span:first-of-type { color: #707070; - /* font-weight: 500; */ } .initial-msg > span:nth-of-type(2) { color: var(--colorNeutralForeground2); - /* font-weight: 400; - font-size: 0.875em; */ } @@ -270,11 +262,6 @@ /* Extra Large screens (≥1200px) */ @media (min-width: 1200px) { - .chat-header, - .initial-msg, - .chat-input { - /* font-size: 0.9rem; */ - } :root { --msg-block-font: 0.7rem; --answer-disclaimer-font: 0.6rem; @@ -287,11 +274,6 @@ /* Extra Extra Large screens (≥1400px) */ @media (min-width: 1400px) { - .chat-header, - .initial-msg, - .chat-input { - /* font-size: 1.1rem; */ - } .chat-input { padding: 0.75rem; margin: 0.75rem; @@ -308,11 +290,6 @@ /* Very Extra Large screens (≥1600px) */ @media (min-width: 1600px) { - .chat-header, - .initial-msg, - .chat-input { - /* font-size: 1.2rem; */ - } :root { --msg-block-font: 1rem; --answer-disclaimer-font: 0.75rem; diff --git a/src/App/src/components/Chat/Chat.tsx b/src/App/src/components/Chat/Chat.tsx index d6f1b8489..7c7ff8eef 100644 --- a/src/App/src/components/Chat/Chat.tsx +++ b/src/App/src/components/Chat/Chat.tsx @@ -1,33 +1,20 @@ -import React, { useEffect, useRef, useState } from "react"; -import { - Button, - Textarea, - Subtitle2, - Subtitle1, - Body1, - Title3, -} from "@fluentui/react-components"; -import "./Chat.css"; -import { SparkleRegular } from "@fluentui/react-icons"; -import ReactMarkdown from "react-markdown"; -import remarkGfm from "remark-gfm"; -import supersub from "remark-supersub"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { Body1, Button, Subtitle2, Textarea } from "@fluentui/react-components"; import { DefaultButton, Spinner, SpinnerSize } from "@fluentui/react"; -import { useAppContext } from "../../state/useAppContext"; -import { actionConstants } from "../../state/ActionConstants"; -import { - type ChartDataResponse, - type Conversation, - type ConversationRequest, - type ParsedChunk, - type ChatMessage, - ToolMessageContent, -} from "../../types/AppTypes"; -import { callConversationApi, getIsChartDisplayDefault, historyUpdate } from "../../api/api"; import { ChatAdd24Regular } from "@fluentui/react-icons"; -import { generateUUIDv4 } from "../../configs/Utils"; -import ChatChart from "../ChatChart/ChatChart"; -import Citations from "../Citations/Citations"; +import "./Chat.css"; +import { getIsChartDisplayDefault } from "../../api/api"; +import { useAppDispatch, useAppSelector } from "../../state/hooks"; +import { setUserMessage } from "../../state/slices/chatSlice"; +import { useAutoScroll } from "../../hooks/useAutoScroll"; +import { useChatApi } from "../../hooks/useChatApi"; +import ChatMessageItem from "./ChatMessageItem"; type ChatProps = { onHandlePanelStates: (name: string) => void; @@ -35,677 +22,105 @@ type ChatProps = { panelShowStates: Record; }; -const [ASSISTANT, TOOL, ERROR, USER] = ["assistant", "tool", "error", "user"]; -const NO_CONTENT_ERROR = "No content in messages object."; - const Chat: React.FC = ({ onHandlePanelStates, panelShowStates, panels, }) => { - const { state, dispatch } = useAppContext(); - const { userMessage, generatingResponse } = state?.chat; - const questionInputRef = useRef(null); - const [isChartLoading, setIsChartLoading] = useState(false) - const abortFuncs = useRef([] as AbortController[]); - const chatMessageStreamEnd = useRef(null); - const [isCharthDisplayDefault , setIsCharthDisplayDefault] = useState(false); - - useEffect(() => { - try { - const fetchIsChartDisplayDefault = async () => { - const chartConfigFlag = await getIsChartDisplayDefault(); - setIsCharthDisplayDefault(chartConfigFlag.isChartDisplayDefault); - }; - fetchIsChartDisplayDefault(); - } catch (error) { - console.error("Failed to fetch isChartDisplayDefault flag", error); - } - }, []); - - const saveToDB = async (newMessages: ChatMessage[], convId: string, reqType: string = 'Text') => { - if (!convId || !newMessages.length) { - return; - } - const isNewConversation = reqType !== 'graph' ? !state.selectedConversationId : false; - dispatch({ - type: actionConstants.UPDATE_HISTORY_UPDATE_API_FLAG, - payload: true, - }); - - if (((reqType !== 'graph' && reqType !== 'error') && newMessages[newMessages.length - 1].role !== ERROR) && isCharthDisplayDefault ){ - setIsChartLoading(true); - setTimeout(()=>{ - makeApiRequestForChart('show in a graph by default', convId, newMessages[newMessages.length - 1].content as string) - },5000) - - } - await historyUpdate(newMessages, convId) - .then(async (res) => { - if (!res.ok) { - if (!messages) { - let err: Error = { - ...new Error(), - message: "Failure fetching current chat state.", - }; - throw err; - } - } - let responseJson = await res.json(); - if (isNewConversation && responseJson?.success) { - const newConversation: Conversation = { - id: responseJson?.data?.conversation_id, - title: responseJson?.data?.title, - messages: state.chat.messages, - date: responseJson?.data?.date, - updatedAt: responseJson?.data?.date, - }; - dispatch({ - type: actionConstants.ADD_NEW_CONVERSATION_TO_CHAT_HISTORY, - payload: newConversation, - }); - dispatch({ - type: actionConstants.UPDATE_SELECTED_CONV_ID, - payload: responseJson?.data?.conversation_id, - }); - } - dispatch({ - type: actionConstants.UPDATE_HISTORY_UPDATE_API_FLAG, - payload: false, - }); - return res as Response; - }) - .catch((err) => { - console.error("Error: while saving data", err); - }) - .finally(() => { - dispatch({ - type: actionConstants.UPDATE_GENERATING_RESPONSE_FLAG, - payload: false, - }); - dispatch({ - type: actionConstants.UPDATE_HISTORY_UPDATE_API_FLAG, - payload: false, - }); - }); - }; - - - const parseCitationFromMessage = (message : any) => { - - try { - message = '{'+ message - const toolMessage = JSON.parse(message as string) as ToolMessageContent; - - return toolMessage.citations; - } catch { - // Silently ignore parse errors for incomplete JSON chunks. This is expected during streaming - } - return []; - }; - const isChartQuery = (query: string) => { - const chartKeywords = ["chart", "graph", "visualize", "plot"]; - return chartKeywords.some((keyword) => - query.toLowerCase().includes(keyword) - ); - }; - - useEffect(() => { - if (state.chat.generatingResponse || state.chat.isStreamingInProgress) { - const chatAPISignal = abortFuncs.current.shift(); - if (chatAPISignal) { - console.log("chatAPISignal", chatAPISignal); - chatAPISignal.abort( - "Chat Aborted due to switch to other conversation while generating" - ); - } - } - }, [state.selectedConversationId]); - - useEffect(() => { - if ( - !state.chatHistory.isFetchingConvMessages && - chatMessageStreamEnd.current - ) { - setTimeout(() => { - chatMessageStreamEnd.current?.scrollIntoView({ behavior: "auto" }); - }, 100); - } - }, [state.chatHistory.isFetchingConvMessages]); + const dispatch = useAppDispatch(); + const userMessage = useAppSelector((state) => state.chat.userMessage); + const messages = useAppSelector((state) => state.chat.messages); + const generatingResponse = useAppSelector( + (state) => state.chat.generatingResponse + ); + const isStreamingInProgress = useAppSelector( + (state) => state.chat.isStreamingInProgress + ); + const isFetchingConvMessages = useAppSelector( + (state) => state.chatHistory.isFetchingConvMessages + ); + const isHistoryUpdateAPIPending = useAppSelector( + (state) => state.chatHistory.isHistoryUpdateAPIPending + ); - const scrollChatToBottom = () => { - if (chatMessageStreamEnd.current) { - setTimeout(() => { - chatMessageStreamEnd.current?.scrollIntoView({ behavior: "smooth" }); - }, 100); - } - }; + const questionInputRef = useRef(null); + const [isChartLoading, setIsChartLoading] = useState(false); + const [isChartDisplayDefault, setIsChartDisplayDefault] = useState(false); useEffect(() => { - scrollChatToBottom(); - }, [state.chat.generatingResponse]); - - const makeApiRequestForChart = async ( - question: string, - conversationId: string, - lrg: string - ) => { - if (generatingResponse || !question.trim()) { - return; - } - - const newMessage: ChatMessage = { - id: generateUUIDv4(), - role: "user", - content: question, - date: new Date().toISOString() - }; - dispatch({ - type: actionConstants.UPDATE_GENERATING_RESPONSE_FLAG, - payload: true, - }); - scrollChatToBottom(); - dispatch({ - type: actionConstants.UPDATE_MESSAGES, - payload: [newMessage], - }); - dispatch({ - type: actionConstants.UPDATE_USER_MESSAGE, - payload: questionInputRef?.current?.value || "", - }); - const abortController = new AbortController(); - abortFuncs.current.unshift(abortController); - - const request: ConversationRequest = { - id: conversationId, - query: question - }; - - const streamMessage: ChatMessage = { - id: generateUUIDv4(), - date: new Date().toISOString(), - role: ASSISTANT, - content: "", - }; - let updatedMessages: ChatMessage[] = []; - try { - const response = await callConversationApi( - request, - abortController.signal - ); - - - if (response?.body) { - let isChartResponseReceived = false; - const reader = response.body.getReader(); - let runningText = ""; - let hasError = false; - while (true) { - const { done, value } = await reader.read(); - if (done) break; - const text = new TextDecoder("utf-8").decode(value); - try { - const textObj = JSON.parse(text); - if (textObj?.object?.data) { - runningText = text; - isChartResponseReceived = true; - } - if (textObj?.error) { - hasError = true; - runningText = text; - } - } catch (e) { - // Silently ignore parse errors for incomplete JSON chunks. This is expected during streaming - } + let isMounted = true; + const loadChartDisplayPreference = async () => { + try { + const chartConfigFlag = await getIsChartDisplayDefault(); + if (isMounted) { + setIsChartDisplayDefault(chartConfigFlag.isChartDisplayDefault); } - // END OF STREAMING - if (hasError) { - const errorMsg = JSON.parse(runningText).error; - const errorMessage: ChatMessage = { - id: generateUUIDv4(), - role: ERROR, - content: errorMsg, - date: new Date().toISOString(), - }; - updatedMessages = [newMessage, errorMessage]; - dispatch({ - type: actionConstants.UPDATE_MESSAGES, - payload: [errorMessage], - }); - scrollChatToBottom(); - } else if (isChartQuery(question)) { - try { - const parsedChartResponse = JSON.parse(runningText); - if ( - "object" in parsedChartResponse && - parsedChartResponse?.object?.type && - parsedChartResponse?.object?.data - ) { - // CHART CHECKING - try { - const chartMessage: ChatMessage = { - id: generateUUIDv4(), - role: ASSISTANT, - content: - parsedChartResponse.object as unknown as ChartDataResponse, - date: new Date().toISOString(), - }; - updatedMessages = [newMessage, chartMessage]; - // Update messages with the response content - dispatch({ - type: actionConstants.UPDATE_MESSAGES, - payload: [chartMessage], - }); - scrollChatToBottom(); - } catch (e) { - console.error("Error handling assistant response:", e); - const chartMessage: ChatMessage = { - id: generateUUIDv4(), - role: ASSISTANT, - content: "Error while generating Chart.", - date: new Date().toISOString(), - }; - updatedMessages = [newMessage, chartMessage]; - dispatch({ - type: actionConstants.UPDATE_MESSAGES, - payload: [chartMessage], - }); - scrollChatToBottom(); - } - } else if ( - parsedChartResponse.error - ) { - const errorMsg = - parsedChartResponse.error || - parsedChartResponse?.object?.message; - const errorMessage: ChatMessage = { - id: generateUUIDv4(), - role: ERROR, - content: errorMsg, - date: new Date().toISOString(), - }; - updatedMessages = [ - ...state.chat.messages, - newMessage, - errorMessage, - ]; - dispatch({ - type: actionConstants.UPDATE_MESSAGES, - payload: [errorMessage], - }); - scrollChatToBottom(); - } - } catch (e) { - // Silently ignore parse errors for incomplete JSON chunks for chart response. This is expected during streaming - } - } - } - saveToDB(updatedMessages, conversationId, 'graph'); - } catch (e) { - console.log("Caught with an error while chat and save", e); - if (abortController.signal.aborted) { - if (streamMessage.content) { - updatedMessages = [newMessage, streamMessage]; - } else { - updatedMessages = [newMessage]; - } - console.log( - "@@@ Abort Signal detected: Formed updated msgs", - updatedMessages - ); - saveToDB(updatedMessages, conversationId, 'graph'); - } - - if (!abortController.signal.aborted) { - if (e instanceof Error) { - alert(e.message); - } else { - alert( - "An error occurred. Please try again. If the problem persists, please contact the site administrator." - ); + } catch { + if (isMounted) { + setIsChartDisplayDefault(false); } } - } finally { - - - dispatch({ - type: actionConstants.UPDATE_GENERATING_RESPONSE_FLAG, - payload: false, - }); - dispatch({ - type: actionConstants.UPDATE_STREAMING_FLAG, - payload: false, - }); - setIsChartLoading(false); - } - return abortController.abort(); - }; - - const makeApiRequestWithCosmosDB = async ( - question: string, - conversationId: string - ) => { - if (generatingResponse || !question.trim()) { - return; - } - const isChatReq = isChartQuery(userMessage) ? "graph" : "Text" - const newMessage: ChatMessage = { - id: generateUUIDv4(), - role: "user", - content: question, - date: new Date().toISOString(), }; - dispatch({ - type: actionConstants.UPDATE_GENERATING_RESPONSE_FLAG, - payload: true, - }); - scrollChatToBottom(); - dispatch({ - type: actionConstants.UPDATE_MESSAGES, - payload: [newMessage], - }); - dispatch({ - type: actionConstants.UPDATE_USER_MESSAGE, - payload: "", - }); - const abortController = new AbortController(); - abortFuncs.current.unshift(abortController); - const request: ConversationRequest = { - id: conversationId, - query: question - }; + void loadChartDisplayPreference(); - const streamMessage: ChatMessage = { - id: generateUUIDv4(), - date: new Date().toISOString(), - role: ASSISTANT, - content: "", - citations:"", + return () => { + isMounted = false; }; - let updatedMessages: ChatMessage[] = []; - try { - const response = await callConversationApi( - request, - abortController.signal - ); - - if (response?.body) { - let isChartResponseReceived = false; - const reader = response.body.getReader(); - let runningText = ""; - let hasError = false; - while (true) { - const { done, value } = await reader.read(); - if (done) break; - const text = new TextDecoder("utf-8").decode(value); - try { - const textObj = JSON.parse(text); - if (textObj?.object?.data) { - runningText = text; - isChartResponseReceived = true; - } - if (textObj?.object?.message) { - runningText = text; - isChartResponseReceived = true; - } - if (textObj?.error) { - hasError = true; - runningText = text; - } - } catch (e) { - // Ignore - will process individual chunks after splitting - } - if (!isChartResponseReceived) { - //text based streaming response - const objects = text.split("\n").filter((val) => val !== ""); - let answerText=''; - let citationString =''; - let answerTextStart = 0; - objects.forEach((textValue, index) => { - try { - if (textValue !== "" && textValue !== "{}") { - const parsed: ParsedChunk = JSON.parse(textValue); - if (parsed?.error && !hasError) { - hasError = true; - runningText = parsed?.error; - } else if (isChartQuery(userMessage) && !hasError) { - runningText = runningText + textValue; - } else if (typeof parsed === "object" && !hasError) { - const responseContent = parsed?.choices?.[0]?.messages?.[0]?.content; - - const answerKey = `"answer":`; - const answerStartIndex = responseContent.indexOf(answerKey); - - if (answerStartIndex !== -1) { - answerTextStart =responseContent .indexOf(`"answer":`) +9; - } - - const citationsKey = `"citations":`; - const citationsStartIndex = responseContent.indexOf(citationsKey); - - if(citationsStartIndex > answerTextStart ){ - answerText = responseContent .substring(answerTextStart, citationsStartIndex).trim(); - citationString = responseContent .substring(citationsStartIndex).trim(); - }else{ - answerText = responseContent .substring(answerTextStart).trim(); - } + }, []); - answerText = answerText.replace(/^"+|"+$|,$/g, '');// first "" - answerText = answerText.replace(/[",]+$/, ''); // last ", - answerText = answerText.replace(/\\n/g, " \n"); - - - streamMessage.content = answerText || ""; - streamMessage.role = - parsed?.choices?.[0]?.messages?.[0]?.role || ASSISTANT; + const { chatMessageStreamEnd, scrollChatToBottom } = useAutoScroll(); + const { sendMessage, startNewChat } = useChatApi({ + scrollChatToBottom, + setIsChartLoading, + isChartDisplayDefault, + }); - streamMessage.citations = citationString; - dispatch({ - type: actionConstants.UPDATE_MESSAGE_BY_ID, - payload: streamMessage, - }); - scrollChatToBottom(); - } - } - } catch (e) { - // Skip incomplete JSON chunks in stream - } - }); - if (hasError) { - console.log("STOPPED DUE TO ERROR FROM API RESPONSE"); - break; - } - } - } - // END OF STREAMING - if (hasError) { - const errorMsg = JSON.parse(runningText).error === "Attempted to access streaming response content, without having called `read()`."?"An error occurred. Please try again later.": JSON.parse(runningText).error; - - const errorMessage: ChatMessage = { - id: generateUUIDv4(), - role: ERROR, - content: errorMsg, - date: new Date().toISOString(), - }; - updatedMessages = [newMessage, errorMessage]; - dispatch({ - type: actionConstants.UPDATE_MESSAGES, - payload: [errorMessage], - }); - scrollChatToBottom(); - } else if (isChartQuery(userMessage)) { - try { - const splitRunningText = runningText.split("}{"); - let parsedChartResponse: any = {}; - parsedChartResponse= JSON.parse("{" + splitRunningText[splitRunningText.length - 1]); - let chartResponse : any = {}; - try { - chartResponse = JSON.parse(parsedChartResponse?.choices[0]?.messages[0]?.content) - } catch (e) { - chartResponse = parsedChartResponse?.choices[0]?.messages[0]?.content; - } - - if (typeof chartResponse === 'object' && 'answer' in chartResponse) { - if ( - chartResponse.answer === "" || - chartResponse.answer === undefined || - (typeof chartResponse.answer === "object" && Object.keys(chartResponse.answer).length === 0) - ) { - chartResponse = "Chart can't be generated, please try again."; - } else { - chartResponse = chartResponse.answer; - } - } - - if ( - chartResponse?.type && - chartResponse?.data - ) { - // CHART CHECKING - try { - const chartMessage: ChatMessage = { - id: generateUUIDv4(), - role: ASSISTANT, - content: - chartResponse as unknown as ChartDataResponse, - date: new Date().toISOString(), - }; - updatedMessages = [newMessage, chartMessage]; - // Update messages with the response content - dispatch({ - type: actionConstants.UPDATE_MESSAGES, - payload: [chartMessage], - }); - scrollChatToBottom(); - } catch (e) { - console.error("Error handling assistant response:", e); - const chartMessage: ChatMessage = { - id: generateUUIDv4(), - role: ASSISTANT, - content: "Error while generating Chart.", - date: new Date().toISOString(), - }; - updatedMessages = [newMessage, chartMessage]; - dispatch({ - type: actionConstants.UPDATE_MESSAGES, - payload: [chartMessage], - }); - scrollChatToBottom(); - } - } else if ( - parsedChartResponse?.error || - parsedChartResponse?.choices[0]?.messages[0]?.content - ) { - let content = parsedChartResponse?.choices[0]?.messages[0]?.content; - let displayContent = content; - try { - const parsed = typeof content === "string" ? JSON.parse(content) : content; - if (parsed && typeof parsed === "object" && "answer" in parsed) { - displayContent = parsed.answer; - } - } catch { - displayContent = content; - } - const errorMsg = - parsedChartResponse?.error || - parsedChartResponse?.choices[0]?.messages[0]?.content - const errorMessage: ChatMessage = { - id: generateUUIDv4(), - role: ERROR, - content: errorMsg, - date: new Date().toISOString(), - }; - updatedMessages = [newMessage, errorMessage]; - dispatch({ - type: actionConstants.UPDATE_MESSAGES, - payload: [errorMessage], - }); - scrollChatToBottom(); - } - } catch (e) { - console.log("Error while parsing charts response", e); - } - } else if (!isChartResponseReceived) { - updatedMessages = [newMessage, streamMessage]; - } - } - if (updatedMessages[updatedMessages.length-1]?.role !== "error") { - saveToDB(updatedMessages, conversationId, isChatReq); - } - } catch (e) { - console.log("Caught with an error while chat and save", e); - if (abortController.signal.aborted) { - if (streamMessage.content) { - updatedMessages = [newMessage, streamMessage]; - } else { - updatedMessages = [newMessage]; - } - console.log( - "@@@ Abort Signal detected: Formed updated msgs", - updatedMessages - ); - saveToDB(updatedMessages, conversationId, 'error'); - } + useEffect(() => { + scrollChatToBottom("auto"); + }, [ + generatingResponse, + isFetchingConvMessages, + messages.length, + scrollChatToBottom, + ]); + + const isInputDisabled = useMemo( + () => generatingResponse || isHistoryUpdateAPIPending, + [generatingResponse, isHistoryUpdateAPIPending] + ); - if (!abortController.signal.aborted) { - if (e instanceof Error) { - alert(e.message); - } else { - alert( - "An error occurred. Please try again. If the problem persists, please contact the site administrator." - ); - } - } - } finally { - dispatch({ - type: actionConstants.UPDATE_GENERATING_RESPONSE_FLAG, - payload: false, - }); - dispatch({ - type: actionConstants.UPDATE_STREAMING_FLAG, - payload: false, - }); - + const handleSend = useCallback(() => { + if (userMessage.trim()) { + void sendMessage(userMessage); } - return abortController.abort(); - }; - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - const conversationId = - state?.selectedConversationId || state.generatedConversationId; - if (userMessage) { - makeApiRequestWithCosmosDB(userMessage, conversationId); - } - if (questionInputRef?.current) { - questionInputRef?.current.focus(); + questionInputRef.current?.focus(); + }, [sendMessage, userMessage]); + + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "Enter" && !event.shiftKey) { + event.preventDefault(); + handleSend(); } - } - }; + }, + [handleSend] + ); - const onClickSend = () => { - const conversationId = - state?.selectedConversationId || state.generatedConversationId; - if (userMessage) { - makeApiRequestWithCosmosDB(userMessage, conversationId); - } - if (questionInputRef?.current) { - questionInputRef?.current.focus(); - } - }; + const handleUserMessageChange = useCallback( + (_event: unknown, data: { value?: string }) => { + dispatch(setUserMessage(data.value || "")); + }, + [dispatch] + ); - const setUserMessage = (value: string) => { - dispatch({ type: actionConstants.UPDATE_USER_MESSAGE, payload: value }); - }; + const handleNewConversation = useCallback(() => { + startNewChat(); + questionInputRef.current?.focus(); + }, [startNewChat]); - const onNewConversation = () => { - dispatch({ type: actionConstants.NEW_CONVERSATION_START }); - dispatch({ type: actionConstants.UPDATE_CITATION,payload: { activeCitation: null, showCitation: false }}) - }; - const { messages, citations } = state.chat; return (
@@ -716,13 +131,12 @@ const Chat: React.FC = ({ onClick={() => onHandlePanelStates(panels.CHATHISTORY)} className="hide-chat-history" > - {`${panelShowStates?.[panels.CHATHISTORY] ? "Hide" : "Show" - } Chat History`} + {`${panelShowStates[panels.CHATHISTORY] ? "Hide" : "Show"} Chat History`}
- {Boolean(state.chatHistory?.isFetchingConvMessages) && ( + {isFetchingConvMessages && (
= ({ />
)} - {!Boolean(state.chatHistory?.isFetchingConvMessages) && - messages.length === 0 && ( -
- {/* */} -

- Start Chatting - - You can ask questions around customers calls, call topics and - call sentiments. - -
- )} - {!Boolean(state.chatHistory?.isFetchingConvMessages) && - messages.map((msg, index) => ( -
- {(() => { - const isLastAssistantMessage = - msg.role === "assistant" && index === messages.length - 1; - if ((msg.role === "user") && typeof msg.content === "string") { - if (msg.content == "show in a graph by default") return null; - return ( -
- {msg.content} -
- ); - - } - msg.content = msg.content as ChartDataResponse; - if (msg?.content?.type && msg?.content?.data) { - return ( -
- -
- - AI-generated content may be incorrect - -
-
- ); - } - if (typeof msg.content === "string") { - return ( -
- - {/* Citation Loader: Show only while citations are fetching */} - {isLastAssistantMessage && generatingResponse ? ( -
- - - -
- ) : ( - - )} - -
- - AI-generated content may be incorrect - -
-
- ); - } - })()} + {!isFetchingConvMessages && messages.length === 0 && ( +
+

+ Start Chatting + + You can ask questions around customers calls, call topics and + call sentiments. + +
+ )} + {!isFetchingConvMessages && + messages.map((message, index) => ( +
+
))} - {((generatingResponse && !state.chat.isStreamingInProgress) || isChartLoading) && ( + {((generatingResponse && !isStreamingInProgress) || isChartLoading) && (
- {isChartLoading ? "Generating chart if possible with the provided data" : "Generating answer"} + + {isChartLoading + ? "Generating chart if possible with the provided data" + : "Generating answer"} + @@ -826,17 +187,15 @@ const Chat: React.FC = ({ shape="circular" appearance="primary" icon={} - onClick={onNewConversation} + onClick={handleNewConversation} title="Create new Conversation" - disabled={ - generatingResponse || state.chatHistory.isHistoryUpdateAPIPending - } + disabled={isInputDisabled} />