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 @@

-6. Click on `+ Add a platform`.
+6. Click on `+ Add redirect URI`.

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''
+ f''
+ f'Bicep Parameter Validation Report '
+ f''
+ f'{_html_escape(accelerator_name) if accelerator_name else "Accelerator"}'
+ f' — Automated Check
'
+ f' '
+ )
+
+ # --- Summary card ---
+ parts.append(
+ f''
+ f''
+ f''
+ f''
+ f'{status_icon} Overall Status: {overall_status} '
+ f' '
+ f''
+ f''
+ )
+ # Accelerator name pill
+ if accelerator_name:
+ parts.append(
+ f''
+ f'Accelerator '
+ f'{_html_escape(accelerator_name)}'
+ f' '
+ )
+ # Scan directory pill
+ if scan_dir:
+ parts.append(
+ f''
+ f'Scan Directory '
+ f'{_html_escape(scan_dir)}/ '
+ f' '
+ )
+ # Error count pill
+ err_pill_color = "#D32F2F" if total_errors > 0 else "#2E7D32"
+ parts.append(
+ f''
+ f'Errors '
+ f''
+ f'{total_errors} '
+ )
+ # Warning count pill
+ warn_pill_color = "#F57C00" if total_warnings > 0 else "#2E7D32"
+ parts.append(
+ f''
+ f'Warnings '
+ f''
+ f'{total_warnings} '
+ )
+ parts.append("
")
+
+ # --- Per-pair detail sections ---
+ parts.append('')
+ 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''
+ f'{badge} '
+ f''
+ f'{_html_escape(r.pair)} '
+ f''
+ f'{len(errors)} error(s), {len(warnings)} warning(s) '
+ f' '
+ )
+
+ if r.issues:
+ # --- Errors section ---
+ if errors:
+ parts.append(
+ ''
+ ''
+ '● Errors '
+ ''
+ ''
+ ''
+ 'Parameter '
+ 'Details '
+ )
+ for idx, issue in enumerate(errors):
+ bg = "#ffffff" if idx % 2 == 0 else "#fff5f5"
+ parts.append(
+ f''
+ f''
+ f'{_html_escape(issue.param_name)} '
+ f'{_html_escape(issue.message)} '
+ f' '
+ )
+ parts.append("
")
+
+ # --- Warnings section ---
+ if warnings:
+ parts.append(
+ ''
+ ''
+ '● Warnings '
+ ''
+ ''
+ ''
+ 'Parameter '
+ 'Details '
+ )
+ for idx, issue in enumerate(warnings):
+ bg = "#ffffff" if idx % 2 == 0 else "#fffaf0"
+ parts.append(
+ f''
+ f''
+ f'{_html_escape(issue.param_name)} '
+ f'{_html_escape(issue.message)} '
+ f' '
+ )
+ parts.append("
")
+ else:
+ parts.append(
+ 'All parameters validated successfully.'
+ ' '
+ )
+
+ parts.append("
")
+
+ 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''
+ f'{"".join(footer_parts)} '
+ )
+
+ # --- Close wrapper ---
+ parts.append("
")
+ 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`}
{
appearance="subtle"
onClick={() => onHandlePanelStates(panels.CHAT)}
>
- {`${panelShowStates?.[panels.CHAT] ? "Hide" : "Show"} Chat`}
+ {`${panelShowStates[panels.CHAT] ? "Hide" : "Show"} Chat`}
@@ -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 ? (
-
+
@@ -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
- ) => (
-
-
-
-
- Reset topics
-
-
-
- {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) => (
+
+
+
+
+ Reset topics
+
+
+
+ {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}
/>
diff --git a/src/App/src/components/Chat/ChatMessageItem.tsx b/src/App/src/components/Chat/ChatMessageItem.tsx
new file mode 100644
index 000000000..f019e8308
--- /dev/null
+++ b/src/App/src/components/Chat/ChatMessageItem.tsx
@@ -0,0 +1,98 @@
+import React, { useMemo } from "react";
+import ReactMarkdown from "react-markdown";
+import remarkGfm from "remark-gfm";
+import supersub from "remark-supersub";
+import { type ChatMessage } from "../../types/AppTypes";
+import ChatChart from "../ChatChart/ChatChart";
+import Citations from "../Citations/Citations";
+import { hasChartContent } from "../../utils/chartUtils";
+import { parseCitationFromMessage } from "../../utils/messageUtils";
+
+type ChatMessageItemProps = {
+ message: ChatMessage;
+ index: number;
+ totalMessages: number;
+ generatingResponse: boolean;
+};
+
+const ChatMessageItemComponent: React.FC = ({
+ message,
+ index,
+ totalMessages,
+ generatingResponse,
+}) => {
+ const isLastAssistantMessage =
+ message.role === "assistant" && index === totalMessages - 1;
+
+ const parsedAnswer = useMemo(
+ () => ({
+ answer: typeof message.content === "string" ? message.content : "",
+ citations:
+ message.role === "assistant"
+ ? parseCitationFromMessage(message.citations)
+ : [],
+ }),
+ [message.citations, message.content, message.role]
+ );
+
+ if (message.role === "user" && typeof message.content === "string") {
+ if (message.content === "show in a graph by default") {
+ return null;
+ }
+
+ return (
+
+ {message.content}
+
+ );
+ }
+
+ if (hasChartContent(message.content)) {
+ return (
+
+
+
+
+ AI-generated content may be incorrect
+
+
+
+ );
+ }
+
+ if (typeof message.content === "string") {
+ return (
+
+
+ {message.content}
+
+ {isLastAssistantMessage && generatingResponse ? (
+
+
+
+
+
+ ) : message.role === "assistant" ? (
+
+ ) : null}
+
+
+
+ AI-generated content may be incorrect
+
+
+
+ );
+ }
+
+ return null;
+};
+
+const ChatMessageItem = React.memo(ChatMessageItemComponent);
+ChatMessageItem.displayName = "ChatMessageItem";
+
+export default ChatMessageItem;
diff --git a/src/App/src/components/ChatChart/ChatChart.tsx b/src/App/src/components/ChatChart/ChatChart.tsx
index 8d914508e..f92cf7c22 100644
--- a/src/App/src/components/ChatChart/ChatChart.tsx
+++ b/src/App/src/components/ChatChart/ChatChart.tsx
@@ -24,7 +24,8 @@ const ChatChart: React.FC = ({ chartContent }) => {
const chartRef = useRef(null);
useEffect(() => {
- if (chartRef.current && chartContent.data && chartContent?.type) {
+ const canvasElement = chartRef.current;
+ if (canvasElement && chartContent.data && chartContent?.type) {
ChartJS.register(...registerables);
const chartConfigData = {
@@ -59,7 +60,8 @@ const ChatChart: React.FC = ({ chartContent }) => {
}
}
- const myChart = new ChartJS(chartRef.current, chartConfigData);
+ const myChart = new ChartJS(canvasElement, chartConfigData);
+ const parentElement = canvasElement.parentElement;
const resizeObserver = new ResizeObserver(() => {
requestAnimationFrame(() => {
@@ -70,16 +72,13 @@ const ChatChart: React.FC = ({ chartContent }) => {
});
});
- if (chartRef?.current?.parentElement !== null) {
- resizeObserver.observe(chartRef.current.parentElement);
+ if (parentElement) {
+ resizeObserver.observe(parentElement);
}
return () => {
- if (
- chartRef?.current !== null &&
- chartRef?.current?.parentElement !== null
- ) {
- resizeObserver.unobserve(chartRef?.current?.parentElement);
+ if (parentElement) {
+ resizeObserver.unobserve(parentElement);
}
if (myChart.destroy) {
myChart.destroy();
diff --git a/src/App/src/components/ChatHistory/ChatHistory.css b/src/App/src/components/ChatHistory/ChatHistory.css
deleted file mode 100644
index a2783c327..000000000
--- a/src/App/src/components/ChatHistory/ChatHistory.css
+++ /dev/null
@@ -1,86 +0,0 @@
-.chat-history-container {
- display: flex;
- flex-direction: column;
- height: calc(100% - 2px);
- width: calc(100% - 2px);
- margin: 0 auto;
- border: 1px solid #ccc;
- border-radius: 8px;
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
- background-color: #f5f5f5;
- overflow-y: auto;
-}
-
-.chat-history-list {
- flex: 1;
- overflow-y: auto;
- padding: 10px;
- background-color: #f9f9f9;
- position: relative;
-}
-
-.chat-history-header {
- font-size: 1rem;
- font-weight: 600;
- height: 6vh;
- display: flex;
- align-items: center;
- margin-left: 0.5rem;
- gap: 3%;
-}
-.initial-msg {
- display: flex;
- align-items: center;
- flex-direction: column;
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- row-gap: 1rem;
- 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: #737373;
- font-weight: 400;
- font-size: 0.875em;
-}
-
-/* Large screens (≥992px) */
-@media (min-width: 992px) {
- .chat-history-header,
- .initial-msg {
- font-size: 0.8rem;
- }
-}
-
-/* Extra Large screens (≥1200px) */
-@media (min-width: 1200px) {
- .chat-history-header,
- .initial-msg {
- font-size: 0.9rem;
- }
-}
-
-/* Extra Extra Large screens (≥1400px) */
-@media (min-width: 1400px) {
- .chat-history-header,
- .initial-msg {
- font-size: 1.1rem;
- }
-}
-
-/* Very Extra Large screens (≥1600px) */
-@media (min-width: 1600px) {
- .chat-history-header,
- .initial-msg {
- font-size: 1.2rem;
- }
-}
diff --git a/src/App/src/components/ChatHistory/ChatHistory.tsx b/src/App/src/components/ChatHistory/ChatHistory.tsx
deleted file mode 100644
index 2301b77bb..000000000
--- a/src/App/src/components/ChatHistory/ChatHistory.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import React from "react";
-import "./ChatHistory.css";
-
-const ChatHistory: React.FC = () => {
- return (
-
-
Chat History
-
-
- {/* Chat History */}
- Coming soon...
-
-
-
- );
-};
-
-export default ChatHistory;
diff --git a/src/App/src/components/ChatHistoryListItemCell/ChatHistoryListItemCell.tsx b/src/App/src/components/ChatHistoryListItemCell/ChatHistoryListItemCell.tsx
index 00b6cfc5b..df0298a83 100644
--- a/src/App/src/components/ChatHistoryListItemCell/ChatHistoryListItemCell.tsx
+++ b/src/App/src/components/ChatHistoryListItemCell/ChatHistoryListItemCell.tsx
@@ -14,12 +14,19 @@ import {
} from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
-import { historyRename, historyDelete } from "../../api/api";
-
import styles from "./ChatHistoryListItemCell.module.css";
import { Conversation } from "../../types/AppTypes";
-import { useAppContext } from "../../state/useAppContext";
-import { actionConstants } from "../../state/ActionConstants";
+import { useAppDispatch, useAppSelector } from "../../state/hooks";
+import {
+ deleteConversation,
+ renameConversation,
+} from "../../state/slices/chatHistorySlice";
+import {
+ setShowAppSpinner,
+ startNewConversation,
+} from "../../state/slices/appSlice";
+import { hideCitation } from "../../state/slices/citationSlice";
+import { resetChatState } from "../../state/slices/chatSlice";
interface ChatHistoryListItemCellProps {
item?: Conversation;
@@ -32,7 +39,16 @@ export const ChatHistoryListItemCell: React.FC<
item,
onSelect,
}) => {
- const { state, dispatch } = useAppContext();
+ const dispatch = useAppDispatch();
+ const selectedConversationId = useAppSelector(
+ (state) => state.app.selectedConversationId
+ );
+ const currentConversationIdForCitation = useAppSelector(
+ (state) => state.citation.currentConversationIdForCitation
+ );
+ const generatingResponse = useAppSelector(
+ (state) => state.chat.generatingResponse
+ );
const [isHovered, setIsHovered] = React.useState(false);
const [edit, setEdit] = useState(false);
const [editTitle, setEditTitle] = useState("");
@@ -42,7 +58,7 @@ export const ChatHistoryListItemCell: React.FC<
const [errorRename, setErrorRename] = useState(undefined);
const [textFieldFocused, setTextFieldFocused] = useState(false);
const textFieldRef = useRef(null);
- const isSelected = item?.id === state.selectedConversationId;
+ const isSelected = item?.id === selectedConversationId;
const dialogContentProps = {
type: DialogType.close,
title: "Are you sure you want to delete this item?",
@@ -69,32 +85,26 @@ export const ChatHistoryListItemCell: React.FC<
}
const onDelete = async () => {
- dispatch({
- type: actionConstants.UPDATE_APP_SPINNER_STATUS,
- payload: true,
- });
- if(state.citation.currentConversationIdForCitation === item.id) {
- dispatch({ type: actionConstants.UPDATE_CITATION,payload: { activeCitation: null, showCitation: false, currentConversationIdForCitation: "" } });
- }else{
- dispatch({ type: actionConstants.UPDATE_CITATION,payload: { showCitation: true } });
- }
- const response = await historyDelete(item.id);
- if (!response.ok) {
- setErrorDelete(true);
- setTimeout(() => {
- setErrorDelete(false);
- }, 5000);
- } else {
- dispatch({
- type: actionConstants.DELETE_CONVERSATION_FROM_LIST,
- payload: item.id,
- });
+ dispatch(setShowAppSpinner(true));
+ try {
+ if (currentConversationIdForCitation === item.id) {
+ dispatch(hideCitation());
+ }
+
+ const result = await dispatch(deleteConversation(item.id));
+ if (deleteConversation.rejected.match(result)) {
+ setErrorDelete(true);
+ setTimeout(() => {
+ setErrorDelete(false);
+ }, 5000);
+ } else if (isSelected) {
+ dispatch(resetChatState());
+ dispatch(startNewConversation());
+ }
+ } finally {
+ toggleDeleteDialog();
+ dispatch(setShowAppSpinner(false));
}
- toggleDeleteDialog();
- dispatch({
- type: actionConstants.UPDATE_APP_SPINNER_STATUS,
- payload: false,
- });
};
const onEdit = (e: any) => {
@@ -135,8 +145,11 @@ export const ChatHistoryListItemCell: React.FC<
return;
}
setRenameLoading(true);
- const response = await historyRename(item.id, editTitle);
- if (!response.ok) {
+ const result = await dispatch(
+ renameConversation({ id: item.id, newTitle: editTitle })
+ );
+
+ if (renameConversation.rejected.match(result)) {
setErrorRename("Error: could not rename item");
setTimeout(() => {
setTextFieldFocused(true);
@@ -150,10 +163,6 @@ export const ChatHistoryListItemCell: React.FC<
setRenameLoading(false);
setEdit(false);
setEditTitle("");
- dispatch({
- type: actionConstants.UPDATE_CONVERSATION_TITLE,
- payload: { id: item?.id, newTitle: editTitle },
- });
}
};
@@ -169,7 +178,6 @@ export const ChatHistoryListItemCell: React.FC<
};
const handleKeyPressEdit = (e: any) => {
- console.log("handleKeyPressEdit", e.key, e);
if (e.key === "Enter") {
return handleSaveEdit(e);
}
@@ -193,7 +201,7 @@ export const ChatHistoryListItemCell: React.FC<
handleSelectItem(e);
}
};
- const isButtonDisabled = state.chat.generatingResponse && isSelected;
+ const isButtonDisabled = generatingResponse && isSelected;
return (
Promise;
onSelectConversation: (id: string) => void;
@@ -26,32 +27,30 @@ interface ChatHistoryListItemGroupsProps {
export const ChatHistoryListItemGroups: React.FC<
ChatHistoryListItemGroupsProps
-> = ({
- handleFetchHistory,
- onSelectConversation,
-}) => {
- const observerTarget = useRef(null);
+> = ({ handleFetchHistory, onSelectConversation }) => {
+ const observerTarget = useRef(null);
const initialCall = useRef(true);
- const { state } = useAppContext();
- const { chatHistory } = state;
+ const conversations = useAppSelector((state) => state.chatHistory.list);
+ const isFetchingConversations = useAppSelector(
+ (state) => state.chatHistory.fetchingConversations
+ );
- const groupedChatHistory = segregateItems(chatHistory.list);
+ const groupedChatHistory = segregateItems(conversations);
const handleSelectHistory = (item?: Conversation) => {
- if (typeof item === "object") {
- onSelectConversation(item?.id);
+ if (item) {
+ onSelectConversation(item.id);
}
};
- const onRenderCell = (item?: Conversation) => {
- return (
- handleSelectHistory(item)}
- key={item?.id}
- />
- );
- };
+ const onRenderCell = (item?: Conversation) => (
+ handleSelectHistory(item)}
+ key={item?.id}
+ />
+ );
+
useEffect(() => {
if (initialCall.current) {
initialCall.current = false;
@@ -59,26 +58,25 @@ export const ChatHistoryListItemGroups: React.FC<
}, []);
useEffect(() => {
- if (initialCall.current) {
+ if (initialCall.current || !observerTarget.current) {
return;
}
+
const observer = new IntersectionObserver(
(entries) => {
- if (entries[0].isIntersecting) {
- if (!chatHistory?.fetchingConversations) {
- handleFetchHistory();
- }
+ if (entries[0].isIntersecting && !isFetchingConversations) {
+ void handleFetchHistory();
}
},
{ threshold: 1 }
);
- if (observerTarget.current) observer.observe(observerTarget.current);
+ observer.observe(observerTarget.current);
return () => {
- if (observerTarget.current) observer.unobserve(observerTarget.current);
+ observer.disconnect();
};
- }, [observerTarget.current, chatHistory?.fetchingConversations]);
+ }, [handleFetchHistory, isFetchingConversations]);
const allConversationsLength = groupedChatHistory.reduce(
(previousValue, currentValue) =>
@@ -86,7 +84,7 @@ export const ChatHistoryListItemGroups: React.FC<
0
);
- if (!chatHistory.fetchingConversations && allConversationsLength === 0) {
+ if (!isFetchingConversations && allConversationsLength === 0) {
return (
- {Boolean(chatHistory?.fetchingConversations) && (
+ {Boolean(isFetchingConversations) && (
= (props) => {
- const {
- clearingError,
- clearing,
- onHideClearAllDialog,
- onClearAllChatHistory,
- handleFetchHistory,
- onSelectConversation,
- showClearAllConfirmationDialog,
- onClickClearAllOption,
- } = props;
- const { state, dispatch } = useAppContext();
- const { chatHistory } = state;
+export const ChatHistoryPanel: React.FC = ({
+ clearingError,
+ clearing,
+ onHideClearAllDialog,
+ onClearAllChatHistory,
+ handleFetchHistory,
+ onSelectConversation,
+ showClearAllConfirmationDialog,
+ onClickClearAllOption,
+}) => {
+ const conversationCount = useAppSelector(
+ (state) => state.chatHistory.list.length
+ );
+ const isFetchingConversations = useAppSelector(
+ (state) => state.chatHistory.fetchingConversations
+ );
+ const generatingResponse = useAppSelector(
+ (state) => state.chat.generatingResponse
+ );
const [showClearAllContextMenu, setShowClearAllContextMenu] =
useState(false);
- const { generatingResponse } = state?.chat;
const clearAllDialogContentProps = {
type: DialogType.close,
title: !clearingError
@@ -75,9 +77,8 @@ export const ChatHistoryPanel: React.FC = (props) => {
};
const disableClearAllChatHistory =
- !chatHistory.list.length ||
- generatingResponse ||
- state.chatHistory.fetchingConversations;
+ !conversationCount || generatingResponse || isFetchingConversations;
+
const menuItems: IContextualMenuItem[] = [
{
key: "clearAll",
@@ -98,7 +99,7 @@ export const ChatHistoryPanel: React.FC = (props) => {
= (props) => {
= (props) => {
-
= (props) => {
{} : onHideClearAllDialog}
+ onDismiss={clearing ? () => undefined : onHideClearAllDialog}
dialogContentProps={clearAllDialogContentProps}
modalProps={modalProps}
>
diff --git a/src/App/src/components/CitationPanel/CitationPanel.css b/src/App/src/components/CitationPanel/CitationPanel.css
index 46e7a9326..f3f03de4d 100644
--- a/src/App/src/components/CitationPanel/CitationPanel.css
+++ b/src/App/src/components/CitationPanel/CitationPanel.css
@@ -1,44 +1,39 @@
-/* General styles for the parent container */
.citationPanel {
display: flex;
- flex-direction: column; /* Stack children vertically */
- border: 1px solid #ddd; /* Optional: Add a border for visibility */
- border-radius: 8px; /* Optional: Rounded corners */
- padding: 16px; /* Optional: Add padding */
- background: radial-gradient(108.78% 108.78% at 50.02% 19.78%, #ffffff 57.29%, #eef6fe 100%); /* Optional: Background color */
- height: 93%; /* Adjust height as needed */
- overflow-y: auto; /* Enable vertical scrolling */
- overflow-x: hidden; /* Prevent horizontal scrolling */
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* Optional: Add a shadow */
+ flex-direction: column;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ padding: 16px;
+ background: radial-gradient(108.78% 108.78% at 50.02% 19.78%, #ffffff 57.29%, #eef6fe 100%);
+ height: 93%;
+ overflow-y: auto;
+ overflow-x: hidden;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
- /* Styling the header section */
.citationPanelInner {
display: flex;
align-items: center;
- justify-content: space-between; /* Space between title and icon */
- padding-bottom: 10px; /* Add some spacing below the header */
- border-bottom: 1px solid #ccc; /* Optional: Add a bottom border */
+ justify-content: space-between;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #ccc;
}
- /* Style the title */
.citationPanelInner span {
font-weight: bold;
font-size: 16px;
color: #333;
}
- /* Style the SVG button */
.citationPanelInner svg {
cursor: pointer;
transition: transform 0.3s;
}
.citationPanelInner svg:hover {
- transform: scale(1.1); /* Slightly enlarge on hover */
+ transform: scale(1.1);
}
- /* Styling the file path link */
h5 {
word-break: break-all;
margin: 12px 0;
@@ -47,7 +42,7 @@
}
h5 a {
- color: #0078d4; /* Microsoft blue */
+ color: #0078d4;
text-decoration: none;
}
@@ -55,7 +50,6 @@
text-decoration: underline;
}
- /* Styling the paragraphs */
p {
margin: 10px 0;
font-size: 14px;
@@ -66,7 +60,7 @@
p a {
word-break: break-all;
}
- /* Optional: Customize the scrollbar */
+
.citationPanel::-webkit-scrollbar {
width: 8px;
}
diff --git a/src/App/src/components/CitationPanel/CitationPanel.tsx b/src/App/src/components/CitationPanel/CitationPanel.tsx
index f5f341c30..8f167f235 100644
--- a/src/App/src/components/CitationPanel/CitationPanel.tsx
+++ b/src/App/src/components/CitationPanel/CitationPanel.tsx
@@ -1,69 +1,65 @@
-import React, { useMemo, useState } from 'react';
-import ReactMarkdown from 'react-markdown';
-import { Stack } from '@fluentui/react';
-import { DismissRegular } from '@fluentui/react-icons';
+import React, { useCallback } from "react";
+import ReactMarkdown from "react-markdown";
+import { Stack } from "@fluentui/react";
+import { DismissRegular } from "@fluentui/react-icons";
import remarkGfm from "remark-gfm";
-import rehypeRaw from "rehype-raw";
-import { useAppContext } from '../../state/useAppContext';
-import { actionConstants } from '../../state/ActionConstants';
+import { useAppDispatch } from "../../state/hooks";
+import { hideCitation } from "../../state/slices/citationSlice";
import "./CitationPanel.css";
+
interface Props {
- activeCitation: any
+ activeCitation: any;
}
-const CitationPanel = ({ activeCitation }: Props) => {
- const { dispatch } = useAppContext()
-
- const onCloseCitation = () => {
- dispatch({ type: actionConstants.UPDATE_CITATION,payload: { activeCitation: null, showCitation: false }})
- }
- return (
-
+const CitationPanelComponent: React.FC
= ({ activeCitation }) => {
+ const dispatch = useAppDispatch();
+
+ const handleCloseCitation = useCallback(() => {
+ dispatch(hideCitation());
+ }, [dispatch]);
-
-
-
+ return (
+
+
+
+
+ Citations
+
+ {
+ if (event.key === " " || event.key === "Enter") {
+ event.preventDefault();
+ handleCloseCitation();
+ }
+ }}
+ tabIndex={0}
+ onClick={handleCloseCitation}
+ />
+
+ {activeCitation.title}
- Citations
-
-
- e.key === " " || e.key === "Enter"
- ? onCloseCitation()
- : () => { }
- }
- tabIndex={0}
- onClick={onCloseCitation}
- />
-
-
- {activeCitation.title}
-
-
-
-
- )
+
+
+
+ );
};
+const CitationPanel = React.memo(CitationPanelComponent);
+CitationPanel.displayName = "CitationPanel";
export default CitationPanel;
\ No newline at end of file
diff --git a/src/App/src/components/Citations/AnswerParser.tsx b/src/App/src/components/Citations/AnswerParser.tsx
index e3de5f528..800d70742 100644
--- a/src/App/src/components/Citations/AnswerParser.tsx
+++ b/src/App/src/components/Citations/AnswerParser.tsx
@@ -1,48 +1,13 @@
import { AskResponse, Citation } from "../../types/AppTypes";
-
type ParsedAnswer = {
- citations: Citation[];
- markdownFormatText: string;
-};
-
-let filteredCitations = [] as Citation[];
-
-// Define a function to check if a citation with the same Chunk_Id already exists in filteredCitations
-const isDuplicate = (citation: Citation,citationIndex:string) => {
- return filteredCitations.some((c) => c.chunk_id === citation.chunk_id) ;
+ citations: Citation[];
+ markdownFormatText: string;
};
export function parseAnswer(answer: AskResponse): ParsedAnswer {
- // let answerText = answer.answer;
- // const citationLinks = answerText.match(/\[(doc\d\d?\d?)]/g);
-
- // const lengthDocN = "[doc".length;
-
- // filteredCitations = [] as Citation[];
- // let citationReindex = 0;
- // citationLinks?.forEach(link => {
- // // Replacing the links/citations with number
- // let citationIndex = link.slice(lengthDocN, link.length - 1);
- // let citation = cloneDeep(answer.citations[Number(citationIndex) - 1]) as Citation;
-
- // if (citation !== undefined && !isDuplicate(citation, citationIndex)) {
- // answerText = answerText.replaceAll(link, ` ^${++citationReindex}^ `);
- // citation.id = citationIndex; // original doc index to de-dupe
- // citation.reindex_id = citationReindex.toString(); // reindex from 1 for display
- // filteredCitations.push(citation);
- // }else{
- // // Replacing duplicate citation with original index
- // let matchingCitation = filteredCitations.find((ct) => citation?.chunk_id == ct?.chunk_id);
- // if (matchingCitation) {
- // answerText= answerText.replaceAll(link, ` ^${matchingCitation.reindex_id}^ `)
- // }
- // }
- // })
-
-
- return {
- citations: answer.citations,
- markdownFormatText: answer.answer
- };
+ return {
+ citations: answer.citations,
+ markdownFormatText: answer.answer,
+ };
}
diff --git a/src/App/src/components/Citations/Citations.tsx b/src/App/src/components/Citations/Citations.tsx
index c09d7c579..4dd023855 100644
--- a/src/App/src/components/Citations/Citations.tsx
+++ b/src/App/src/components/Citations/Citations.tsx
@@ -1,81 +1,87 @@
-import React, { useMemo } from 'react';
-import { parseAnswer } from './AnswerParser';
-import { useAppContext } from '../../state/useAppContext';
-import { actionConstants } from '../../state/ActionConstants';
+import React, { useCallback, useMemo } from "react";
+import { parseAnswer } from "./AnswerParser";
+import { useAppDispatch, useAppSelector } from "../../state/hooks";
+import { setCitationState } from "../../state/slices/citationSlice";
import "./Citations.css";
-import { AskResponse, Citation } from '../../types/AppTypes';
-import { fetchCitationContent } from '../../api/api';
+import { AskResponse, Citation } from "../../types/AppTypes";
+import { fetchCitationContent } from "../../api/api";
interface Props {
- answer: AskResponse;
- onSpeak?: any;
- isActive?: boolean;
- index: number;
+ answer: AskResponse;
+ onSpeak?: unknown;
+ isActive?: boolean;
+ index: number;
}
-const Citations = ({ answer, index }: Props) => {
-
- const { state, dispatch } = useAppContext();
- const parsedAnswer = useMemo(() => parseAnswer(answer), [answer]);
- const filePathTruncationLimit = 50;
- const createCitationFilepath = (
- citation: Citation,
- index: number,
- truncate: boolean = false
- ) => {
- let citationFilename = "";
- citationFilename = citation.title ? (citation.title ?? `Citation ${index}`) : `Citation ${index}`;
- return citationFilename;
- };
+const CitationsComponent: React.FC = ({ answer, index }) => {
+ const dispatch = useAppDispatch();
+ const selectedConversationId = useAppSelector(
+ (state) => state.app.selectedConversationId
+ );
+ const parsedAnswer = useMemo(() => parseAnswer(answer), [answer]);
- const onCitationClicked = async (
- citation: Citation
- ) => {
- const citationContent = await fetchCitationContent(citation);
- dispatch({
- type: actionConstants.UPDATE_CITATION,
- payload: { showCitation: true, activeCitation: {...citation, content:citationContent.content, title: citationContent.title}, currentConversationIdForCitation: state?.selectedConversationId},
- });
- };
+ const createCitationFilepath = useCallback(
+ (citation: Citation, citationIndex: number) =>
+ citation.title ? citation.title : `Citation ${citationIndex}`,
+ []
+ );
+ const handleCitationClicked = useCallback(
+ async (citation: Citation) => {
+ const citationContent = await fetchCitationContent(citation);
+ dispatch(
+ setCitationState({
+ showCitation: true,
+ activeCitation: {
+ ...citation,
+ content: citationContent.content,
+ title: citationContent.title,
+ },
+ currentConversationIdForCitation: selectedConversationId,
+ })
+ );
+ },
+ [dispatch, selectedConversationId]
+ );
- return (
-
- {parsedAnswer.citations.map((citation, idx) => {
- return (
-
- e.key === " " || e.key === "Enter"
- ? onCitationClicked(citation)
- : () => { }
- }
- tabIndex={0}
- title={createCitationFilepath(citation, ++idx)}
- key={idx}
- onClick={() => onCitationClicked(citation)}
- className={"citationContainer"}
- >
-
- {idx}
-
- {createCitationFilepath(citation, idx, true)}
-
- );
- })}
-
)
+ return (
+
+ {parsedAnswer.citations.map((citation, citationOffset) => {
+ const displayIndex = citationOffset + 1;
+
+ return (
+
+ event.key === " " || event.key === "Enter"
+ ? handleCitationClicked(citation)
+ : undefined
+ }
+ tabIndex={0}
+ title={createCitationFilepath(citation, displayIndex)}
+ key={`${index}-${displayIndex}-${citation.chunk_id ?? citation.id}`}
+ onClick={() => handleCitationClicked(citation)}
+ className="citationContainer"
+ >
+ {displayIndex}
+ {createCitationFilepath(citation, displayIndex)}
+
+ );
+ })}
+
+ );
};
+const Citations = React.memo(CitationsComponent);
+Citations.displayName = "Citations";
export default Citations;
\ No newline at end of file
diff --git a/src/App/src/configs/Utils.tsx b/src/App/src/configs/Utils.tsx
index e4a14d06d..45ed6397e 100644
--- a/src/App/src/configs/Utils.tsx
+++ b/src/App/src/configs/Utils.tsx
@@ -113,8 +113,7 @@ export const segregateItems = (items: Conversation[]) => {
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(today.getDate() - 1);
- // Sort items by updatedAt in descending order
- items.sort(
+ const sortedItems = [...items].sort(
(a, b) =>
new Date(b.updatedAt ? b.updatedAt : new Date()).getTime() -
new Date(a.updatedAt ? a.updatedAt : new Date()).getTime()
@@ -133,7 +132,7 @@ export const segregateItems = (items: Conversation[]) => {
Past: {},
};
- items.forEach((item) => {
+ sortedItems.forEach((item) => {
const itemDate = new Date(item.updatedAt ? item.updatedAt : new Date());
const itemDateOnly = itemDate.toDateString();
if (itemDateOnly === today.toDateString()) {
@@ -181,7 +180,6 @@ export async function loadConfig() {
const configData = await response.json();
return configData;
} catch (error) {
- console.error("Error loading config:", error);
throw error;
}
}
@@ -245,8 +243,8 @@ export const tryGetRaiPrettyError = (errorMessage: string) => {
)
}
}
- } catch (e) {
- console.error('Failed to parse the error:', e)
+ } catch {
+ return errorMessage
}
return errorMessage
}
@@ -263,8 +261,8 @@ export const parseErrorMessage = (errorMessage: string) => {
innerErrorString = innerErrorString.replaceAll("\\'", "'")
let newErrorMessage = errorCodeMessage + ' ' + innerErrorString
errorMessage = newErrorMessage
- } catch (e) {
- console.error('Error parsing inner error message: ', e)
+ } catch {
+ return tryGetRaiPrettyError(errorMessage)
}
}
diff --git a/src/App/src/hooks/useAutoScroll.ts b/src/App/src/hooks/useAutoScroll.ts
new file mode 100644
index 000000000..e83fc511e
--- /dev/null
+++ b/src/App/src/hooks/useAutoScroll.ts
@@ -0,0 +1,23 @@
+import { useCallback, useRef } from "react";
+
+export const useAutoScroll = () => {
+ const chatMessageStreamEnd = useRef(null);
+
+ const scrollChatToBottom = useCallback(
+ (behavior: ScrollBehavior = "smooth") => {
+ if (!chatMessageStreamEnd.current) {
+ return;
+ }
+
+ setTimeout(() => {
+ chatMessageStreamEnd.current?.scrollIntoView({ behavior });
+ }, 100);
+ },
+ []
+ );
+
+ return {
+ chatMessageStreamEnd,
+ scrollChatToBottom,
+ };
+};
diff --git a/src/App/src/hooks/useChatApi.ts b/src/App/src/hooks/useChatApi.ts
new file mode 100644
index 000000000..6f1f4ac08
--- /dev/null
+++ b/src/App/src/hooks/useChatApi.ts
@@ -0,0 +1,514 @@
+import {
+ type Dispatch,
+ type SetStateAction,
+ useCallback,
+ useEffect,
+ useRef,
+} from "react";
+import {
+ callConversationApi,
+ historyUpdate,
+} from "../api/api";
+import { generateUUIDv4 } from "../configs/Utils";
+import { useAppDispatch, useAppSelector } from "../state/hooks";
+import {
+ appendMessages,
+ resetChatState,
+ setGeneratingResponse,
+ setStreamingInProgress,
+ setUserMessage,
+ updateMessageById,
+} from "../state/slices/chatSlice";
+import { hideCitation } from "../state/slices/citationSlice";
+import {
+ addConversationToHistory,
+ setHistoryUpdateApiPending,
+} from "../state/slices/chatHistorySlice";
+import {
+ setSelectedConversationId,
+ startNewConversation,
+} from "../state/slices/appSlice";
+import {
+ type ChatMessage,
+ type Conversation,
+ type ConversationRequest,
+ type ParsedChunk,
+} from "../types/AppTypes";
+import { hasChartContent, parseChartContent } from "../utils/chartUtils";
+import { isChartQuery } from "../utils/messageUtils";
+
+type UseChatApiOptions = {
+ scrollChatToBottom: () => void;
+ setIsChartLoading: Dispatch>;
+ isChartDisplayDefault: boolean;
+};
+
+const ASSISTANT = "assistant";
+const ERROR = "error";
+const USER = "user";
+
+const getErrorMessage = (errorLine: string) => {
+ try {
+ const parsedError = JSON.parse(errorLine);
+ return parsedError.error ===
+ "Attempted to access streaming response content, without having called `read()`."
+ ? "An error occurred. Please try again later."
+ : parsedError.error;
+ } catch {
+ return errorLine;
+ }
+};
+
+export const useChatApi = ({
+ scrollChatToBottom,
+ setIsChartLoading,
+ isChartDisplayDefault,
+}: UseChatApiOptions) => {
+ const dispatch = useAppDispatch();
+ const generatingResponse = useAppSelector(
+ (state) => state.chat.generatingResponse
+ );
+ const isStreamingInProgress = useAppSelector(
+ (state) => state.chat.isStreamingInProgress
+ );
+ const selectedConversationId = useAppSelector(
+ (state) => state.app.selectedConversationId
+ );
+ const generatedConversationId = useAppSelector(
+ (state) => state.app.generatedConversationId
+ );
+ const abortControllersRef = useRef([]);
+
+ useEffect(() => {
+ if (generatingResponse || isStreamingInProgress) {
+ const chatAPISignal = abortControllersRef.current.shift();
+ chatAPISignal?.abort(
+ "Chat Aborted due to switch to other conversation while generating"
+ );
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [selectedConversationId]);
+
+ const readStreamingResponse = useCallback(
+ async (
+ response: Response,
+ handlers: {
+ onToolDelta?: (content: string) => void;
+ onAssistantDelta?: (content: string) => void;
+ }
+ ) => {
+ const reader = response.body?.getReader();
+
+ if (!reader) {
+ return {
+ accumulatedContent: "",
+ errorLine: "",
+ hasError: false,
+ };
+ }
+
+ let accumulatedContent = "";
+ let errorLine = "";
+ let hasError = false;
+ let lineBuffer = "";
+ const decoder = new TextDecoder("utf-8");
+
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) {
+ break;
+ }
+
+ lineBuffer += decoder.decode(value, { stream: true });
+ const lines = lineBuffer.split("\n");
+ lineBuffer = lines.pop() ?? "";
+
+ for (const rawLine of lines) {
+ const line = rawLine.trim();
+ if (!line || line === "{}") {
+ continue;
+ }
+
+ try {
+ const parsedChunk: ParsedChunk = JSON.parse(line);
+ if (parsedChunk?.error) {
+ hasError = true;
+ errorLine = line;
+ break;
+ }
+
+ const delta = parsedChunk?.choices?.[0]?.delta;
+ if (delta?.role === "tool" && delta.content) {
+ handlers.onToolDelta?.(delta.content);
+ }
+
+ if (delta?.role === "assistant" && delta.content) {
+ accumulatedContent += delta.content;
+ handlers.onAssistantDelta?.(accumulatedContent);
+ }
+ } catch {
+ continue;
+ }
+ }
+
+ if (hasError) {
+ break;
+ }
+ }
+
+ return {
+ accumulatedContent,
+ errorLine,
+ hasError,
+ };
+ },
+ []
+ );
+
+ const handleChartResult = useCallback(
+ (
+ chartResponse: unknown,
+ streamMessage: ChatMessage,
+ newMessage: ChatMessage,
+ suppressErrors = false
+ ): ChatMessage[] => {
+ if (hasChartContent(chartResponse)) {
+ const chartMessage: ChatMessage = {
+ ...streamMessage,
+ content: chartResponse,
+ role: ASSISTANT,
+ };
+
+ dispatch(updateMessageById(chartMessage));
+ scrollChatToBottom();
+ return [newMessage, chartMessage];
+ }
+
+ if (suppressErrors) {
+ return [];
+ }
+
+ const errorMessage: ChatMessage = {
+ ...streamMessage,
+ content:
+ typeof chartResponse === "string"
+ ? chartResponse
+ : JSON.stringify(chartResponse),
+ role: ERROR,
+ };
+
+ dispatch(updateMessageById(errorMessage));
+ scrollChatToBottom();
+ return [newMessage, errorMessage];
+ },
+ [dispatch, scrollChatToBottom]
+ );
+
+ const saveConversation = useCallback(
+ async (
+ newMessages: ChatMessage[],
+ conversationId: string,
+ requestType = "Text"
+ ) => {
+ if (!conversationId || !newMessages.length) {
+ return false;
+ }
+
+ const isNewConversation =
+ requestType !== "graph" && !selectedConversationId;
+
+ dispatch(setHistoryUpdateApiPending(true));
+
+ try {
+ const response = await historyUpdate(newMessages, conversationId);
+ if (!response.ok) {
+ throw new Error("Unable to persist the current conversation.");
+ }
+
+ const responseJson = await response.json();
+ if (isNewConversation && responseJson?.success) {
+ const newConversation: Conversation = {
+ id: responseJson?.data?.conversation_id,
+ title: responseJson?.data?.title,
+ messages: [...newMessages],
+ date: responseJson?.data?.date,
+ updatedAt: responseJson?.data?.date,
+ };
+
+ dispatch(addConversationToHistory(newConversation));
+ dispatch(setSelectedConversationId(responseJson?.data?.conversation_id));
+ }
+
+ return true;
+ } catch {
+ return false;
+ } finally {
+ dispatch(setGeneratingResponse(false));
+ dispatch(setHistoryUpdateApiPending(false));
+ }
+ },
+ [dispatch, selectedConversationId]
+ );
+
+ const makeChartRequest = useCallback(
+ async (
+ question: string,
+ conversationId: string,
+ isAutomatic = false
+ ) => {
+ if (generatingResponse || !question.trim()) {
+ return;
+ }
+
+ const newMessage: ChatMessage = {
+ id: generateUUIDv4(),
+ role: USER,
+ content: question,
+ date: new Date().toISOString(),
+ };
+
+ if (!isAutomatic) {
+ dispatch(setGeneratingResponse(true));
+ scrollChatToBottom();
+ dispatch(appendMessages([newMessage]));
+ dispatch(setUserMessage(""));
+ }
+
+ const abortController = new AbortController();
+ abortControllersRef.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);
+ const { accumulatedContent, errorLine, hasError } =
+ await readStreamingResponse(response, {
+ onAssistantDelta: () => undefined,
+ });
+
+ if (hasError) {
+ const errorMessage: ChatMessage = {
+ id: generateUUIDv4(),
+ role: ERROR,
+ content: getErrorMessage(errorLine),
+ date: new Date().toISOString(),
+ };
+
+ updatedMessages = isAutomatic ? [] : [newMessage, errorMessage];
+ if (!isAutomatic) {
+ dispatch(appendMessages([errorMessage]));
+ scrollChatToBottom();
+ }
+ } else {
+ const chartResponse = parseChartContent(accumulatedContent);
+ updatedMessages = handleChartResult(
+ chartResponse,
+ streamMessage,
+ newMessage,
+ isAutomatic
+ );
+ }
+
+ if (!isAutomatic || updatedMessages.length > 0) {
+ await saveConversation(updatedMessages, conversationId, "graph");
+ }
+ } catch (error) {
+ if (abortController.signal.aborted && !isAutomatic) {
+ const partialMessages = streamMessage.content
+ ? [newMessage, streamMessage]
+ : [newMessage];
+ await saveConversation(partialMessages, conversationId, "graph");
+ }
+
+ if (!abortController.signal.aborted && !isAutomatic) {
+ alert(
+ error instanceof Error
+ ? error.message
+ : "An error occurred. Please try again."
+ );
+ }
+ } finally {
+ if (!isAutomatic) {
+ dispatch(setGeneratingResponse(false));
+ }
+
+ dispatch(setStreamingInProgress(false));
+ setIsChartLoading(false);
+ }
+ },
+ [
+ dispatch,
+ generatingResponse,
+ handleChartResult,
+ readStreamingResponse,
+ saveConversation,
+ scrollChatToBottom,
+ setIsChartLoading,
+ ]
+ );
+
+ const sendMessage = useCallback(
+ async (question: string) => {
+ if (generatingResponse || !question.trim()) {
+ return;
+ }
+
+ const conversationId = selectedConversationId || generatedConversationId;
+ const requestType = isChartQuery(question) ? "graph" : "Text";
+ const newMessage: ChatMessage = {
+ id: generateUUIDv4(),
+ role: USER,
+ content: question,
+ date: new Date().toISOString(),
+ };
+
+ dispatch(setGeneratingResponse(true));
+ scrollChatToBottom();
+ dispatch(appendMessages([newMessage]));
+ dispatch(setUserMessage(""));
+
+ const abortController = new AbortController();
+ abortControllersRef.current.unshift(abortController);
+
+ const request: ConversationRequest = {
+ id: conversationId,
+ query: question,
+ };
+
+ const streamMessage: ChatMessage = {
+ id: generateUUIDv4(),
+ date: new Date().toISOString(),
+ role: ASSISTANT,
+ content: "",
+ citations: "",
+ };
+ let updatedMessages: ChatMessage[] = [];
+
+ try {
+ const response = await callConversationApi(request, abortController.signal);
+ const isChart = isChartQuery(question);
+ const { accumulatedContent, errorLine, hasError } =
+ await readStreamingResponse(response, {
+ onToolDelta: (content) => {
+ streamMessage.citations = content;
+ if (!isChart) {
+ dispatch(setStreamingInProgress(true));
+ dispatch(updateMessageById({ ...streamMessage }));
+ }
+ },
+ onAssistantDelta: (content) => {
+ if (!isChart) {
+ streamMessage.content = content;
+ streamMessage.role = ASSISTANT;
+ dispatch(setStreamingInProgress(true));
+ dispatch(updateMessageById({ ...streamMessage }));
+ scrollChatToBottom();
+ }
+ },
+ });
+
+ if (hasError) {
+ const errorMessage: ChatMessage = {
+ id: generateUUIDv4(),
+ role: ERROR,
+ content: getErrorMessage(errorLine),
+ date: new Date().toISOString(),
+ };
+
+ updatedMessages = [newMessage, errorMessage];
+ dispatch(appendMessages([errorMessage]));
+ scrollChatToBottom();
+ } else if (isChart) {
+ const chartResponse = parseChartContent(accumulatedContent);
+ updatedMessages = handleChartResult(chartResponse, streamMessage, newMessage);
+ } else {
+ updatedMessages = [
+ newMessage,
+ {
+ ...streamMessage,
+ content: accumulatedContent || streamMessage.content,
+ role: ASSISTANT,
+ },
+ ];
+ }
+
+ if (updatedMessages[updatedMessages.length - 1]?.role !== ERROR) {
+ const didSave = await saveConversation(
+ updatedMessages,
+ conversationId,
+ requestType
+ );
+
+ if (
+ didSave &&
+ requestType !== "graph" &&
+ updatedMessages[updatedMessages.length - 1]?.role !== ERROR &&
+ isChartDisplayDefault
+ ) {
+ setIsChartLoading(true);
+ setTimeout(() => {
+ void makeChartRequest(
+ "show in a graph by default",
+ conversationId,
+ true
+ );
+ }, 5000);
+ }
+ }
+ } catch (error) {
+ if (abortController.signal.aborted) {
+ const partialMessages = streamMessage.content
+ ? [newMessage, streamMessage]
+ : [newMessage];
+ await saveConversation(partialMessages, conversationId, "error");
+ }
+
+ if (!abortController.signal.aborted) {
+ alert(
+ error instanceof Error
+ ? error.message
+ : "An error occurred. Please try again."
+ );
+ }
+ } finally {
+ dispatch(setGeneratingResponse(false));
+ dispatch(setStreamingInProgress(false));
+ }
+ },
+ [
+ dispatch,
+ generatedConversationId,
+ generatingResponse,
+ handleChartResult,
+ isChartDisplayDefault,
+ makeChartRequest,
+ readStreamingResponse,
+ saveConversation,
+ scrollChatToBottom,
+ selectedConversationId,
+ setIsChartLoading,
+ ]
+ );
+
+ const startNewChat = useCallback(() => {
+ dispatch(resetChatState());
+ dispatch(startNewConversation());
+ dispatch(hideCitation());
+ }, [dispatch]);
+
+ return {
+ sendMessage,
+ startNewChat,
+ };
+};
diff --git a/src/App/src/index.tsx b/src/App/src/index.tsx
index ab7645b13..21869a6a7 100644
--- a/src/App/src/index.tsx
+++ b/src/App/src/index.tsx
@@ -1,22 +1,22 @@
-import React from 'react';
-import ReactDOM from 'react-dom/client';
-import './index.css';
-import App from './App';
-import reportWebVitals from './reportWebVitals';
+import React from "react";
+import ReactDOM from "react-dom/client";
+import { Provider } from "react-redux";
+import "./index.css";
+import App from "./App";
+import reportWebVitals from "./reportWebVitals";
import { initializeIcons } from "@fluentui/react";
-import AppProvider from './state/AppProvider';
+import { store } from "./state/store";
+
initializeIcons();
const root = ReactDOM.createRoot(
- document.getElementById('root') as HTMLElement
+ document.getElementById("root") as HTMLElement
);
+
root.render(
-
+
-
+
);
-// If you want to start measuring performance in your app, pass a function
-// to log results (for example: reportWebVitals(console.log))
-// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
diff --git a/src/App/src/logo.svg b/src/App/src/logo.svg
deleted file mode 100644
index 9dfc1c058..000000000
--- a/src/App/src/logo.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/App/src/state/ActionConstants.tsx b/src/App/src/state/ActionConstants.tsx
deleted file mode 100644
index 3491fb218..000000000
--- a/src/App/src/state/ActionConstants.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-export const actionConstants = {
- SET_FILTERS: "SET_FILTERS",
- UPDATE_FILTERS_FETCHED_FLAG: "UPDATE_FILTERS_FETCHED_FLAG",
- UPDATE_CHARTS_DATA: "UPDATE_CHARTS_DATA",
- UPDATE_INITIAL_CHARTS_FETCHED_FLAG: "UPDATE_INITIAL_CHARTS_FETCHED_FLAG",
- UPDATE_SELECTED_FILTERS: "UPDATE_SELECTED_FILTERS",
- UPDATE_USER_MESSAGE: "UPDATE_USER_MESSAGE",
- UPDATE_GENERATING_RESPONSE_FLAG: "UPDATE_GENERATING_RESPONSE_FLAG",
- UPDATE_MESSAGES: "UPDATE_MESSAGES",
- ADD_CONVERSATIONS_TO_LIST: "ADD_CONVERSATIONS_TO_LIST",
- SAVE_CONFIG: "SAVE_CONFIG",
- UPDATE_SELECTED_CONV_ID: "UPDATE_SELECTED_CONV_ID",
- UPDATE_GENERATED_CONV_ID: "UPDATE_GENERATED_CONV_ID",
- NEW_CONVERSATION_START: "NEW_CONVERSATION_START",
- UPDATE_CONVERSATIONS_FETCHING_FLAG: "UPDATE_CONVERSATIONS_FETCHING_FLAG",
- UPDATE_CONVERSATION_TITLE: "UPDATE_CONVERSATION_TITLE",
- UPDATE_ON_CLEAR_ALL_CONVERSATIONS: "UPDATE_ON_CLEAR_ALL_CONVERSATIONS",
- SHOW_CHATHISTORY_CONVERSATION: "SHOW_CHATHISTORY_CONVERSATION",
- UPDATE_CHATHISTORY_CONVERSATION_FLAG: "UPDATE_CHATHISTORY_CONVERSATION_FLAG",
- DELETE_CONVERSATION_FROM_LIST: "DELETE_CONVERSATION_FROM_LIST",
- STORE_COSMOS_INFO: "STORE_COSMOS_INFO",
- ADD_NEW_CONVERSATION_TO_CHAT_HISTORY: "ADD_NEW_CONVERSATION_TO_CHAT_HISTORY",
- UPDATE_APP_SPINNER_STATUS: "UPDATE_APP_SPINNER_STATUS",
- UPDATE_HISTORY_UPDATE_API_FLAG: "UPDATE_HISTORY_UPDATE_API_FLAG",
- UPDATE_MESSAGE_BY_ID: "UPDATE_MESSAGE_BY_ID",
- UPDATE_STREAMING_FLAG: "UPDATE_STREAMING_FLAG",
- UPDATE_CHARTS_FETCHING_FLAG: "UPDATE_CHARTS_FETCHING_FLAG",
- UPDATE_FILTERS_FETCHING_FLAG: "UPDATE_FILTERS_FETCHING_FLAG",
- UPDATE_CITATION: "UPDATE_CITATION",
-} as const;
diff --git a/src/App/src/state/AppProvider.tsx b/src/App/src/state/AppProvider.tsx
deleted file mode 100644
index 7c812e435..000000000
--- a/src/App/src/state/AppProvider.tsx
+++ /dev/null
@@ -1,271 +0,0 @@
-import { useReducer, createContext, type ReactNode, useEffect } from "react";
-import {
- type AppConfig,
- type ChartConfigItem,
- type ChatMessage,
- type Conversation,
- type CosmosDBHealth,
- CosmosDBStatus,
- type FilterMetaData,
- type SelectedFilters,
-} from "../types/AppTypes";
-import { appReducer } from "./AppReducer";
-import { actionConstants } from "./ActionConstants";
-import { defaultSelectedFilters, generateUUIDv4 } from "../configs/Utils";
-import { historyEnsure } from "../api/api";
-
-export type AppState = {
- dashboards: {
- filtersMetaFetched: boolean;
- initialChartsDataFetched: boolean;
- filtersMeta: FilterMetaData;
- charts: ChartConfigItem[];
- selectedFilters: SelectedFilters;
- fetchingFilters: boolean;
- fetchingCharts: boolean
- };
- chat: {
- generatingResponse: boolean;
- messages: ChatMessage[];
- userMessage: string;
- isStreamingInProgress: boolean;
- citations: string |null;
- };
- citation: {
- activeCitation?: any;
- showCitation: boolean;
- currentConversationIdForCitation?: string;
- };
- chatHistory: {
- list: Conversation[];
- fetchingConversations: boolean;
- isFetchingConvMessages: boolean;
- isHistoryUpdateAPIPending: boolean;
- };
- selectedConversationId: string;
- generatedConversationId: string;
- config: {
- appConfig: AppConfig;
- charts: ChartConfigItem[];
- };
- cosmosInfo: CosmosDBHealth;
- showAppSpinner: boolean;
-};
-
-const initialState: AppState = {
- dashboards: {
- filtersMetaFetched: false,
- initialChartsDataFetched: false,
- filtersMeta: {
- Sentiment: [],
- Topic: [],
- DateRange: [],
- },
- charts: [],
- selectedFilters: { ...defaultSelectedFilters },
- fetchingCharts: true,
- fetchingFilters: true
- },
- chat: {
- generatingResponse: false,
- messages: [],
- userMessage: "",
- citations: "",
- isStreamingInProgress: false,
- },
- citation: {
- activeCitation: null,
- showCitation: false,
- currentConversationIdForCitation: '',
- },
- chatHistory: {
- list: [],
- fetchingConversations: false,
- isFetchingConvMessages: false,
- isHistoryUpdateAPIPending: false,
- },
- selectedConversationId: "",
- generatedConversationId: generateUUIDv4(),
- config: {
- appConfig: null,
- charts: [],
- },
- cosmosInfo: { cosmosDB: false, status: "" },
- showAppSpinner: false,
-};
-
-export type Action =
- | {
- type: typeof actionConstants.SET_FILTERS;
- payload: FilterMetaData;
- }
- | {
- type: typeof actionConstants.UPDATE_FILTERS_FETCHED_FLAG;
- payload: boolean;
- }
- | {
- type: typeof actionConstants.UPDATE_CHARTS_DATA;
- payload: ChartConfigItem[];
- }
- | {
- type: typeof actionConstants.UPDATE_INITIAL_CHARTS_FETCHED_FLAG;
- payload: boolean;
- }
- | {
- type: typeof actionConstants.UPDATE_SELECTED_FILTERS;
- payload: SelectedFilters;
- }
- | {
- type: typeof actionConstants.UPDATE_USER_MESSAGE;
- payload: string;
- }
- | {
- type: typeof actionConstants.UPDATE_GENERATING_RESPONSE_FLAG;
- payload: boolean;
- }
- | {
- type: typeof actionConstants.UPDATE_MESSAGES;
- payload: ChatMessage[];
- }
- | {
- type: typeof actionConstants.ADD_CONVERSATIONS_TO_LIST;
- payload: Conversation[];
- }
- | {
- type: typeof actionConstants.SAVE_CONFIG;
- payload: AppState["config"];
- }
- | {
- type: typeof actionConstants.UPDATE_SELECTED_CONV_ID;
- payload: string;
- }
- | {
- type: typeof actionConstants.UPDATE_GENERATED_CONV_ID;
- payload: string;
- }
- | {
- type: typeof actionConstants.NEW_CONVERSATION_START;
- }
- | {
- type: typeof actionConstants.UPDATE_CONVERSATIONS_FETCHING_FLAG;
- payload: boolean;
- }
- | {
- type: typeof actionConstants.UPDATE_CONVERSATION_TITLE;
- payload: { id: string; newTitle: string };
- }
- | {
- type: typeof actionConstants.UPDATE_ON_CLEAR_ALL_CONVERSATIONS;
- }
- | {
- type: typeof actionConstants.SHOW_CHATHISTORY_CONVERSATION;
- payload: { id: string; messages: ChatMessage[] };
- }
- | {
- type: typeof actionConstants.UPDATE_CHATHISTORY_CONVERSATION_FLAG;
- payload: boolean;
- }
- | {
- type: typeof actionConstants.DELETE_CONVERSATION_FROM_LIST;
- payload: string;
- }
- | {
- type: typeof actionConstants.STORE_COSMOS_INFO;
- payload: CosmosDBHealth;
- }
- | {
- type: typeof actionConstants.ADD_NEW_CONVERSATION_TO_CHAT_HISTORY;
- payload: Conversation;
- }
- | {
- type: typeof actionConstants.UPDATE_APP_SPINNER_STATUS;
- payload: boolean;
- }
- | {
- type: typeof actionConstants.UPDATE_HISTORY_UPDATE_API_FLAG;
- payload: boolean;
- }
- | {
- type: typeof actionConstants.UPDATE_MESSAGE_BY_ID;
- payload: ChatMessage;
- }
- | {
- type: typeof actionConstants.UPDATE_STREAMING_FLAG;
- payload: boolean;
- }
- | {
- type: typeof actionConstants.UPDATE_CHARTS_FETCHING_FLAG;
- payload: boolean;
- }
- | {
- type: typeof actionConstants.UPDATE_FILTERS_FETCHING_FLAG;
- payload: boolean;
- }
- | {
- type: typeof actionConstants.UPDATE_CITATION;
- payload: {activeCitation?: any, showCitation: boolean, currentConversationIdForCitation?: string};
- };
-
-export const AppContext = createContext<{
- state: AppState;
- dispatch: React.Dispatch;
-}>({ state: initialState, dispatch: () => {} });
-
-export const AppProvider = ({ children }: { children: ReactNode }) => {
- const [state, dispatch] = useReducer(appReducer, initialState);
-
- useEffect(() => {
- const getHistoryEnsure = async () => {
- // dispatch({ type: 'UPDATE_CHAT_HISTORY_LOADING_STATE', payload: ChatHistoryLoadingState.Loading })
- historyEnsure()
- .then((response) => {
- if (response?.cosmosDB) {
- console.log("COSMOS DB IS OKAY ");
- dispatch({
- type: actionConstants.STORE_COSMOS_INFO,
- payload: response,
- });
- // fetchChatHistory()
- // .then(res => {
- // if (res) {
- // // dispatch({ type: 'UPDATE_CHAT_HISTORY_LOADING_STATE', payload: ChatHistoryLoadingState.Success })
- // } else {
- // // dispatch({ type: 'UPDATE_CHAT_HISTORY_LOADING_STATE', payload: ChatHistoryLoadingState.Fail })
- // // dispatch({
- // // type: 'SET_COSMOSDB_STATUS',
- // // payload: { cosmosDB: false, status: CosmosDBStatus.NotWorking }
- // // })
- // }
- // })
- // .catch(_err => {
- // // dispatch({ type: 'UPDATE_CHAT_HISTORY_LOADING_STATE', payload: ChatHistoryLoadingState.Fail })
- // // dispatch({
- // // type: 'SET_COSMOSDB_STATUS',
- // // payload: { cosmosDB: false, status: CosmosDBStatus.NotWorking }
- // // })
- // })
- } else {
- dispatch({
- type: actionConstants.STORE_COSMOS_INFO,
- payload: response,
- });
- }
- })
- .catch((_err) => {
- dispatch({
- type: actionConstants.STORE_COSMOS_INFO,
- payload: { cosmosDB: false, status: CosmosDBStatus.NotConfigured },
- });
- });
- };
- // getHistoryEnsure();
- }, []);
-
- return (
-
- {children}
-
- );
-};
-
-export default AppProvider;
diff --git a/src/App/src/state/AppReducer.tsx b/src/App/src/state/AppReducer.tsx
deleted file mode 100644
index 38ca78703..000000000
--- a/src/App/src/state/AppReducer.tsx
+++ /dev/null
@@ -1,262 +0,0 @@
-import { generateUUIDv4 } from "../configs/Utils";
-import { actionConstants } from "./ActionConstants";
-import { Action, type AppState } from "./AppProvider";
-
-const appReducer = (state: AppState, action: Action): AppState => {
- switch (action.type) {
- case actionConstants.SET_FILTERS:
- return {
- ...state,
- dashboards: { ...state.dashboards, filtersMeta: action.payload },
- };
- case actionConstants.UPDATE_FILTERS_FETCHED_FLAG:
- return {
- ...state,
- dashboards: { ...state.dashboards, filtersMetaFetched: action.payload },
- };
- case actionConstants.UPDATE_CHARTS_DATA:
- return {
- ...state,
- dashboards: { ...state.dashboards, charts: action.payload },
- };
- case actionConstants.UPDATE_INITIAL_CHARTS_FETCHED_FLAG:
- return {
- ...state,
- dashboards: {
- ...state.dashboards,
- initialChartsDataFetched: action.payload,
- },
- };
- case actionConstants.UPDATE_SELECTED_FILTERS:
- return {
- ...state,
- dashboards: {
- ...state.dashboards,
- selectedFilters: action.payload,
- },
- };
- case actionConstants.UPDATE_USER_MESSAGE:
- return {
- ...state,
- chat: {
- ...state.chat,
- userMessage: action.payload,
- },
- };
- case actionConstants.UPDATE_GENERATING_RESPONSE_FLAG:
- return {
- ...state,
- chat: {
- ...state.chat,
- generatingResponse: action.payload,
- },
- };
- case actionConstants.UPDATE_MESSAGES:
- return {
- ...state,
- chat: {
- ...state.chat,
- messages: [...state.chat.messages, ...action.payload],
- },
- };
- case actionConstants.ADD_CONVERSATIONS_TO_LIST:
- return {
- ...state,
- chatHistory: {
- ...state.chatHistory,
- list: [...state.chatHistory.list, ...action.payload],
- },
- };
- case actionConstants.SAVE_CONFIG:
- return {
- ...state,
- config: {
- ...state.config,
- ...action.payload,
- },
- };
- case actionConstants.UPDATE_SELECTED_CONV_ID:
- return {
- ...state,
- selectedConversationId: action.payload,
- };
- case actionConstants.UPDATE_GENERATED_CONV_ID:
- return {
- ...state,
- generatedConversationId: generateUUIDv4(),
- };
- case actionConstants.NEW_CONVERSATION_START:
- return {
- ...state,
- chat: { ...state.chat, messages: [] },
- selectedConversationId: "",
- generatedConversationId: generateUUIDv4(),
- };
- case actionConstants.UPDATE_CONVERSATIONS_FETCHING_FLAG:
- return {
- ...state,
- chatHistory: {
- ...state.chatHistory,
- fetchingConversations: action.payload,
- },
- };
- case actionConstants.UPDATE_CONVERSATION_TITLE:
- const tempConvsList = [...state.chatHistory.list];
- const index = tempConvsList.findIndex(
- (obj) => obj.id === action.payload.id
- );
- if (index > -1) {
- tempConvsList[index].title = action.payload.newTitle;
- }
- return {
- ...state,
- chatHistory: {
- ...state.chatHistory,
- list: [...tempConvsList],
- },
- };
- case actionConstants.UPDATE_ON_CLEAR_ALL_CONVERSATIONS:
- return {
- ...state,
- chatHistory: {
- ...state.chatHistory,
- list: [],
- },
- chat: { ...state.chat, messages: [] },
- selectedConversationId: "",
- generatedConversationId: generateUUIDv4(),
- };
- case actionConstants.SHOW_CHATHISTORY_CONVERSATION:
- const tempConvList = [...state.chatHistory.list];
- const matchedIndex = tempConvList.findIndex(
- (obj) => obj.id === action.payload.id
- );
- if (matchedIndex > -1) {
- // Update the messages of the matched conversation
- tempConvList[matchedIndex].messages = action.payload.messages;
- }
- return {
- ...state,
- chat: { ...state.chat, messages: action.payload.messages },
- chatHistory: {
- ...state.chatHistory,
- list: tempConvList,
- },
- };
- case actionConstants.UPDATE_CHATHISTORY_CONVERSATION_FLAG:
- return {
- ...state,
- chatHistory: {
- ...state.chatHistory,
- isFetchingConvMessages: action.payload,
- },
- };
- case actionConstants.DELETE_CONVERSATION_FROM_LIST:
- const updatedChatHistoryList = state.chatHistory.list.filter(
- (conversation) => conversation.id !== action.payload
- );
- const isDeletedSelectedConv =
- action.payload === state.selectedConversationId;
-
- return {
- ...state,
- chatHistory: {
- ...state.chatHistory,
- list: updatedChatHistoryList,
- },
- selectedConversationId: isDeletedSelectedConv
- ? ""
- : state.selectedConversationId,
- chat: {
- ...state.chat,
- messages: isDeletedSelectedConv ? [] : state.chat.messages,
- userMessage: isDeletedSelectedConv ? "" : state.chat.userMessage,
- },
- };
- case actionConstants.STORE_COSMOS_INFO:
- return {
- ...state,
- cosmosInfo: action.payload,
- };
- case actionConstants.ADD_NEW_CONVERSATION_TO_CHAT_HISTORY:
- return {
- ...state,
- chatHistory: {
- ...state.chatHistory,
- list: [action.payload, ...state.chatHistory.list],
- },
- };
- case actionConstants.UPDATE_APP_SPINNER_STATUS:
- return {
- ...state,
- showAppSpinner: action.payload,
- };
- case actionConstants.UPDATE_HISTORY_UPDATE_API_FLAG:
- return {
- ...state,
- chatHistory: {
- ...state.chatHistory,
- isHistoryUpdateAPIPending: action.payload,
- },
- };
- case actionConstants.UPDATE_MESSAGE_BY_ID:
- const messageID = action.payload.id;
- // console.log("aaction::",action.payload)
- const matchIndex = state.chat.messages.findIndex(
- (obj) => String(obj.id) === String(messageID)
- );
- let tempMessages = [...state.chat.messages];
- if (matchIndex > -1) {
- tempMessages[matchIndex] = action.payload;
- } else {
- tempMessages = [...state.chat.messages, action.payload];
- }
- return {
- ...state,
- chat: {
- ...state.chat,
- messages: tempMessages,
- citations: "",
- isStreamingInProgress: true,
- },
- };
- case actionConstants.UPDATE_STREAMING_FLAG:
- return {
- ...state,
- chat: {
- ...state.chat,
- isStreamingInProgress: action.payload,
- },
- };
- case actionConstants.UPDATE_CHARTS_FETCHING_FLAG:
- return {
- ...state,
- dashboards: {
- ...state.dashboards,
- fetchingCharts: action.payload,
- },
- };
- case actionConstants.UPDATE_FILTERS_FETCHING_FLAG:
- return {
- ...state,
- dashboards: {
- ...state.dashboards,
- fetchingFilters: action.payload,
- },
- };
- case actionConstants.UPDATE_CITATION:
- return {
- ...state,
- citation: {
- ...state.citation,
- activeCitation: action.payload.activeCitation || state.citation.activeCitation,
- showCitation: action.payload.showCitation,
- currentConversationIdForCitation: action.payload?.currentConversationIdForCitation || state.citation.currentConversationIdForCitation,
- },
- };
- default:
- return state;
- }
-};
-
-export { appReducer };
diff --git a/src/App/src/state/hooks.ts b/src/App/src/state/hooks.ts
new file mode 100644
index 000000000..1705e0166
--- /dev/null
+++ b/src/App/src/state/hooks.ts
@@ -0,0 +1,5 @@
+import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
+import type { AppDispatch, RootState } from "./store";
+
+export const useAppDispatch = () => useDispatch();
+export const useAppSelector: TypedUseSelectorHook = useSelector;
diff --git a/src/App/src/state/slices/appSlice.ts b/src/App/src/state/slices/appSlice.ts
new file mode 100644
index 000000000..c0716978c
--- /dev/null
+++ b/src/App/src/state/slices/appSlice.ts
@@ -0,0 +1,81 @@
+import { createAsyncThunk, createSlice, type PayloadAction } from "@reduxjs/toolkit";
+import { generateUUIDv4 } from "../../configs/Utils";
+import { getLayoutConfig } from "../../api/api";
+import {
+ type AppConfig,
+ type ChartConfigItem,
+ type CosmosDBHealth,
+} from "../../types/AppTypes";
+
+export type AppSliceState = {
+ selectedConversationId: string;
+ generatedConversationId: string;
+ config: {
+ appConfig: AppConfig;
+ charts: ChartConfigItem[];
+ };
+ cosmosInfo: CosmosDBHealth;
+ showAppSpinner: boolean;
+};
+
+const initialState: AppSliceState = {
+ selectedConversationId: "",
+ generatedConversationId: generateUUIDv4(),
+ config: {
+ appConfig: null,
+ charts: [],
+ },
+ cosmosInfo: { cosmosDB: false, status: "" },
+ showAppSpinner: false,
+};
+
+export const fetchLayoutConfig = createAsyncThunk(
+ "app/fetchLayoutConfig",
+ async () => getLayoutConfig()
+);
+
+const appSlice = createSlice({
+ name: "app",
+ initialState,
+ reducers: {
+ setSelectedConversationId(state, action: PayloadAction) {
+ state.selectedConversationId = action.payload;
+ },
+ setGeneratedConversationId(state, action: PayloadAction) {
+ state.generatedConversationId = action.payload;
+ },
+ startNewConversation(state) {
+ state.selectedConversationId = "";
+ state.generatedConversationId = generateUUIDv4();
+ },
+ setShowAppSpinner(state, action: PayloadAction) {
+ state.showAppSpinner = action.payload;
+ },
+ setCosmosInfo(state, action: PayloadAction) {
+ state.cosmosInfo = action.payload;
+ },
+ setConfig(
+ state,
+ action: PayloadAction<{ appConfig: AppConfig; charts: ChartConfigItem[] }>
+ ) {
+ state.config = action.payload;
+ },
+ },
+ extraReducers: (builder) => {
+ builder
+ .addCase(fetchLayoutConfig.fulfilled, (state, action) => {
+ state.config = action.payload;
+ });
+ },
+});
+
+export const {
+ setSelectedConversationId,
+ setGeneratedConversationId,
+ startNewConversation,
+ setShowAppSpinner,
+ setCosmosInfo,
+ setConfig,
+} = appSlice.actions;
+
+export default appSlice.reducer;
diff --git a/src/App/src/state/slices/chatHistorySlice.ts b/src/App/src/state/slices/chatHistorySlice.ts
new file mode 100644
index 000000000..d1e1dd86e
--- /dev/null
+++ b/src/App/src/state/slices/chatHistorySlice.ts
@@ -0,0 +1,223 @@
+import {
+ createAsyncThunk,
+ createSlice,
+ type PayloadAction,
+} from "@reduxjs/toolkit";
+import {
+ historyDelete,
+ historyDeleteAll,
+ historyList,
+ historyRead,
+ historyRename,
+} from "../../api/api";
+import { type ChatMessage, type Conversation } from "../../types/AppTypes";
+
+export type ChatHistoryState = {
+ list: Conversation[];
+ fetchingConversations: boolean;
+ isFetchingConvMessages: boolean;
+ isHistoryUpdateAPIPending: boolean;
+};
+
+const initialState: ChatHistoryState = {
+ list: [],
+ fetchingConversations: false,
+ isFetchingConvMessages: false,
+ isHistoryUpdateAPIPending: false,
+};
+
+export const fetchConversations = createAsyncThunk<
+ Conversation[],
+ number,
+ { rejectValue: string }
+>("chatHistory/fetchConversations", async (offset, { rejectWithValue }) => {
+ const conversations = await historyList(offset);
+
+ if (!conversations) {
+ return rejectWithValue("Unable to load conversations.");
+ }
+
+ return conversations;
+});
+
+export const fetchConversationMessages = createAsyncThunk<
+ { id: string; messages: ChatMessage[] },
+ string,
+ { rejectValue: string }
+>(
+ "chatHistory/fetchConversationMessages",
+ async (conversationId, { rejectWithValue }) => {
+ try {
+ const messages = await historyRead(conversationId);
+ return { id: conversationId, messages };
+ } catch {
+ return rejectWithValue("Unable to load conversation messages.");
+ }
+ }
+);
+
+export const renameConversation = createAsyncThunk<
+ { id: string; newTitle: string },
+ { id: string; newTitle: string },
+ { rejectValue: string }
+>("chatHistory/renameConversation", async ({ id, newTitle }, { rejectWithValue }) => {
+ const response = await historyRename(id, newTitle);
+
+ if (!response.ok) {
+ return rejectWithValue("Unable to rename conversation.");
+ }
+
+ return { id, newTitle };
+});
+
+export const deleteConversation = createAsyncThunk<
+ string,
+ string,
+ { rejectValue: string }
+>("chatHistory/deleteConversation", async (conversationId, { rejectWithValue }) => {
+ const response = await historyDelete(conversationId);
+
+ if (!response.ok) {
+ return rejectWithValue("Unable to delete conversation.");
+ }
+
+ return conversationId;
+});
+
+export const clearAllConversations = createAsyncThunk<
+ void,
+ void,
+ { rejectValue: string }
+>("chatHistory/clearAllConversations", async (_, { rejectWithValue }) => {
+ const response = await historyDeleteAll();
+
+ if (!response.ok) {
+ return rejectWithValue("Unable to clear chat history.");
+ }
+});
+
+const chatHistorySlice = createSlice({
+ name: "chatHistory",
+ initialState,
+ reducers: {
+ setFetchingConversations(state, action: PayloadAction) {
+ state.fetchingConversations = action.payload;
+ },
+ setConversationMessagesFetching(state, action: PayloadAction) {
+ state.isFetchingConvMessages = action.payload;
+ },
+ setHistoryUpdateApiPending(state, action: PayloadAction) {
+ state.isHistoryUpdateAPIPending = action.payload;
+ },
+ addConversationToHistory(state, action: PayloadAction) {
+ const existingConversation = state.list.find(
+ (conversation) => conversation.id === action.payload.id
+ );
+
+ if (!existingConversation) {
+ state.list.unshift(action.payload);
+ }
+ },
+ appendConversations(state, action: PayloadAction) {
+ const existingIds = new Set(state.list.map((conversation) => conversation.id));
+ action.payload.forEach((conversation) => {
+ if (!existingIds.has(conversation.id)) {
+ state.list.push(conversation);
+ existingIds.add(conversation.id);
+ }
+ });
+ },
+ updateConversationTitleInState(
+ state,
+ action: PayloadAction<{ id: string; newTitle: string }>
+ ) {
+ const targetConversation = state.list.find(
+ (conversation) => conversation.id === action.payload.id
+ );
+
+ if (targetConversation) {
+ targetConversation.title = action.payload.newTitle;
+ }
+ },
+ removeConversationFromList(state, action: PayloadAction) {
+ state.list = state.list.filter(
+ (conversation) => conversation.id !== action.payload
+ );
+ },
+ clearChatHistoryState(state) {
+ state.list = [];
+ state.fetchingConversations = false;
+ state.isFetchingConvMessages = false;
+ state.isHistoryUpdateAPIPending = false;
+ },
+ },
+ extraReducers: (builder) => {
+ builder
+ .addCase(fetchConversations.pending, (state) => {
+ state.fetchingConversations = true;
+ })
+ .addCase(fetchConversations.fulfilled, (state, action) => {
+ state.fetchingConversations = false;
+ const existingIds = new Set(state.list.map((conversation) => conversation.id));
+
+ action.payload.forEach((conversation) => {
+ if (!existingIds.has(conversation.id)) {
+ state.list.push(conversation);
+ existingIds.add(conversation.id);
+ }
+ });
+ })
+ .addCase(fetchConversations.rejected, (state) => {
+ state.fetchingConversations = false;
+ })
+ .addCase(fetchConversationMessages.pending, (state) => {
+ state.isFetchingConvMessages = true;
+ })
+ .addCase(fetchConversationMessages.fulfilled, (state, action) => {
+ state.isFetchingConvMessages = false;
+ const conversation = state.list.find(
+ (item) => item.id === action.payload.id
+ );
+
+ if (conversation) {
+ conversation.messages = action.payload.messages;
+ }
+ })
+ .addCase(fetchConversationMessages.rejected, (state) => {
+ state.isFetchingConvMessages = false;
+ })
+ .addCase(renameConversation.fulfilled, (state, action) => {
+ const conversation = state.list.find(
+ (item) => item.id === action.payload.id
+ );
+
+ if (conversation) {
+ conversation.title = action.payload.newTitle;
+ }
+ })
+ .addCase(deleteConversation.fulfilled, (state, action) => {
+ state.list = state.list.filter(
+ (conversation) => conversation.id !== action.payload
+ );
+ })
+ .addCase(clearAllConversations.fulfilled, (state) => {
+ state.list = [];
+ state.fetchingConversations = false;
+ state.isFetchingConvMessages = false;
+ state.isHistoryUpdateAPIPending = false;
+ });
+ },
+});
+
+export const {
+ setFetchingConversations,
+ setConversationMessagesFetching,
+ setHistoryUpdateApiPending,
+ addConversationToHistory,
+ appendConversations,
+ updateConversationTitleInState,
+ removeConversationFromList,
+ clearChatHistoryState,
+} = chatHistorySlice.actions;
+
+export default chatHistorySlice.reducer;
diff --git a/src/App/src/state/slices/chatSlice.ts b/src/App/src/state/slices/chatSlice.ts
new file mode 100644
index 000000000..9692dc7fe
--- /dev/null
+++ b/src/App/src/state/slices/chatSlice.ts
@@ -0,0 +1,74 @@
+import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
+import { type ChatMessage } from "../../types/AppTypes";
+
+export type ChatState = {
+ generatingResponse: boolean;
+ messages: ChatMessage[];
+ userMessage: string;
+ isStreamingInProgress: boolean;
+ citations: string | null;
+};
+
+const initialState: ChatState = {
+ generatingResponse: false,
+ messages: [],
+ userMessage: "",
+ citations: "",
+ isStreamingInProgress: false,
+};
+
+const chatSlice = createSlice({
+ name: "chat",
+ initialState,
+ reducers: {
+ setGeneratingResponse(state, action: PayloadAction) {
+ state.generatingResponse = action.payload;
+ },
+ setMessages(state, action: PayloadAction) {
+ state.messages = action.payload;
+ },
+ appendMessages(state, action: PayloadAction) {
+ state.messages.push(...action.payload);
+ },
+ setUserMessage(state, action: PayloadAction) {
+ state.userMessage = action.payload;
+ },
+ updateMessageById(state, action: PayloadAction) {
+ const messageIndex = state.messages.findIndex(
+ (message) => message.id === action.payload.id
+ );
+
+ if (messageIndex === -1) {
+ state.messages.push(action.payload);
+ return;
+ }
+
+ state.messages[messageIndex] = {
+ ...state.messages[messageIndex],
+ ...action.payload,
+ };
+ },
+ setStreamingInProgress(state, action: PayloadAction) {
+ state.isStreamingInProgress = action.payload;
+ },
+ setChatCitations(state, action: PayloadAction) {
+ state.citations = action.payload;
+ },
+ resetChatState() {
+ return { ...initialState };
+ },
+ },
+});
+
+export const {
+ setGeneratingResponse,
+ setMessages,
+ appendMessages,
+ setUserMessage,
+ updateMessageById,
+ setStreamingInProgress,
+ setChatCitations,
+ resetChatState,
+} = chatSlice.actions;
+
+export default chatSlice.reducer;
diff --git a/src/App/src/state/slices/citationSlice.ts b/src/App/src/state/slices/citationSlice.ts
new file mode 100644
index 000000000..10429c7ce
--- /dev/null
+++ b/src/App/src/state/slices/citationSlice.ts
@@ -0,0 +1,32 @@
+import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
+
+export type CitationState = {
+ activeCitation?: unknown;
+ showCitation: boolean;
+ currentConversationIdForCitation?: string;
+};
+
+const initialState: CitationState = {
+ activeCitation: null,
+ showCitation: false,
+ currentConversationIdForCitation: "",
+};
+
+const citationSlice = createSlice({
+ name: "citation",
+ initialState,
+ reducers: {
+ setCitationState(state, action: PayloadAction>) {
+ Object.assign(state, action.payload);
+ },
+ hideCitation(state) {
+ state.activeCitation = null;
+ state.showCitation = false;
+ state.currentConversationIdForCitation = "";
+ },
+ },
+});
+
+export const { setCitationState, hideCitation } = citationSlice.actions;
+
+export default citationSlice.reducer;
diff --git a/src/App/src/state/slices/dashboardSlice.ts b/src/App/src/state/slices/dashboardSlice.ts
new file mode 100644
index 000000000..d389d71db
--- /dev/null
+++ b/src/App/src/state/slices/dashboardSlice.ts
@@ -0,0 +1,75 @@
+import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
+import {
+ type ChartConfigItem,
+ type FilterMetaData,
+ type SelectedFilters,
+} from "../../types/AppTypes";
+import { defaultSelectedFilters } from "../../configs/Utils";
+
+export type DashboardState = {
+ filtersMetaFetched: boolean;
+ initialChartsDataFetched: boolean;
+ filtersMeta: FilterMetaData;
+ charts: ChartConfigItem[];
+ selectedFilters: SelectedFilters;
+ fetchingFilters: boolean;
+ fetchingCharts: boolean;
+};
+
+const initialState: DashboardState = {
+ filtersMetaFetched: false,
+ initialChartsDataFetched: false,
+ filtersMeta: {
+ Sentiment: [],
+ Topic: [],
+ DateRange: [],
+ },
+ charts: [],
+ selectedFilters: { ...defaultSelectedFilters },
+ fetchingCharts: true,
+ fetchingFilters: true,
+};
+
+const dashboardSlice = createSlice({
+ name: "dashboards",
+ initialState,
+ reducers: {
+ setFiltersMeta(state, action: PayloadAction) {
+ state.filtersMeta = action.payload;
+ },
+ setFiltersMetaFetched(state, action: PayloadAction) {
+ state.filtersMetaFetched = action.payload;
+ },
+ setChartsData(state, action: PayloadAction) {
+ state.charts = action.payload;
+ },
+ setInitialChartsDataFetched(state, action: PayloadAction) {
+ state.initialChartsDataFetched = action.payload;
+ },
+ setSelectedFilters(state, action: PayloadAction) {
+ state.selectedFilters = action.payload;
+ },
+ setFetchingCharts(state, action: PayloadAction) {
+ state.fetchingCharts = action.payload;
+ },
+ setFetchingFilters(state, action: PayloadAction) {
+ state.fetchingFilters = action.payload;
+ },
+ resetSelectedFilters(state) {
+ state.selectedFilters = { ...defaultSelectedFilters };
+ },
+ },
+});
+
+export const {
+ setFiltersMeta,
+ setFiltersMetaFetched,
+ setChartsData,
+ setInitialChartsDataFetched,
+ setSelectedFilters,
+ setFetchingCharts,
+ setFetchingFilters,
+ resetSelectedFilters,
+} = dashboardSlice.actions;
+
+export default dashboardSlice.reducer;
diff --git a/src/App/src/state/store.ts b/src/App/src/state/store.ts
new file mode 100644
index 000000000..c26465046
--- /dev/null
+++ b/src/App/src/state/store.ts
@@ -0,0 +1,20 @@
+import { configureStore } from "@reduxjs/toolkit";
+import appReducer from "./slices/appSlice";
+import chatReducer from "./slices/chatSlice";
+import citationReducer from "./slices/citationSlice";
+import chatHistoryReducer from "./slices/chatHistorySlice";
+import dashboardReducer from "./slices/dashboardSlice";
+
+export const store = configureStore({
+ reducer: {
+ app: appReducer,
+ dashboards: dashboardReducer,
+ chat: chatReducer,
+ citation: citationReducer,
+ chatHistory: chatHistoryReducer,
+ },
+ devTools: process.env.NODE_ENV !== "production",
+});
+
+export type RootState = ReturnType;
+export type AppDispatch = typeof store.dispatch;
diff --git a/src/App/src/state/useAppContext.tsx b/src/App/src/state/useAppContext.tsx
deleted file mode 100644
index 8982574fa..000000000
--- a/src/App/src/state/useAppContext.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import { useContext } from "react";
-import { AppContext } from "./AppProvider";
-
-export const useAppContext = () => {
- return useContext(AppContext);
-};
diff --git a/src/App/src/types/AppTypes.ts b/src/App/src/types/AppTypes.ts
index 0db22ccdb..50777123b 100644
--- a/src/App/src/types/AppTypes.ts
+++ b/src/App/src/types/AppTypes.ts
@@ -1,5 +1,3 @@
-import { ReactNode } from "react";
-
export type FilterObject = {
key: string;
displayValue: string;
@@ -7,8 +5,6 @@ export type FilterObject = {
export type FilterMetaData = Record;
export type SelectedFilters = Record;
-type Roles = "assistant" | "user" | "error";
-
export enum Feedback {
Neutral = "neutral",
Positive = "positive",
@@ -145,16 +141,16 @@ export type HistoryMetaData = {
export type ParsedChunk = {
error?: string;
- choices: [
- {
- messages: [
- {
- content: string;
- role: string;
- }
- ];
- }
- ];
+ choices?: Array<{
+ messages?: Array<{
+ content: string;
+ role: string;
+ }>;
+ delta?: {
+ content: string;
+ role: "assistant" | "tool";
+ };
+ }>;
};
export type ToolMessageContent = {
diff --git a/src/App/src/utils/apiUtils.ts b/src/App/src/utils/apiUtils.ts
new file mode 100644
index 000000000..a4aa86f54
--- /dev/null
+++ b/src/App/src/utils/apiUtils.ts
@@ -0,0 +1,94 @@
+export const createErrorResponse = (
+ status = 500,
+ message = "Request failed."
+): Response =>
+ new Response(JSON.stringify({ error: message }), {
+ status,
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+export async function retryRequest(
+ operation: () => Promise,
+ retries = 2,
+ baseDelayMs = 300
+): Promise {
+ let lastError: unknown;
+
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
+ try {
+ return await operation();
+ } catch (error) {
+ lastError = error;
+
+ if (attempt === retries) {
+ break;
+ }
+
+ await new Promise((resolve) => setTimeout(resolve, baseDelayMs * 2 ** attempt));
+ }
+ }
+
+ throw lastError instanceof Error
+ ? lastError
+ : new Error("Request retry limit exceeded.");
+}
+
+export class RequestCache {
+ private readonly cache = new Map>();
+
+ getOrCreate(key: string, factory: () => Promise): Promise {
+ if (!this.cache.has(key)) {
+ this.cache.set(key, factory());
+ }
+
+ return this.cache.get(key)!;
+ }
+
+ clear(key?: string) {
+ if (key) {
+ this.cache.delete(key);
+ return;
+ }
+
+ this.cache.clear();
+ }
+}
+
+export function debounce void>(
+ callback: T,
+ delay: number
+) {
+ let timeoutId: ReturnType | undefined;
+
+ return (...args: Parameters) => {
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+
+ timeoutId = setTimeout(() => {
+ callback(...args);
+ }, delay);
+ };
+}
+
+export function throttle void>(
+ callback: T,
+ wait: number
+) {
+ let isThrottled = false;
+
+ return (...args: Parameters) => {
+ if (isThrottled) {
+ return;
+ }
+
+ callback(...args);
+ isThrottled = true;
+
+ setTimeout(() => {
+ isThrottled = false;
+ }, wait);
+ };
+}
diff --git a/src/App/src/utils/chartUtils.ts b/src/App/src/utils/chartUtils.ts
new file mode 100644
index 000000000..4830d01f0
--- /dev/null
+++ b/src/App/src/utils/chartUtils.ts
@@ -0,0 +1,41 @@
+import { type ChartDataResponse } from "../types/AppTypes";
+
+export const parseChartContent = (content: string): unknown => {
+ try {
+ const parsedResponse = JSON.parse(content);
+
+ if (parsedResponse?.error) {
+ return parsedResponse.error;
+ }
+
+ if (parsedResponse?.object) {
+ return parsedResponse.object;
+ }
+
+ return parsedResponse;
+ } catch {
+ return content;
+ }
+};
+
+export const hasChartContent = (content: unknown): content is ChartDataResponse => {
+ if (!content || typeof content === "string") {
+ return false;
+ }
+
+ const chartContent = content as ChartDataResponse;
+ return Boolean(chartContent.type && chartContent.data);
+};
+
+export const getSentimentColor = (label: string): string => {
+ switch (label.toLowerCase()) {
+ case "positive":
+ return "#6576F9";
+ case "neutral":
+ return "#B2BBFC";
+ case "negative":
+ return "#FF749B";
+ default:
+ return "#ccc";
+ }
+};
diff --git a/src/App/src/utils/messageUtils.ts b/src/App/src/utils/messageUtils.ts
new file mode 100644
index 000000000..0e85a7dcd
--- /dev/null
+++ b/src/App/src/utils/messageUtils.ts
@@ -0,0 +1,48 @@
+import { type Citation } from "../types/AppTypes";
+
+const CHART_KEYWORDS = ["chart", "graph", "visualize", "plot"];
+
+export const isChartQuery = (query: string) =>
+ CHART_KEYWORDS.some((keyword) => query.toLowerCase().includes(keyword));
+
+export const parseCitationFromMessage = (message: unknown): Citation[] => {
+ if (!message) {
+ return [];
+ }
+
+ try {
+ let parsedMessage: any;
+
+ if (typeof message === "string") {
+ if (message.trim().startsWith('"citations":')) {
+ const wrappedMessage = `{${message.trim()}}`;
+ parsedMessage = JSON.parse(wrappedMessage.replace(/\}\}$/, "}"));
+ } else {
+ parsedMessage = JSON.parse(message);
+ }
+ } else {
+ parsedMessage = message;
+ }
+
+ if (Array.isArray(parsedMessage)) {
+ return parsedMessage.map((item: any, index: number) => ({
+ content: item.content || "",
+ id: String(index + 1),
+ title: item.title || null,
+ filepath: item.filepath || null,
+ url: item.url || null,
+ metadata: item.metadata || null,
+ chunk_id: item.chunk_id || null,
+ reindex_id: String(index + 1),
+ }));
+ }
+
+ if (Array.isArray(parsedMessage?.citations)) {
+ return parsedMessage.citations;
+ }
+ } catch {
+ return [];
+ }
+
+ return [];
+};
diff --git a/src/api/.env.sample b/src/api/.env.sample
index 956b87146..3a76dacf9 100644
--- a/src/api/.env.sample
+++ b/src/api/.env.sample
@@ -16,10 +16,6 @@ AZURE_COSMOSDB_ACCOUNT=
AZURE_COSMOSDB_CONVERSATIONS_CONTAINER="conversations"
AZURE_COSMOSDB_DATABASE="db_conversation_history"
AZURE_COSMOSDB_ENABLE_FEEDBACK="True"
-AZURE_OPENAI_API_VERSION=
-AZURE_OPENAI_DEPLOYMENT_MODEL=
-AZURE_OPENAI_ENDPOINT=
-AZURE_OPENAI_RESOURCE=
DISPLAY_CHART_DEFAULT="False"
REACT_APP_LAYOUT_CONFIG="{\n \"appConfig\": {\n \"THREE_COLUMN\": {\n \"DASHBOARD\": 50,\n \"CHAT\": 33,\n \"CHATHISTORY\": 17\n },\n \"TWO_COLUMN\": {\n \"DASHBOARD_CHAT\": {\n \"DASHBOARD\": 65,\n \"CHAT\": 35\n },\n \"CHAT_CHATHISTORY\": {\n \"CHAT\": 80,\n \"CHATHISTORY\": 20\n }\n }\n },\n \"charts\": [\n {\n \"id\": \"SATISFIED\",\n \"name\": \"Satisfied\",\n \"type\": \"card\",\n \"layout\": { \"row\": 1, \"column\": 1, \"height\": 11 }\n },\n {\n \"id\": \"TOTAL_CALLS\",\n \"name\": \"Total Calls\",\n \"type\": \"card\",\n \"layout\": { \"row\": 1, \"column\": 2, \"span\": 1 }\n },\n {\n \"id\": \"AVG_HANDLING_TIME\",\n \"name\": \"Average Handling Time\",\n \"type\": \"card\",\n \"layout\": { \"row\": 1, \"column\": 3, \"span\": 1 }\n },\n {\n \"id\": \"SENTIMENT\",\n \"name\": \"Topics Overview\",\n \"type\": \"donutchart\",\n \"layout\": { \"row\": 2, \"column\": 1, \"width\": 40, \"height\": 44.5 }\n },\n {\n \"id\": \"AVG_HANDLING_TIME_BY_TOPIC\",\n \"name\": \"Average Handling Time By Topic\",\n \"type\": \"bar\",\n \"layout\": { \"row\": 2, \"column\": 2, \"row-span\": 2, \"width\": 60 }\n },\n {\n \"id\": \"TOPICS\",\n \"name\": \"Trending Topics\",\n \"type\": \"table\",\n \"layout\": { \"row\": 3, \"column\": 1, \"span\": 2 }\n },\n {\n \"id\": \"KEY_PHRASES\",\n \"name\": \"Key Phrases\",\n \"type\": \"wordcloud\",\n \"layout\": { \"row\": 3, \"column\": 2, \"height\": 44.5 }\n }\n ]\n}"
RESOURCE_GROUP_NAME=
diff --git a/src/api/api/history_routes.py b/src/api/api/history_routes.py
index 9086200a7..3c79f3929 100644
--- a/src/api/api/history_routes.py
+++ b/src/api/api/history_routes.py
@@ -387,8 +387,22 @@ async def delete_all_conversations(request: Request):
user_id = authenticated_user["user_principal_id"]
logger.info("DELETE /history/delete_all called")
- # Get all user conversations
+ # Get all user conversations.
+ # NOTE: After the web page sits idle for >10 minutes, the first call
+ # to Cosmos DB from this long-lived process can fail with a transient
+ # connection-reset / token-expiry error. `history_service.get_conversations`
+ # swallows that exception and returns []. Without the retry below,
+ # "Clear all chat history" would then 404 -> get masked to 500 even
+ # though chats actually exist. Single-conversation delete is unaffected
+ # because it does not pre-list conversations. Retry once before giving up.
conversations = await history_service.get_conversations(user_id, offset=0, limit=None)
+ if not conversations:
+ logger.info(
+ "delete_all: initial get_conversations returned empty; "
+ "retrying once to recover from possible idle-connection failure"
+ )
+ conversations = await history_service.get_conversations(user_id, offset=0, limit=None)
+
if not conversations:
track_event_if_configured("DeleteAllConversationsNotFound", {
"user_id": user_id
@@ -413,6 +427,10 @@ async def delete_all_conversations(request: Request):
status_code=200,
)
+ except HTTPException:
+ # Let FastAPI translate HTTPException to its proper status code
+ # instead of masking it as a 500 below.
+ raise
except Exception as e:
logger.exception("Exception in /history/delete_all: %s", str(e))
track_event_if_configured("AllConversationsDeleteError", {
diff --git a/src/api/common/config/config.py b/src/api/common/config/config.py
index 2c61d918e..e2af591ec 100644
--- a/src/api/common/config/config.py
+++ b/src/api/common/config/config.py
@@ -19,12 +19,6 @@ def __init__(self):
self.driver = "{ODBC Driver 18 for SQL Server}"
self.mid_id = os.getenv("SQLDB_USER_MID")
- # Azure OpenAI configuration
- self.azure_openai_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
- self.azure_openai_deployment_model = os.getenv("AZURE_OPENAI_DEPLOYMENT_MODEL")
- self.azure_openai_api_version = os.getenv("AZURE_OPENAI_API_VERSION")
- self.azure_openai_resource = os.getenv("AZURE_OPENAI_RESOURCE")
-
# Azure AI Search configuration
self.azure_ai_search_endpoint = os.getenv("AZURE_AI_SEARCH_ENDPOINT")
self.azure_ai_search_api_key = os.getenv("AZURE_AI_SEARCH_API_KEY")
diff --git a/src/api/common/database/cosmosdb_service.py b/src/api/common/database/cosmosdb_service.py
index 7a3dab8e8..62950e1fd 100644
--- a/src/api/common/database/cosmosdb_service.py
+++ b/src/api/common/database/cosmosdb_service.py
@@ -115,7 +115,7 @@ async def delete_messages(self, conversation_id, user_id):
item=message["id"], partition_key=user_id
)
response_list.append(resp)
- return response_list
+ return response_list
async def get_conversations(self, user_id, limit, sort_order="DESC", offset=0):
parameters = [{"name": "@userId", "value": user_id}]
diff --git a/src/api/common/database/sqldb_service.py b/src/api/common/database/sqldb_service.py
index 294b93cb6..3b7ca02bd 100644
--- a/src/api/common/database/sqldb_service.py
+++ b/src/api/common/database/sqldb_service.py
@@ -64,6 +64,7 @@ async def get_db_connection():
if conn is None:
raise RuntimeError("Unable to connect using ODBC Driver 18 or 17 with Azure Credential")
+ return conn
except Exception as e:
logging.error("Failed with Azure Credential: %s", str(e))
raise RuntimeError("Unable to connect to SQL database using Microsoft Entra authentication.") from e
@@ -179,7 +180,7 @@ async def fetch_chart_data(chart_filters: ChartFilters = ''):
req_body = ''
try:
req_body = chart_filters.model_dump()
- except BaseException:
+ except Exception: # model_dump may fail if filters are empty or invalid
pass
if req_body != '':
where_clause = ''
diff --git a/src/api/helpers/azure_credential_utils.py b/src/api/helpers/azure_credential_utils.py
index cdf7ce6d1..357fb2a8d 100644
--- a/src/api/helpers/azure_credential_utils.py
+++ b/src/api/helpers/azure_credential_utils.py
@@ -39,3 +39,16 @@ def get_azure_credential(client_id=None):
return AzureCliCredential()
else:
return ManagedIdentityCredential(client_id=client_id)
+
+
+def build_async_azure_credential(client_id=None):
+ """
+ Synchronously builds an async Azure credential suitable for async SDK clients
+ (e.g., azure.cosmos.aio.CosmosClient). Use this from sync constructors that
+ need to hand an async-capable credential to an async client; use
+ get_azure_credential_async() instead from `async with` blocks.
+ """
+ if os.getenv("APP_ENV", "prod").lower() == 'dev':
+ return AioAzureCliCredential()
+ else:
+ return AioManagedIdentityCredential(client_id=client_id)
diff --git a/src/api/helpers/azure_openai_helper.py b/src/api/helpers/azure_openai_helper.py
deleted file mode 100644
index 8db11edc9..000000000
--- a/src/api/helpers/azure_openai_helper.py
+++ /dev/null
@@ -1,26 +0,0 @@
-
-"""
-Helper functions for initializing and managing Azure OpenAI client instances.
-"""
-
-import openai
-from azure.identity import get_bearer_token_provider
-from helpers.azure_credential_utils import get_azure_credential
-from common.config.config import Config
-
-
-def get_azure_openai_client():
- """
- Initializes and returns an Azure OpenAI client using a bearer token provider.
- """
-
- config = Config()
- token_provider = get_bearer_token_provider(
- get_azure_credential(client_id=config.azure_client_id), "https://cognitiveservices.azure.com/.default"
- )
- client = openai.AzureOpenAI(
- azure_endpoint=config.azure_openai_endpoint,
- api_version=config.azure_openai_api_version,
- azure_ad_token_provider=token_provider,
- )
- return client
diff --git a/src/api/helpers/chat_helper.py b/src/api/helpers/chat_helper.py
deleted file mode 100644
index d7df3b1ab..000000000
--- a/src/api/helpers/chat_helper.py
+++ /dev/null
@@ -1,74 +0,0 @@
-"""Helper functions for processing RAG responses and generating Chart.js-compatible chart data using Azure OpenAI."""
-
-import json
-import time
-import uuid
-import logging
-from helpers.azure_openai_helper import get_azure_openai_client
-from common.config.config import Config
-
-# Configure logger
-logger = logging.getLogger(__name__)
-logger.setLevel(logging.INFO)
-
-
-def process_rag_response(rag_response, query):
- """
- Parses the RAG response dynamically to extract chart data for Chart.js.
- """
- try:
- config = Config()
- client = get_azure_openai_client()
- system_prompt = """You are an assistant that helps generate valid chart data to be shown using chart.js with version 4.4.4 compatible.
- Include chart type and chart options.
- Pick the best chart type for given data.
- Do not generate a chart unless the input contains some numbers. Otherwise return a message that Chart cannot be generated.
- Only return a valid JSON output and nothing else.
- Verify that the generated JSON can be parsed using json.loads.
- Do not include tooltip callbacks in JSON.
- Always make sure that the generated json can be rendered in chart.js.
- Always remove any extra trailing commas.
- Verify and refine that JSON should not have any syntax errors like extra closing brackets.
- Ensure Y-axis labels are fully visible by increasing **ticks.padding**, **ticks.maxWidth**, or enabling word wrapping where necessary.
- Ensure bars and data points are evenly spaced and not squished or cropped at **100%** resolution by maintaining appropriate **barPercentage** and **categoryPercentage** values."""
- user_prompt = f"""Generate chart data for -
- {query}
- {rag_response}
- """
- logger.info("Processing chart data for response: %s", rag_response)
- completion = client.chat.completions.create(
- model=config.azure_openai_deployment_model,
- messages=[
- {"role": "system", "content": system_prompt},
- {"role": "user", "content": user_prompt},
- ],
- temperature=0,
- )
- chart_data = completion.choices[0].message.content.strip().replace("```json", "").replace("```", "")
- logger.info("Generated chart data: %s", chart_data)
- return json.loads(chart_data)
- except Exception as e:
- logger.error("Error processing RAG response: %s", str(e))
- return {"error": "Chart could not be generated from this data. Please ask a different question."}
-
-
-async def complete_chat_request(query, last_rag_response=None):
- """
- Completes a chat request by generating a chart from the RAG response.
- """
- if not last_rag_response:
- return {"error": "A previous RAG response is required to generate a chart."}
- # Process RAG response to generate chart data
- chart_data = process_rag_response(last_rag_response, query)
- if not chart_data or "error" in chart_data:
- return {
- "error": "Chart could not be generated from this data. Please ask a different question.",
- "error_desc": str(chart_data),
- }
- logger.info("Successfully generated chart data.")
- return {
- "id": str(uuid.uuid4()),
- "model": "azure-openai",
- "created": int(time.time()),
- "object": chart_data,
- }
diff --git a/src/api/requirements.txt b/src/api/requirements.txt
index 07f563955..27307e234 100644
--- a/src/api/requirements.txt
+++ b/src/api/requirements.txt
@@ -1,18 +1,18 @@
# Base packages
cachetools==6.2.6
-python-dotenv==1.2.1
-fastapi==0.128.0
-uvicorn[standard]==0.40.0
-pydantic[email]==2.11.10
+python-dotenv==1.2.2
+fastapi==0.136.0
+uvicorn[standard]==0.44.0
+pydantic[email]==2.13.2
# Azure SDK Core
-azure-core==1.38.0
-requests==2.32.5
-types-requests==2.32.4.20260107
-aiohttp==3.13.3
+azure-core==1.39.0
+requests==2.33.1
+types-requests==2.33.0.20260408
+aiohttp==3.13.5
# Azure Services
-azure-identity==1.25.2
+azure-identity==1.25.3
azure-search-documents==11.6.0
azure-ai-projects==2.0.0b3
azure-ai-agents==1.2.0b5
@@ -21,20 +21,20 @@ agent-framework-azure-ai==1.0.0rc2
azure-cosmos==4.15.0
# Additional utilities
-openai==2.24.0
+openai==2.32.0
pyodbc==5.3.0
-pandas==3.0.1
+pandas==3.0.2
-opentelemetry-exporter-otlp-proto-grpc==1.39.0
-opentelemetry-exporter-otlp-proto-http==1.39.0
+opentelemetry-exporter-otlp-proto-grpc==1.40.0
+opentelemetry-exporter-otlp-proto-http==1.40.0
azure-monitor-events-extension==0.1.0
-opentelemetry-sdk==1.39.0
-opentelemetry-api==1.39.0
-opentelemetry-semantic-conventions==0.60b0
-opentelemetry-instrumentation==0.60b0
-azure-monitor-opentelemetry==1.8.3
+opentelemetry-sdk==1.40.0
+opentelemetry-api==1.40.0
+opentelemetry-semantic-conventions==0.61b0
+opentelemetry-instrumentation==0.61b0
+azure-monitor-opentelemetry==1.8.7
# Development tools
-pytest==9.0.2
-pytest-cov==7.0.0
+pytest==9.0.3
+pytest-cov==7.1.0
pytest-asyncio==1.3.0
diff --git a/src/api/services/chat_service.py b/src/api/services/chat_service.py
index cbe89a758..ca972f898 100644
--- a/src/api/services/chat_service.py
+++ b/src/api/services/chat_service.py
@@ -12,13 +12,13 @@
import os
import random
import re
+from typing import AsyncGenerator
from common.logging.event_utils import track_event_if_configured
from helpers.azure_credential_utils import get_azure_credential_async
from common.database.sqldb_service import SQLTool, get_db_connection as get_sqldb_connection
from fastapi import HTTPException, status
-from fastapi.responses import StreamingResponse
from azure.ai.projects.aio import AIProjectClient
@@ -105,7 +105,6 @@ class ChatService:
def __init__(self):
config = Config()
- self.azure_openai_deployment_name = config.azure_openai_deployment_model
self.orchestrator_agent_name = config.orchestrator_agent_name
self.azure_client_id = config.azure_client_id
self.ai_project_endpoint = config.ai_project_endpoint
@@ -117,9 +116,12 @@ def get_thread_cache(self):
thread_cache = ExpCache(maxsize=1000, ttl=3600.0)
return thread_cache
- async def stream_openai_text(self, conversation_id: str, query: str, user_id: str = "") -> StreamingResponse:
+ async def stream_openai_text(self, conversation_id: str, query: str, user_id: str = "") -> AsyncGenerator[tuple[str, str], None]:
"""
Get a streaming text response from OpenAI.
+
+ Yields:
+ tuple[str, str]: (role, content) tuples where role is "assistant" or "tool"
"""
logger.info("stream_openai_text called: conversation_id=%s, query_length=%d",
conversation_id, len(query) if query else 0)
@@ -129,6 +131,7 @@ async def stream_openai_text(self, conversation_id: str, query: str, user_id: st
):
complete_response = ""
db_conn = None
+ had_error = False
try:
if not query:
query = "Please provide a query."
@@ -155,7 +158,6 @@ async def stream_openai_text(self, conversation_id: str, query: str, user_id: st
logger.info("Orchestrator agent retrieved successfully: '%s'", self.orchestrator_agent_name)
citations = []
- first_chunk = True
citation_marker_map = {} # Maps original markers to sequential numbers
citation_counter = 0
@@ -186,17 +188,12 @@ def replace_citation_marker(match):
chunk_text = str(chunk.text) if chunk.text else ""
- # Replace complete citation markers like 【4:0†source】 with [1], [2], etc.
- chunk_text = re.sub(r'【\d+:\d+†[^】]+】', replace_citation_marker, chunk_text)
+ # Replace complete citation markers like 【4:0†source】 or 【4:0 source】 with [1], [2], etc.
+ chunk_text = re.sub(r'【\d+:\d+†?[^】]*】', replace_citation_marker, chunk_text)
if chunk_text:
complete_response += chunk_text
- if first_chunk:
- first_chunk = False
- logger.info("First chunk received for conversation %s, streaming response", conversation_id)
- yield "{ \"answer\": " + chunk_text
- else:
- yield chunk_text
+ yield ("assistant", chunk_text)
logger.info("Streaming complete for conversation %s: response_length=%d, citation_count=%d",
conversation_id, len(complete_response), len(citations))
@@ -209,6 +206,7 @@ def replace_citation_marker(match):
})
cache[conversation_id] = thread_conversation_id
+ citation_json = "[]"
if citations:
citation_list = []
seen_doc_ids = set() # Track unique document IDs to avoid duplicates
@@ -234,13 +232,11 @@ def replace_citation_marker(match):
if doc_id:
seen_doc_ids.add(doc_id)
- citation_list.append(json.dumps({"url": url, "title": title}))
- yield ", \"citations\": [" + ",".join(citation_list) + "]}"
- else:
- yield ", \"citations\": []}"
+ citation_list.append({"url": url, "title": title})
+ citation_json = json.dumps(citation_list)
except Exception as e:
- complete_response = str(e)
+ had_error = True
logger.exception("Error in stream_openai_text: %s", e)
cache = self.get_thread_cache()
thread_conversation_id = cache.pop(conversation_id, None)
@@ -266,12 +262,16 @@ def replace_citation_marker(match):
if db_conn is not None:
try:
db_conn.close()
- except Exception:
+ except Exception: # Best-effort connection cleanup
pass
- # Provide a fallback response when no data is received from OpenAI.
- if complete_response == "":
- logger.info("No response received from OpenAI.")
- yield "I cannot answer this question with the current data. Please rephrase or add more details."
+
+ # Only emit fallback and tool citations if no error occurred
+ if not had_error:
+ if complete_response == "":
+ logger.info("No response received from OpenAI.")
+ yield ("assistant", "I cannot answer this question with the current data. Please rephrase or add more details.")
+
+ yield ("tool", citation_json)
async def stream_chat_request(self, conversation_id, query, user_id: str = ""):
"""
@@ -281,20 +281,15 @@ async def stream_chat_request(self, conversation_id, query, user_id: str = ""):
async def generate():
try:
- assistant_content = ""
- async for chunk in self.stream_openai_text(conversation_id, query, user_id=user_id):
- if isinstance(chunk, dict):
- chunk = json.dumps(chunk) # Convert dict to JSON string
- assistant_content += str(chunk)
-
- if assistant_content:
- # Optimized response - only send fields used by frontend
+ async for role, content in self.stream_openai_text(conversation_id, query, user_id=user_id):
+ if content:
response = {
"choices": [
{
- "messages": [
- {"role": "assistant", "content": assistant_content}
- ]
+ "delta": {
+ "role": role,
+ "content": content
+ }
}
]
}
diff --git a/src/api/services/history_service.py b/src/api/services/history_service.py
index b0d522567..9b35e6e80 100644
--- a/src/api/services/history_service.py
+++ b/src/api/services/history_service.py
@@ -5,7 +5,7 @@
from azure.ai.projects.aio import AIProjectClient
from common.config.config import Config
from common.database.cosmosdb_service import CosmosConversationClient
-from helpers.azure_credential_utils import get_azure_credential, get_azure_credential_async
+from helpers.azure_credential_utils import get_azure_credential_async, build_async_azure_credential
from agent_framework.azure import AzureAIProjectAgentProvider
@@ -28,7 +28,6 @@ def __init__(self):
and self.azure_cosmosdb_conversations_container
)
- self.azure_openai_deployment_name = config.azure_openai_deployment_model
self.azure_client_id = config.azure_client_id
self.title_agent_name = config.title_agent_name
@@ -47,7 +46,7 @@ def init_cosmosdb_client(self):
return CosmosConversationClient(
cosmosdb_endpoint=cosmos_endpoint,
- credential=get_azure_credential(client_id=self.azure_client_id),
+ credential=build_async_azure_credential(client_id=self.azure_client_id),
database_name=self.azure_cosmosdb_database,
container_name=self.azure_cosmosdb_conversations_container,
enable_message_feedback=self.azure_cosmosdb_enable_feedback,
diff --git a/src/start.cmd b/src/start.cmd
index 74f06ab29..386a736ac 100644
--- a/src/start.cmd
+++ b/src/start.cmd
@@ -107,7 +107,7 @@ if exist "%API_ENV_FILE%" (
echo 2. Manually create %API_ENV_FILE% with required environment variables
echo 3. Copy an existing .env file to %API_ENV_FILE%
echo.
- echo For more information, see: documents/LocalDebuggingSetup.md
+ echo For more information, see: documents/LocalDevelopmentSetup.md
exit /b 1
)
@@ -119,7 +119,7 @@ for /f "tokens=1,* delims==" %%A in ('type "%ENV_FILE_FOR_ROLES%"') do (
if "%%A"=="AZURE_COSMOSDB_ACCOUNT" set AZURE_COSMOSDB_ACCOUNT=%%~B
if "%%A"=="AZURE_AI_FOUNDRY_NAME" set "AI_FOUNDRY_NAME=%%~B"
if "%%A"=="AZURE_AI_SEARCH_NAME" set "SEARCH_SERVICE_NAME=%%~B"
- if "%%A"=="AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" set "EXISTING_AI_PROJECT_RESOURCE_ID=%%~B"
+ if "%%A"=="AZURE_EXISTING_AIPROJECT_RESOURCE_ID" set "EXISTING_AI_PROJECT_RESOURCE_ID=%%~B"
if "%%A"=="SQLDB_SERVER" (
set SQLDB_SERVER=%%~B
for /f "tokens=1 delims=." %%C in ("%%~B") do set SQLDB_SERVER_NAME=%%C
@@ -131,7 +131,7 @@ set APP_ENV_FILE=%ROOT_DIR%\src\App\.env
(
echo REACT_APP_API_BASE_URL=http://127.0.0.1:8000
) > "%APP_ENV_FILE%"
-echo Updated src/App/.env with APP_API_BASE_URL
+echo Updated src/App/.env with REACT_APP_API_BASE_URL
REM Add or update APP_ENV="dev" in API .env file
echo Checking for existing APP_ENV in src/api/.env...
@@ -284,6 +284,18 @@ if errorlevel 1 (
)
cd %ROOT_DIR%
+REM Close any processes using ports 8000 and 3000
+echo Checking for processes using ports 8000 and 3000...
+REM Kill any existing processes on ports 8000 and 3000 before starting
+for %%P in (8000 3000) do (
+ for /f "tokens=5" %%A in ('netstat -ano ^| findstr "LISTENING" ^| findstr ":%%P "') do (
+ if "%%A" neq "0" (
+ echo Port %%P is already in use by PID %%A. Stopping it...
+ taskkill /F /PID %%A /T >nul 2>&1
+ )
+ )
+)
+
REM Start backend and frontend
echo Starting backend server...
cd %ROOT_DIR%
@@ -297,10 +309,27 @@ timeout /t 30 /nobreak >nul
echo Starting frontend server...
cd %ROOT_DIR%\src\App
-call npm start
-echo Both servers have been started.
-echo Backend running at http://127.0.0.1:8000
-echo Frontend running at http://localhost:3000
+REM Show server information before starting
+echo.
+echo ========================================
+echo Both servers are now running:
+echo Backend: http://127.0.0.1:8000
+echo Frontend: http://localhost:3000
+echo ========================================
+echo Press Ctrl+C to stop all servers
+echo.
+
+REM Start npm with PowerShell wrapper for automatic cleanup on Ctrl+C (single line for reliability)
+powershell -NoProfile -ExecutionPolicy Bypass -Command "try { npm start } finally { Write-Host ''; Write-Host 'Stopping all processes...'; Start-Sleep -Milliseconds 500; @(8000, 3000) | ForEach-Object { $port = $_; Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue | ForEach-Object { $pid_ = $_.OwningProcess; if ($pid_ -and $pid_ -ne 0) { taskkill /F /PID $pid_ /T 2>$null } } }; Write-Host 'Cleanup complete.'; Write-Host ''; Write-Host 'All servers stopped.' -ForegroundColor Yellow }"
+
+REM Fallback cleanup in case PowerShell finally block was interrupted
+for %%P in (8000 3000) do (
+ for /f "tokens=5" %%A in ('netstat -ano ^| findstr "LISTENING" ^| findstr ":%%P "') do (
+ if "%%A" neq "0" (
+ taskkill /F /PID %%A /T >nul 2>&1
+ )
+ )
+)
endlocal
\ No newline at end of file
diff --git a/src/start.sh b/src/start.sh
index d720836dc..6b7e19c22 100644
--- a/src/start.sh
+++ b/src/start.sh
@@ -45,7 +45,7 @@ check_local_env() {
echo " 2. Manually create $API_ENV_FILE with required environment variables"
echo " 3. Copy an existing .env file to $API_ENV_FILE"
echo ""
- echo "For more information, see: documents/LocalDebuggingSetup.md"
+ echo "For more information, see: documents/LocalDevelopmentSetup.md"
exit 1
fi
}
@@ -79,7 +79,7 @@ setup_environment() {
AZURE_COSMOSDB_ACCOUNT) AZURE_COSMOSDB_ACCOUNT="$value" ;;
AZURE_AI_FOUNDRY_NAME) AI_FOUNDRY_NAME="$value" ;;
AZURE_AI_SEARCH_NAME) SEARCH_SERVICE_NAME="$value" ;;
- AZURE_EXISTING_AI_PROJECT_RESOURCE_ID) EXISTING_AI_PROJECT_RESOURCE_ID="$value" ;;
+ AZURE_EXISTING_AIPROJECT_RESOURCE_ID) EXISTING_AI_PROJECT_RESOURCE_ID="$value" ;;
SQLDB_SERVER)
SQLDB_SERVER="$value"
SQLDB_SERVER_NAME="${value%%.*}"
@@ -97,7 +97,12 @@ setup_environment() {
echo "Checking for existing APP_ENV in src/api/.env..."
if grep -q "^APP_ENV=" "$API_ENV_FILE" 2>/dev/null; then
echo "APP_ENV already exists, updating to \"dev\"..."
- sed -i 's/^APP_ENV=.*/APP_ENV="dev"/' "$API_ENV_FILE"
+ # macOS/BSD sed requires -i with extension, use portable approach
+ if [[ "$OSTYPE" == "darwin"* ]]; then
+ sed -i '' 's/^APP_ENV=.*/APP_ENV="dev"/' "$API_ENV_FILE"
+ else
+ sed -i 's/^APP_ENV=.*/APP_ENV="dev"/' "$API_ENV_FILE"
+ fi
else
echo "APP_ENV not found, adding APP_ENV=\"dev\"..."
echo 'APP_ENV="dev"' >> "$API_ENV_FILE"
@@ -286,6 +291,15 @@ setup_environment() {
npm install --force || { echo "Failed to restore frontend npm packages"; exit 1; }
cd "$ROOT_DIR"
+ # Kill any existing processes on ports 8000 and 3000 before starting
+ for port in 8000 3000; do
+ pid=$(lsof -ti :"$port" 2>/dev/null)
+ if [ -n "$pid" ]; then
+ echo "Port $port is already in use by PID $pid. Stopping it..."
+ kill -9 $pid
+ fi
+ done
+
# Start backend and frontend
echo "Starting backend server..."
cd "$ROOT_DIR"
@@ -305,11 +319,43 @@ setup_environment() {
echo "Starting frontend server..."
cd "$ROOT_DIR/src/App"
+
+ # Show server information before starting
+ echo ""
+ echo "========================================"
+ echo "Both servers are now running:"
+ echo " Backend: http://127.0.0.1:8000"
+ echo " Frontend: http://localhost:3000"
+ echo "========================================"
+ echo "Press Ctrl+C to stop all servers"
+ echo ""
+
+ # Setup cleanup function to kill both backend and frontend on Ctrl+C
+ cleanup() {
+ echo ""
+ echo "Cleaning up processes..."
+ sleep 0.5
+
+ for port in 8000 3000; do
+ pid=$(lsof -ti :"$port" 2>/dev/null)
+ if [ -n "$pid" ]; then
+ echo "Stopping process on port $port (PID $pid)..."
+ kill -9 $pid 2>/dev/null
+ fi
+ done
+
+ echo "All servers stopped."
+ exit 0
+ }
+
+ # Trap Ctrl+C and other termination signals
+ trap cleanup INT TERM
+
+ # Start npm (this will block until Ctrl+C)
npm start
-
- echo "Both servers have been started."
- echo "Backend running at http://127.0.0.1:8000"
- echo "Frontend running at http://localhost:3000"
+
+ # Cleanup after npm exits normally
+ cleanup
}
# Check if .azure folder exists first
diff --git a/src/tests/api/api/test_api_routes.py b/src/tests/api/api/test_api_routes.py
index 27b0b657f..2ef4dcf51 100644
--- a/src/tests/api/api/test_api_routes.py
+++ b/src/tests/api/api/test_api_routes.py
@@ -1,208 +1,208 @@
-import json
-import pytest
-from unittest.mock import AsyncMock, patch, Mock
-from fastapi import FastAPI
-from fastapi.testclient import TestClient
-
-from api import api_routes
-
-@pytest.fixture
-def create_test_client():
- def _create_client():
- app = FastAPI()
- app.include_router(api_routes.router)
- return TestClient(app)
- return _create_client
-
-
-def test_fetch_chart_data_basic(create_test_client):
- with patch("api.api_routes.ChartService") as MockChartService:
- mock_instance = MockChartService.return_value
- mock_instance.fetch_chart_data = AsyncMock(return_value={"data": "mocked"})
-
- client = create_test_client()
- response = client.get("/fetchChartData")
-
- assert response.status_code == 200
- assert response.json() == {"data": "mocked"}
-
-
-
-def test_fetch_filter_data_basic(create_test_client):
- with patch("api.api_routes.ChartService") as MockChartService:
- mock_instance = MockChartService.return_value
- mock_instance.fetch_filter_data = AsyncMock(return_value={"filters": "mocked"})
-
- client = create_test_client()
- response = client.get("/fetchFilterData")
-
- assert response.status_code == 200
- assert response.json() == {"filters": "mocked"}
-
-
-def test_fetch_chart_data_with_filters_basic(create_test_client):
- with patch("api.api_routes.ChartService") as MockChartService:
- mock_instance = MockChartService.return_value
- mock_instance.fetch_chart_data_with_filters = AsyncMock(return_value=[
- {
- "id": "TOTAL_CALLS",
- "chart_name": "Total Calls",
- "chart_type": "card",
- "chart_value": [
- {"name": "Total Calls", "value": float("nan"), "unit_of_measurement": ""}
- ]
- }
- ])
-
- client = create_test_client()
- payload = {
- "selected_filters": {
- "Topic": ["Tech"],
- "Sentiment": ["Positive"],
- "DateRange": ["Last 30 Days"]
- }
- }
- response = client.post("/fetchChartDataWithFilters", json=payload)
- expected = [
- {
- "id": "TOTAL_CALLS",
- "chart_name": "Total Calls",
- "chart_type": "card",
- "chart_value": [
- {"name": "Total Calls", "value": None, "unit_of_measurement": ""}
- ]
- }
- ]
- assert response.status_code == 200
- assert response.json() == expected
-
-def test_fetch_chart_data_with_filters_error(create_test_client):
- with patch("api.api_routes.ChartService") as MockChartService:
- mock_instance = MockChartService.return_value
- mock_instance.fetch_chart_data_with_filters = AsyncMock(side_effect=Exception("fail"))
-
- client = create_test_client()
- payload = {
- "selected_filters": {
- "Topic": ["Tech"],
- "Sentiment": ["Positive"],
- "DateRange": ["Last 30 Days"]
- }
- }
- response = client.post("/fetchChartDataWithFilters", json=payload)
-
- assert response.status_code == 500
- assert "error" in response.json()
-
-
-def test_fetch_chart_data_error_handling(create_test_client):
- with patch("api.api_routes.ChartService") as MockChartService:
- mock_instance = MockChartService.return_value
- mock_instance.fetch_chart_data = AsyncMock(side_effect=Exception("fail"))
-
- client = create_test_client()
- response = client.get("/fetchChartData")
-
- assert response.status_code == 500
- assert "error" in response.json()
-
-
-def test_chat_endpoint_basic(create_test_client):
- with patch("api.api_routes.ChatService") as MockChatService:
- mock_instance = MockChatService.return_value
- mock_instance.stream_chat_request = AsyncMock(return_value=iter([b'{"message": "mocked stream"}']))
-
- client = create_test_client()
- payload = {
- "conversation_id": "test",
- "messages": [{"content": "Show me a chart"}],
- "last_rag_response": "previous data"
- }
-
- response = client.post("/chat", json=payload)
-
- assert response.status_code == 200
- assert response.json() == {"message": "mocked stream"}
-
-
-def test_get_layout_config_valid(create_test_client, monkeypatch):
- test_config = {"layout": "mocked"}
- monkeypatch.setenv("REACT_APP_LAYOUT_CONFIG", json.dumps(test_config))
-
- client = create_test_client()
- response = client.get("/layout-config")
-
- assert response.status_code == 200
- assert response.json() == test_config
-
-
-def test_get_layout_config_invalid_json(create_test_client, monkeypatch):
- monkeypatch.setenv("REACT_APP_LAYOUT_CONFIG", "{bad json")
-
- client = create_test_client()
- response = client.get("/layout-config")
-
- assert response.status_code == 400
- assert "error" in response.json()
-
-
-def test_get_chart_config_found(create_test_client, monkeypatch):
- monkeypatch.setenv("DISPLAY_CHART_DEFAULT", "true")
-
- client = create_test_client()
- response = client.get("/display-chart-default")
-
- assert response.status_code == 200
- assert response.json() == {"isChartDisplayDefault": "true"}
-
-
-def test_get_chart_config_missing(create_test_client, monkeypatch):
- monkeypatch.delenv("DISPLAY_CHART_DEFAULT", raising=False)
-
- client = create_test_client()
- response = client.get("/display-chart-default")
-
- assert response.status_code == 400
- assert "error" in response.json()
-
-
-def test_fetch_filter_data_error_handling(create_test_client):
- with patch("api.api_routes.ChartService") as MockChartService:
- mock_instance = MockChartService.return_value
- mock_instance.fetch_filter_data = AsyncMock(side_effect=Exception("fail"))
-
- client = create_test_client()
- response = client.get("/fetchFilterData")
-
- assert response.status_code == 500
- assert "error" in response.json()
-
-
-def test_layout_config_json_decode_error(create_test_client, monkeypatch):
- monkeypatch.setenv("REACT_APP_LAYOUT_CONFIG", "not-a-json")
-
- client = create_test_client()
- response = client.get("/layout-config")
-
- assert response.status_code == 400
- assert "error" in response.json()
-
-
-def test_get_chart_config_success(create_test_client, monkeypatch):
- monkeypatch.setenv("DISPLAY_CHART_DEFAULT", "false")
-
- client = create_test_client()
- response = client.get("/display-chart-default")
-
- assert response.status_code == 200
- assert response.json() == {"isChartDisplayDefault": "false"}
-
-
-def test_get_chart_config_env_missing(create_test_client, monkeypatch):
- monkeypatch.delenv("DISPLAY_CHART_DEFAULT", raising=False)
-
- client = create_test_client()
- response = client.get("/display-chart-default")
-
- assert response.status_code == 400
+import json
+import pytest
+from unittest.mock import AsyncMock, patch
+from fastapi import FastAPI
+from fastapi.testclient import TestClient
+
+from api import api_routes
+
+@pytest.fixture
+def create_test_client():
+ def _create_client():
+ app = FastAPI()
+ app.include_router(api_routes.router)
+ return TestClient(app)
+ return _create_client
+
+
+def test_fetch_chart_data_basic(create_test_client):
+ with patch("api.api_routes.ChartService") as MockChartService:
+ mock_instance = MockChartService.return_value
+ mock_instance.fetch_chart_data = AsyncMock(return_value={"data": "mocked"})
+
+ client = create_test_client()
+ response = client.get("/fetchChartData")
+
+ assert response.status_code == 200
+ assert response.json() == {"data": "mocked"}
+
+
+
+def test_fetch_filter_data_basic(create_test_client):
+ with patch("api.api_routes.ChartService") as MockChartService:
+ mock_instance = MockChartService.return_value
+ mock_instance.fetch_filter_data = AsyncMock(return_value={"filters": "mocked"})
+
+ client = create_test_client()
+ response = client.get("/fetchFilterData")
+
+ assert response.status_code == 200
+ assert response.json() == {"filters": "mocked"}
+
+
+def test_fetch_chart_data_with_filters_basic(create_test_client):
+ with patch("api.api_routes.ChartService") as MockChartService:
+ mock_instance = MockChartService.return_value
+ mock_instance.fetch_chart_data_with_filters = AsyncMock(return_value=[
+ {
+ "id": "TOTAL_CALLS",
+ "chart_name": "Total Calls",
+ "chart_type": "card",
+ "chart_value": [
+ {"name": "Total Calls", "value": float("nan"), "unit_of_measurement": ""}
+ ]
+ }
+ ])
+
+ client = create_test_client()
+ payload = {
+ "selected_filters": {
+ "Topic": ["Tech"],
+ "Sentiment": ["Positive"],
+ "DateRange": ["Last 30 Days"]
+ }
+ }
+ response = client.post("/fetchChartDataWithFilters", json=payload)
+ expected = [
+ {
+ "id": "TOTAL_CALLS",
+ "chart_name": "Total Calls",
+ "chart_type": "card",
+ "chart_value": [
+ {"name": "Total Calls", "value": None, "unit_of_measurement": ""}
+ ]
+ }
+ ]
+ assert response.status_code == 200
+ assert response.json() == expected
+
+def test_fetch_chart_data_with_filters_error(create_test_client):
+ with patch("api.api_routes.ChartService") as MockChartService:
+ mock_instance = MockChartService.return_value
+ mock_instance.fetch_chart_data_with_filters = AsyncMock(side_effect=Exception("fail"))
+
+ client = create_test_client()
+ payload = {
+ "selected_filters": {
+ "Topic": ["Tech"],
+ "Sentiment": ["Positive"],
+ "DateRange": ["Last 30 Days"]
+ }
+ }
+ response = client.post("/fetchChartDataWithFilters", json=payload)
+
+ assert response.status_code == 500
+ assert "error" in response.json()
+
+
+def test_fetch_chart_data_error_handling(create_test_client):
+ with patch("api.api_routes.ChartService") as MockChartService:
+ mock_instance = MockChartService.return_value
+ mock_instance.fetch_chart_data = AsyncMock(side_effect=Exception("fail"))
+
+ client = create_test_client()
+ response = client.get("/fetchChartData")
+
+ assert response.status_code == 500
+ assert "error" in response.json()
+
+
+def test_chat_endpoint_basic(create_test_client):
+ with patch("api.api_routes.ChatService") as MockChatService:
+ mock_instance = MockChatService.return_value
+ mock_instance.stream_chat_request = AsyncMock(return_value=iter([b'{"message": "mocked stream"}']))
+
+ client = create_test_client()
+ payload = {
+ "conversation_id": "test",
+ "messages": [{"content": "Show me a chart"}],
+ "last_rag_response": "previous data"
+ }
+
+ response = client.post("/chat", json=payload)
+
+ assert response.status_code == 200
+ assert response.json() == {"message": "mocked stream"}
+
+
+def test_get_layout_config_valid(create_test_client, monkeypatch):
+ test_config = {"layout": "mocked"}
+ monkeypatch.setenv("REACT_APP_LAYOUT_CONFIG", json.dumps(test_config))
+
+ client = create_test_client()
+ response = client.get("/layout-config")
+
+ assert response.status_code == 200
+ assert response.json() == test_config
+
+
+def test_get_layout_config_invalid_json(create_test_client, monkeypatch):
+ monkeypatch.setenv("REACT_APP_LAYOUT_CONFIG", "{bad json")
+
+ client = create_test_client()
+ response = client.get("/layout-config")
+
+ assert response.status_code == 400
+ assert "error" in response.json()
+
+
+def test_get_chart_config_found(create_test_client, monkeypatch):
+ monkeypatch.setenv("DISPLAY_CHART_DEFAULT", "true")
+
+ client = create_test_client()
+ response = client.get("/display-chart-default")
+
+ assert response.status_code == 200
+ assert response.json() == {"isChartDisplayDefault": "true"}
+
+
+def test_get_chart_config_missing(create_test_client, monkeypatch):
+ monkeypatch.delenv("DISPLAY_CHART_DEFAULT", raising=False)
+
+ client = create_test_client()
+ response = client.get("/display-chart-default")
+
+ assert response.status_code == 400
+ assert "error" in response.json()
+
+
+def test_fetch_filter_data_error_handling(create_test_client):
+ with patch("api.api_routes.ChartService") as MockChartService:
+ mock_instance = MockChartService.return_value
+ mock_instance.fetch_filter_data = AsyncMock(side_effect=Exception("fail"))
+
+ client = create_test_client()
+ response = client.get("/fetchFilterData")
+
+ assert response.status_code == 500
+ assert "error" in response.json()
+
+
+def test_layout_config_json_decode_error(create_test_client, monkeypatch):
+ monkeypatch.setenv("REACT_APP_LAYOUT_CONFIG", "not-a-json")
+
+ client = create_test_client()
+ response = client.get("/layout-config")
+
+ assert response.status_code == 400
+ assert "error" in response.json()
+
+
+def test_get_chart_config_success(create_test_client, monkeypatch):
+ monkeypatch.setenv("DISPLAY_CHART_DEFAULT", "false")
+
+ client = create_test_client()
+ response = client.get("/display-chart-default")
+
+ assert response.status_code == 200
+ assert response.json() == {"isChartDisplayDefault": "false"}
+
+
+def test_get_chart_config_env_missing(create_test_client, monkeypatch):
+ monkeypatch.delenv("DISPLAY_CHART_DEFAULT", raising=False)
+
+ client = create_test_client()
+ response = client.get("/display-chart-default")
+
+ assert response.status_code == 400
assert "error" in response.json()
\ No newline at end of file
diff --git a/src/tests/api/auth/test_auth_utils.py b/src/tests/api/auth/test_auth_utils.py
index deda9c27f..05e660589 100644
--- a/src/tests/api/auth/test_auth_utils.py
+++ b/src/tests/api/auth/test_auth_utils.py
@@ -1,67 +1,67 @@
-import unittest
-from unittest.mock import patch, MagicMock
-import base64
-import json
-
-from auth import auth_utils,sample_user
-
-
-class TestAuthUtils(unittest.TestCase):
-
- @patch("auth.sample_user")
- def test_get_authenticated_user_details_dev_mode(self, mock_sample_user):
- mock_sample_user.sample_user = {
- "x-ms-client-principal-id": "123",
- "x-ms-client-principal-name": "testuser",
- "x-ms-client-principal-idp": "aad",
- "x-ms-token-aad-id-token": "token123",
- "x-ms-client-principal": "encodedstring"
- }
-
- request_headers = {}
- result = auth_utils.get_authenticated_user_details(request_headers)
-
- self.assertEqual(result["user_principal_id"], "123")
- self.assertEqual(result["user_name"], "testuser")
- self.assertEqual(result["auth_provider"], "aad")
- self.assertEqual(result["auth_token"], "token123")
- self.assertEqual(result["client_principal_b64"], "encodedstring")
- self.assertEqual(result["aad_id_token"], "token123")
-
- def test_get_authenticated_user_details_prod_mode(self):
- request_headers = {
- "x-ms-client-principal-id": "123",
- "x-ms-client-principal-name": "testuser",
- "x-ms-client-principal-idp": "aad",
- "x-ms-token-aad-id-token": "token123",
- "x-ms-client-principal": "encodedstring"
- }
-
- result = auth_utils.get_authenticated_user_details(request_headers)
-
- self.assertEqual(result["user_principal_id"], "123")
- self.assertEqual(result["user_name"], "testuser")
- self.assertEqual(result["auth_provider"], "aad")
- self.assertEqual(result["auth_token"], "token123")
- self.assertEqual(result["client_principal_b64"], "encodedstring")
- self.assertEqual(result["aad_id_token"], "token123")
-
- def test_get_tenantid_valid_b64(self):
- payload = {"tid": "tenant123"}
- b64_encoded = base64.b64encode(json.dumps(payload).encode()).decode()
-
- result = auth_utils.get_tenantid(b64_encoded)
- self.assertEqual(result, "tenant123")
-
- def test_get_tenantid_invalid_b64(self):
- with self.assertLogs(level='ERROR'):
- result = auth_utils.get_tenantid("notbase64!!!")
- self.assertEqual(result, "")
-
- def test_get_tenantid_none(self):
- result = auth_utils.get_tenantid(None)
- self.assertEqual(result, "")
-
-
-if __name__ == '__main__':
- unittest.main()
+import unittest
+from unittest.mock import patch
+import base64
+import json
+
+from auth import auth_utils,sample_user
+
+
+class TestAuthUtils(unittest.TestCase):
+
+ @patch("auth.sample_user")
+ def test_get_authenticated_user_details_dev_mode(self, mock_sample_user):
+ mock_sample_user.sample_user = {
+ "x-ms-client-principal-id": "123",
+ "x-ms-client-principal-name": "testuser",
+ "x-ms-client-principal-idp": "aad",
+ "x-ms-token-aad-id-token": "token123",
+ "x-ms-client-principal": "encodedstring"
+ }
+
+ request_headers = {}
+ result = auth_utils.get_authenticated_user_details(request_headers)
+
+ self.assertEqual(result["user_principal_id"], "123")
+ self.assertEqual(result["user_name"], "testuser")
+ self.assertEqual(result["auth_provider"], "aad")
+ self.assertEqual(result["auth_token"], "token123")
+ self.assertEqual(result["client_principal_b64"], "encodedstring")
+ self.assertEqual(result["aad_id_token"], "token123")
+
+ def test_get_authenticated_user_details_prod_mode(self):
+ request_headers = {
+ "x-ms-client-principal-id": "123",
+ "x-ms-client-principal-name": "testuser",
+ "x-ms-client-principal-idp": "aad",
+ "x-ms-token-aad-id-token": "token123",
+ "x-ms-client-principal": "encodedstring"
+ }
+
+ result = auth_utils.get_authenticated_user_details(request_headers)
+
+ self.assertEqual(result["user_principal_id"], "123")
+ self.assertEqual(result["user_name"], "testuser")
+ self.assertEqual(result["auth_provider"], "aad")
+ self.assertEqual(result["auth_token"], "token123")
+ self.assertEqual(result["client_principal_b64"], "encodedstring")
+ self.assertEqual(result["aad_id_token"], "token123")
+
+ def test_get_tenantid_valid_b64(self):
+ payload = {"tid": "tenant123"}
+ b64_encoded = base64.b64encode(json.dumps(payload).encode()).decode()
+
+ result = auth_utils.get_tenantid(b64_encoded)
+ self.assertEqual(result, "tenant123")
+
+ def test_get_tenantid_invalid_b64(self):
+ with self.assertLogs(level='ERROR'):
+ result = auth_utils.get_tenantid("notbase64!!!")
+ self.assertEqual(result, "")
+
+ def test_get_tenantid_none(self):
+ result = auth_utils.get_tenantid(None)
+ self.assertEqual(result, "")
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/src/tests/api/common/config/test_config.py b/src/tests/api/common/config/test_config.py
index f09fbed42..292037cac 100644
--- a/src/tests/api/common/config/test_config.py
+++ b/src/tests/api/common/config/test_config.py
@@ -11,10 +11,6 @@ def mock_env_vars():
"SQLDB_SERVER": "test_server",
"SQLDB_USERNAME": "test_user",
"SQLDB_USER_MID": "test_mid",
- "AZURE_OPENAI_ENDPOINT": "https://openai.test",
- "AZURE_OPENAI_DEPLOYMENT_MODEL": "gpt-4",
- "AZURE_OPENAI_API_VERSION": "2023-03-15-preview",
- "AZURE_OPENAI_RESOURCE": "test_resource",
"AZURE_AI_SEARCH_ENDPOINT": "https://search.test",
"AZURE_AI_SEARCH_API_KEY": "search_key",
"AZURE_AI_SEARCH_INDEX": "test_index",
@@ -39,12 +35,6 @@ def test_config_initialization(mock_env_vars):
assert config.driver == "{ODBC Driver 18 for SQL Server}"
assert config.mid_id == "test_mid"
- # Azure OpenAI config
- assert config.azure_openai_endpoint == "https://openai.test"
- assert config.azure_openai_deployment_model == "gpt-4"
- assert config.azure_openai_api_version == "2023-03-15-preview"
- assert config.azure_openai_resource == "test_resource"
-
# Azure AI Search config
assert config.azure_ai_search_endpoint == "https://search.test"
assert config.azure_ai_search_api_key == "search_key"
diff --git a/src/tests/api/common/database/test_cosmosdb_service.py b/src/tests/api/common/database/test_cosmosdb_service.py
index 1dce7b7df..41145de5f 100644
--- a/src/tests/api/common/database/test_cosmosdb_service.py
+++ b/src/tests/api/common/database/test_cosmosdb_service.py
@@ -1,252 +1,252 @@
-from unittest.mock import AsyncMock, MagicMock, patch
-import pytest
-from azure.cosmos import exceptions
-from common.database.cosmosdb_service import CosmosConversationClient
-
-
-class AsyncIteratorWrapper:
- """Utility class to wrap async iteration over items."""
- def __init__(self, items):
- self._items = items
-
- def __aiter__(self):
- return self._async_gen()
-
- async def _async_gen(self):
- for item in self._items:
- yield item
-
-
-@pytest.fixture
-def mock_cosmos_clients():
- """Fixture to mock Cosmos DB container, database, and client."""
- mock_container = MagicMock()
- mock_database = MagicMock()
- mock_database.get_container_client.return_value = mock_container
- mock_cosmos = MagicMock()
- mock_cosmos.get_database_client.return_value = mock_database
- return mock_cosmos, mock_database, mock_container
-
-
-@pytest.fixture
-def cosmos_client(mock_cosmos_clients):
- """Fixture to create a CosmosConversationClient instance with mocked CosmosClient."""
- cosmos_mock, _, _ = mock_cosmos_clients
- with patch("common.database.cosmosdb_service.CosmosClient", return_value=cosmos_mock):
- return CosmosConversationClient(
- cosmosdb_endpoint="https://fake-cosmos.documents.azure.com",
- credential="fake-key",
- database_name="test-db",
- container_name="test-container"
- )
-
-
-class TestCosmosDbService:
-
- @pytest.mark.asyncio
- async def test_ensure_success(self, cosmos_client, mock_cosmos_clients):
- """Test ensure() returns success if both DB and container are accessible."""
- _, db, container = mock_cosmos_clients
- db.read = AsyncMock(return_value=True)
- container.read = AsyncMock(return_value=True)
- result, msg = await cosmos_client.ensure()
- assert result is True and "successfully" in msg
-
- @pytest.mark.asyncio
- async def test_ensure_fail_when_client_is_none(self):
- """Test ensure() fails if cosmos client is not initialized."""
- client = CosmosConversationClient("url", "key", "db", "container")
- client.cosmosdb_client = None
- result, msg = await client.ensure()
- assert result is False and "not initialized" in msg
-
- @pytest.mark.asyncio
- async def test_ensure_database_read_fails(self, cosmos_client, mock_cosmos_clients):
- """Test ensure() fails when reading DB fails."""
- _, db, _ = mock_cosmos_clients
- db.read = AsyncMock(side_effect=Exception("Fail"))
- result, msg = await cosmos_client.ensure()
- assert result is False and "not found" in msg
-
- @pytest.mark.asyncio
- async def test_ensure_container_read_fails(self, cosmos_client, mock_cosmos_clients):
- """Test ensure() fails when reading container fails."""
- _, db, container = mock_cosmos_clients
- db.read = AsyncMock(return_value=True)
- container.read = AsyncMock(side_effect=Exception("Fail"))
- result, msg = await cosmos_client.ensure()
- assert result is False and "container" in msg
-
- def test_constructor_invalid_credential(self):
- """Test constructor raises ValueError on bad credentials."""
- with patch("common.database.cosmosdb_service.CosmosClient", side_effect=exceptions.CosmosHttpResponseError(status_code=401)):
- with pytest.raises(ValueError, match="Invalid credentials"):
- CosmosConversationClient("url", "bad", "db", "container")
-
- def test_constructor_invalid_database(self):
- """Test constructor raises ValueError for invalid DB name."""
- cosmos_mock = MagicMock()
- cosmos_mock.get_database_client.side_effect = exceptions.CosmosResourceNotFoundError()
- with patch("common.database.cosmosdb_service.CosmosClient", return_value=cosmos_mock):
- with pytest.raises(ValueError, match="Invalid CosmosDB database name"):
- CosmosConversationClient("url", "key", "invalid", "container")
-
- def test_constructor_invalid_container(self):
- """Test constructor raises ValueError for invalid container."""
- cosmos_mock = MagicMock()
- db_mock = MagicMock()
- db_mock.get_container_client.side_effect = exceptions.CosmosResourceNotFoundError()
- cosmos_mock.get_database_client.return_value = db_mock
- with patch("common.database.cosmosdb_service.CosmosClient", return_value=cosmos_mock):
- with pytest.raises(ValueError, match="Invalid CosmosDB container name"):
- CosmosConversationClient("url", "key", "db", "bad")
-
- @pytest.mark.asyncio
- async def test_create_conversation_success(self, cosmos_client, mock_cosmos_clients):
- """Test successful creation of conversation."""
- _, _, container = mock_cosmos_clients
- container.upsert_item = AsyncMock(return_value={"id": "c1"})
- result = await cosmos_client.create_conversation("user1", "c1", "title")
- assert result["id"] == "c1"
-
- @pytest.mark.asyncio
- async def test_create_conversation_failure(self, cosmos_client, mock_cosmos_clients):
- """Test failure to create conversation returns False."""
- _, _, container = mock_cosmos_clients
- container.upsert_item = AsyncMock(return_value=None)
- result = await cosmos_client.create_conversation("user1", "c2", "title")
- assert result is False
-
- @pytest.mark.asyncio
- async def test_upsert_conversation_success(self, cosmos_client, mock_cosmos_clients):
- """Test successful upsert of conversation."""
- _, _, container = mock_cosmos_clients
- container.upsert_item = AsyncMock(return_value={"id": "x"})
- result = await cosmos_client.upsert_conversation({"id": "x"})
- assert result["id"] == "x"
-
- @pytest.mark.asyncio
- async def test_upsert_conversation_failure(self, cosmos_client, mock_cosmos_clients):
- """Test upsert returns False when result is None."""
- _, _, container = mock_cosmos_clients
- container.upsert_item = AsyncMock(return_value=None)
- result = await cosmos_client.upsert_conversation({"id": "x"})
- assert result is False
-
- @pytest.mark.asyncio
- async def test_get_conversation_found(self, cosmos_client, mock_cosmos_clients):
- """Test get_conversation returns a result if found."""
- _, _, container = mock_cosmos_clients
- container.query_items.return_value = AsyncIteratorWrapper([{"id": "c1"}])
- result = await cosmos_client.get_conversation("user1", "c1")
- assert result["id"] == "c1"
-
- @pytest.mark.asyncio
- async def test_get_conversation_not_found(self, cosmos_client, mock_cosmos_clients):
- """Test get_conversation returns None when not found."""
- _, _, container = mock_cosmos_clients
- container.query_items.return_value = AsyncIteratorWrapper([])
- result = await cosmos_client.get_conversation("user1", "none")
- assert result is None
-
- @pytest.mark.asyncio
- async def test_get_conversations_with_limit(self, cosmos_client, mock_cosmos_clients):
- """Test get_conversations returns a list of messages."""
- _, _, container = mock_cosmos_clients
- container.query_items.return_value = AsyncIteratorWrapper([{"id": "1"}, {"id": "2"}])
- result = await cosmos_client.get_conversations("user1", limit=2, offset=0)
- assert len(result) == 2
-
- @pytest.mark.asyncio
- async def test_create_message_with_feedback(self, cosmos_client, mock_cosmos_clients):
- """Test message creation with feedback enabled."""
- _, _, container = mock_cosmos_clients
- cosmos_client.enable_message_feedback = True
- container.upsert_item = AsyncMock(return_value={"id": "m1"})
- cosmos_client.get_conversation = AsyncMock(return_value={"id": "c1", "updatedAt": "old"})
- cosmos_client.upsert_conversation = AsyncMock()
- result = await cosmos_client.create_message("m1", "c1", "user1", {"role": "user", "text": "hi"})
- assert result["id"] == "m1"
-
- @pytest.mark.asyncio
- async def test_create_message_without_feedback(self, cosmos_client, mock_cosmos_clients):
- """Test message creation with feedback disabled."""
- _, _, container = mock_cosmos_clients
- cosmos_client.enable_message_feedback = False
- container.upsert_item = AsyncMock(return_value={"id": "m2"})
- cosmos_client.get_conversation = AsyncMock(return_value={"id": "c2", "updatedAt": "old"})
- cosmos_client.upsert_conversation = AsyncMock()
- result = await cosmos_client.create_message("m2", "c2", "user1", {"role": "assistant", "text": "hello"})
- assert result["id"] == "m2"
-
- @pytest.mark.asyncio
- async def test_create_message_conversation_not_found(self, cosmos_client, mock_cosmos_clients):
- """Test message creation fails when conversation not found."""
- _, _, container = mock_cosmos_clients
- cosmos_client.enable_message_feedback = True
- container.upsert_item = AsyncMock(return_value={"id": "m3"})
- cosmos_client.get_conversation = AsyncMock(return_value=None)
- result = await cosmos_client.create_message("m3", "notfound", "user1", {"role": "user", "text": "nope"})
- assert result == "Conversation not found"
-
- @pytest.mark.asyncio
- async def test_update_message_feedback_success(self, cosmos_client, mock_cosmos_clients):
- """Test updating message feedback successfully."""
- _, _, container = mock_cosmos_clients
- container.read_item = AsyncMock(return_value={"id": "m1"})
- container.upsert_item = AsyncMock(return_value={"id": "m1", "feedback": "Good"})
- result = await cosmos_client.update_message_feedback("user1", "m1", "Good")
- assert result["feedback"] == "Good"
-
- @pytest.mark.asyncio
- async def test_update_message_feedback_not_found(self, cosmos_client, mock_cosmos_clients):
- """Test updating feedback fails when message is missing."""
- _, _, container = mock_cosmos_clients
- container.read_item = AsyncMock(return_value=None)
- result = await cosmos_client.update_message_feedback("user1", "m2", "Bad")
- assert result is False
-
- @pytest.mark.asyncio
- async def test_get_messages(self, cosmos_client, mock_cosmos_clients):
- """Test getting messages for a conversation."""
- _, _, container = mock_cosmos_clients
- container.query_items.return_value = AsyncIteratorWrapper([
- {"id": "m1"}, {"id": "m2"}
- ])
- result = await cosmos_client.get_messages("user1", "c1")
- assert len(result) == 2
-
- @pytest.mark.asyncio
- async def test_delete_messages_with_messages(self, cosmos_client, mock_cosmos_clients):
- """Test deleting messages when messages exist."""
- _, _, container = mock_cosmos_clients
- cosmos_client.get_messages = AsyncMock(return_value=[
- {"id": "m1"}, {"id": "m2"}
- ])
- container.delete_item = AsyncMock(return_value=True)
- result = await cosmos_client.delete_messages("c1", "user1")
- assert len(result) == 2
-
- @pytest.mark.asyncio
- async def test_delete_messages_no_messages(self, cosmos_client):
- """Test delete_messages returns None when there are no messages."""
- cosmos_client.get_messages = AsyncMock(return_value=[])
- result = await cosmos_client.delete_messages("c1", "user1")
- assert result is None
-
- @pytest.mark.asyncio
- async def test_delete_conversation_found(self, cosmos_client, mock_cosmos_clients):
- """Test deleting an existing conversation."""
- _, _, container = mock_cosmos_clients
- container.read_item = AsyncMock(return_value={"id": "c1"})
- container.delete_item = AsyncMock(return_value=True)
- result = await cosmos_client.delete_conversation("user1", "c1")
- assert result is True
-
- @pytest.mark.asyncio
- async def test_delete_conversation_not_found(self, cosmos_client, mock_cosmos_clients):
- """Test deleting a non-existent conversation returns True (no-op)."""
- _, _, container = mock_cosmos_clients
- container.read_item = AsyncMock(return_value=None)
- result = await cosmos_client.delete_conversation("user1", "none")
- assert result is True
+from unittest.mock import AsyncMock, MagicMock, patch
+import pytest
+from azure.cosmos import exceptions
+from common.database.cosmosdb_service import CosmosConversationClient
+
+
+class AsyncIteratorWrapper:
+ """Utility class to wrap async iteration over items."""
+ def __init__(self, items):
+ self._items = items
+
+ def __aiter__(self):
+ return self._async_gen()
+
+ async def _async_gen(self):
+ for item in self._items:
+ yield item
+
+
+@pytest.fixture
+def mock_cosmos_clients():
+ """Fixture to mock Cosmos DB container, database, and client."""
+ mock_container = MagicMock()
+ mock_database = MagicMock()
+ mock_database.get_container_client.return_value = mock_container
+ mock_cosmos = MagicMock()
+ mock_cosmos.get_database_client.return_value = mock_database
+ return mock_cosmos, mock_database, mock_container
+
+
+@pytest.fixture
+def cosmos_client(mock_cosmos_clients):
+ """Fixture to create a CosmosConversationClient instance with mocked CosmosClient."""
+ cosmos_mock, _, _ = mock_cosmos_clients
+ with patch("common.database.cosmosdb_service.CosmosClient", return_value=cosmos_mock):
+ return CosmosConversationClient(
+ cosmosdb_endpoint="https://fake-cosmos.documents.azure.com",
+ credential="fake-key",
+ database_name="test-db",
+ container_name="test-container"
+ )
+
+
+class TestCosmosDbService:
+
+ @pytest.mark.asyncio
+ async def test_ensure_success(self, cosmos_client, mock_cosmos_clients):
+ """Test ensure() returns success if both DB and container are accessible."""
+ _, db, container = mock_cosmos_clients
+ db.read = AsyncMock(return_value=True)
+ container.read = AsyncMock(return_value=True)
+ result, msg = await cosmos_client.ensure()
+ assert result is True and "successfully" in msg
+
+ @pytest.mark.asyncio
+ async def test_ensure_fail_when_client_is_none(self):
+ """Test ensure() fails if cosmos client is not initialized."""
+ client = CosmosConversationClient("url", "key", "db", "container")
+ client.cosmosdb_client = None
+ result, msg = await client.ensure()
+ assert result is False and "not initialized" in msg
+
+ @pytest.mark.asyncio
+ async def test_ensure_database_read_fails(self, cosmos_client, mock_cosmos_clients):
+ """Test ensure() fails when reading DB fails."""
+ _, db, _ = mock_cosmos_clients
+ db.read = AsyncMock(side_effect=Exception("Fail"))
+ result, msg = await cosmos_client.ensure()
+ assert result is False and "not found" in msg
+
+ @pytest.mark.asyncio
+ async def test_ensure_container_read_fails(self, cosmos_client, mock_cosmos_clients):
+ """Test ensure() fails when reading container fails."""
+ _, db, container = mock_cosmos_clients
+ db.read = AsyncMock(return_value=True)
+ container.read = AsyncMock(side_effect=Exception("Fail"))
+ result, msg = await cosmos_client.ensure()
+ assert result is False and "container" in msg
+
+ def test_constructor_invalid_credential(self):
+ """Test constructor raises ValueError on bad credentials."""
+ with patch("common.database.cosmosdb_service.CosmosClient", side_effect=exceptions.CosmosHttpResponseError(status_code=401)):
+ with pytest.raises(ValueError, match="Invalid credentials"):
+ CosmosConversationClient("url", "bad", "db", "container")
+
+ def test_constructor_invalid_database(self):
+ """Test constructor raises ValueError for invalid DB name."""
+ cosmos_mock = MagicMock()
+ cosmos_mock.get_database_client.side_effect = exceptions.CosmosResourceNotFoundError()
+ with patch("common.database.cosmosdb_service.CosmosClient", return_value=cosmos_mock):
+ with pytest.raises(ValueError, match="Invalid CosmosDB database name"):
+ CosmosConversationClient("url", "key", "invalid", "container")
+
+ def test_constructor_invalid_container(self):
+ """Test constructor raises ValueError for invalid container."""
+ cosmos_mock = MagicMock()
+ db_mock = MagicMock()
+ db_mock.get_container_client.side_effect = exceptions.CosmosResourceNotFoundError()
+ cosmos_mock.get_database_client.return_value = db_mock
+ with patch("common.database.cosmosdb_service.CosmosClient", return_value=cosmos_mock):
+ with pytest.raises(ValueError, match="Invalid CosmosDB container name"):
+ CosmosConversationClient("url", "key", "db", "bad")
+
+ @pytest.mark.asyncio
+ async def test_create_conversation_success(self, cosmos_client, mock_cosmos_clients):
+ """Test successful creation of conversation."""
+ _, _, container = mock_cosmos_clients
+ container.upsert_item = AsyncMock(return_value={"id": "c1"})
+ result = await cosmos_client.create_conversation("user1", "c1", "title")
+ assert result["id"] == "c1"
+
+ @pytest.mark.asyncio
+ async def test_create_conversation_failure(self, cosmos_client, mock_cosmos_clients):
+ """Test failure to create conversation returns False."""
+ _, _, container = mock_cosmos_clients
+ container.upsert_item = AsyncMock(return_value=None)
+ result = await cosmos_client.create_conversation("user1", "c2", "title")
+ assert result is False
+
+ @pytest.mark.asyncio
+ async def test_upsert_conversation_success(self, cosmos_client, mock_cosmos_clients):
+ """Test successful upsert of conversation."""
+ _, _, container = mock_cosmos_clients
+ container.upsert_item = AsyncMock(return_value={"id": "x"})
+ result = await cosmos_client.upsert_conversation({"id": "x"})
+ assert result["id"] == "x"
+
+ @pytest.mark.asyncio
+ async def test_upsert_conversation_failure(self, cosmos_client, mock_cosmos_clients):
+ """Test upsert returns False when result is None."""
+ _, _, container = mock_cosmos_clients
+ container.upsert_item = AsyncMock(return_value=None)
+ result = await cosmos_client.upsert_conversation({"id": "x"})
+ assert result is False
+
+ @pytest.mark.asyncio
+ async def test_get_conversation_found(self, cosmos_client, mock_cosmos_clients):
+ """Test get_conversation returns a result if found."""
+ _, _, container = mock_cosmos_clients
+ container.query_items.return_value = AsyncIteratorWrapper([{"id": "c1"}])
+ result = await cosmos_client.get_conversation("user1", "c1")
+ assert result["id"] == "c1"
+
+ @pytest.mark.asyncio
+ async def test_get_conversation_not_found(self, cosmos_client, mock_cosmos_clients):
+ """Test get_conversation returns None when not found."""
+ _, _, container = mock_cosmos_clients
+ container.query_items.return_value = AsyncIteratorWrapper([])
+ result = await cosmos_client.get_conversation("user1", "none")
+ assert result is None
+
+ @pytest.mark.asyncio
+ async def test_get_conversations_with_limit(self, cosmos_client, mock_cosmos_clients):
+ """Test get_conversations returns a list of messages."""
+ _, _, container = mock_cosmos_clients
+ container.query_items.return_value = AsyncIteratorWrapper([{"id": "1"}, {"id": "2"}])
+ result = await cosmos_client.get_conversations("user1", limit=2, offset=0)
+ assert len(result) == 2
+
+ @pytest.mark.asyncio
+ async def test_create_message_with_feedback(self, cosmos_client, mock_cosmos_clients):
+ """Test message creation with feedback enabled."""
+ _, _, container = mock_cosmos_clients
+ cosmos_client.enable_message_feedback = True
+ container.upsert_item = AsyncMock(return_value={"id": "m1"})
+ cosmos_client.get_conversation = AsyncMock(return_value={"id": "c1", "updatedAt": "old"})
+ cosmos_client.upsert_conversation = AsyncMock()
+ result = await cosmos_client.create_message("m1", "c1", "user1", {"role": "user", "text": "hi"})
+ assert result["id"] == "m1"
+
+ @pytest.mark.asyncio
+ async def test_create_message_without_feedback(self, cosmos_client, mock_cosmos_clients):
+ """Test message creation with feedback disabled."""
+ _, _, container = mock_cosmos_clients
+ cosmos_client.enable_message_feedback = False
+ container.upsert_item = AsyncMock(return_value={"id": "m2"})
+ cosmos_client.get_conversation = AsyncMock(return_value={"id": "c2", "updatedAt": "old"})
+ cosmos_client.upsert_conversation = AsyncMock()
+ result = await cosmos_client.create_message("m2", "c2", "user1", {"role": "assistant", "text": "hello"})
+ assert result["id"] == "m2"
+
+ @pytest.mark.asyncio
+ async def test_create_message_conversation_not_found(self, cosmos_client, mock_cosmos_clients):
+ """Test message creation fails when conversation not found."""
+ _, _, container = mock_cosmos_clients
+ cosmos_client.enable_message_feedback = True
+ container.upsert_item = AsyncMock(return_value={"id": "m3"})
+ cosmos_client.get_conversation = AsyncMock(return_value=None)
+ result = await cosmos_client.create_message("m3", "notfound", "user1", {"role": "user", "text": "nope"})
+ assert result == "Conversation not found"
+
+ @pytest.mark.asyncio
+ async def test_update_message_feedback_success(self, cosmos_client, mock_cosmos_clients):
+ """Test updating message feedback successfully."""
+ _, _, container = mock_cosmos_clients
+ container.read_item = AsyncMock(return_value={"id": "m1"})
+ container.upsert_item = AsyncMock(return_value={"id": "m1", "feedback": "Good"})
+ result = await cosmos_client.update_message_feedback("user1", "m1", "Good")
+ assert result["feedback"] == "Good"
+
+ @pytest.mark.asyncio
+ async def test_update_message_feedback_not_found(self, cosmos_client, mock_cosmos_clients):
+ """Test updating feedback fails when message is missing."""
+ _, _, container = mock_cosmos_clients
+ container.read_item = AsyncMock(return_value=None)
+ result = await cosmos_client.update_message_feedback("user1", "m2", "Bad")
+ assert result is False
+
+ @pytest.mark.asyncio
+ async def test_get_messages(self, cosmos_client, mock_cosmos_clients):
+ """Test getting messages for a conversation."""
+ _, _, container = mock_cosmos_clients
+ container.query_items.return_value = AsyncIteratorWrapper([
+ {"id": "m1"}, {"id": "m2"}
+ ])
+ result = await cosmos_client.get_messages("user1", "c1")
+ assert len(result) == 2
+
+ @pytest.mark.asyncio
+ async def test_delete_messages_with_messages(self, cosmos_client, mock_cosmos_clients):
+ """Test deleting messages when messages exist."""
+ _, _, container = mock_cosmos_clients
+ cosmos_client.get_messages = AsyncMock(return_value=[
+ {"id": "m1"}, {"id": "m2"}
+ ])
+ container.delete_item = AsyncMock(return_value=True)
+ result = await cosmos_client.delete_messages("c1", "user1")
+ assert len(result) == 2
+
+ @pytest.mark.asyncio
+ async def test_delete_messages_no_messages(self, cosmos_client):
+ """Test delete_messages returns empty list when there are no messages."""
+ cosmos_client.get_messages = AsyncMock(return_value=[])
+ result = await cosmos_client.delete_messages("c1", "user1")
+ assert result == []
+
+ @pytest.mark.asyncio
+ async def test_delete_conversation_found(self, cosmos_client, mock_cosmos_clients):
+ """Test deleting an existing conversation."""
+ _, _, container = mock_cosmos_clients
+ container.read_item = AsyncMock(return_value={"id": "c1"})
+ container.delete_item = AsyncMock(return_value=True)
+ result = await cosmos_client.delete_conversation("user1", "c1")
+ assert result is True
+
+ @pytest.mark.asyncio
+ async def test_delete_conversation_not_found(self, cosmos_client, mock_cosmos_clients):
+ """Test deleting a non-existent conversation returns True (no-op)."""
+ _, _, container = mock_cosmos_clients
+ container.read_item = AsyncMock(return_value=None)
+ result = await cosmos_client.delete_conversation("user1", "none")
+ assert result is True
diff --git a/src/tests/api/common/logging/test_event_utils.py b/src/tests/api/common/logging/test_event_utils.py
index 159367ea4..db633ff85 100644
--- a/src/tests/api/common/logging/test_event_utils.py
+++ b/src/tests/api/common/logging/test_event_utils.py
@@ -1,5 +1,5 @@
import logging
-from unittest.mock import patch, MagicMock
+from unittest.mock import patch
import pytest
from common.logging.event_utils import track_event_if_configured
diff --git a/src/tests/api/helpers/test_azure_openai_helper.py b/src/tests/api/helpers/test_azure_openai_helper.py
deleted file mode 100644
index e2ae999c2..000000000
--- a/src/tests/api/helpers/test_azure_openai_helper.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from unittest.mock import patch, MagicMock
-import pytest
-import sys
-import os
-
-sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../api")))
-
-import helpers.azure_openai_helper as azure_openai_helper
-
-class TestAzureOpenAIHelper:
- @patch("helpers.azure_openai_helper.openai.AzureOpenAI")
- @patch("helpers.azure_openai_helper.get_bearer_token_provider")
- @patch("helpers.azure_openai_helper.get_azure_credential")
- @patch("helpers.azure_openai_helper.Config")
- def test_get_azure_openai_client(
- self, mock_config, mock_get_azure_credential, mock_token_provider, mock_azure_openai
- ):
- """Test that get_azure_openai_client returns a properly configured client."""
- # Arrange
- mock_config_instance = MagicMock()
- mock_config_instance.azure_openai_endpoint = "https://test-endpoint"
- mock_config_instance.azure_openai_api_version = "2024-01-01"
- mock_config.return_value = mock_config_instance
-
- mock_credential = MagicMock()
- mock_get_azure_credential.return_value = mock_credential
-
- mock_token = MagicMock()
- mock_token_provider.return_value = mock_token
-
- mock_client = MagicMock()
- mock_azure_openai.return_value = mock_client
-
- # Act
- client = azure_openai_helper.get_azure_openai_client()
-
- # Assert
- mock_config.assert_called_once()
- mock_get_azure_credential.assert_called_once()
- mock_token_provider.assert_called_once_with(
- mock_credential, "https://cognitiveservices.azure.com/.default"
- )
- mock_azure_openai.assert_called_once_with(
- azure_endpoint="https://test-endpoint",
- api_version="2024-01-01",
- azure_ad_token_provider=mock_token,
- )
- assert client == mock_client
\ No newline at end of file
diff --git a/src/tests/api/helpers/test_chat_helper.py b/src/tests/api/helpers/test_chat_helper.py
deleted file mode 100644
index ab4160b98..000000000
--- a/src/tests/api/helpers/test_chat_helper.py
+++ /dev/null
@@ -1,182 +0,0 @@
-import json
-import time
-import uuid
-import pytest
-from unittest.mock import patch, MagicMock, AsyncMock
-import sys
-import os
-
-sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../api")))
-
-from helpers.chat_helper import process_rag_response, complete_chat_request
-
-
-class TestChatHelper:
- @patch("helpers.chat_helper.Config")
- @patch("helpers.azure_openai_helper.openai.AzureOpenAI")
- def test_process_rag_response_success(self, mock_azure_openai, mock_config):
- # Mock the Azure OpenAI client and its response
- mock_client = MagicMock()
- mock_azure_openai.return_value = mock_client
-
- # Mock the completion response
- mock_completion = MagicMock()
- mock_completion.choices = [MagicMock()]
- mock_completion.choices[0].message.content = '{"type": "bar", "data": {"labels": ["A", "B"], "datasets": [{"data": [1, 2]}]}}'
- mock_client.chat.completions.create.return_value = mock_completion
-
- # Mock the config
- mock_config.return_value.azure_openai_endpoint = "https://test-endpoint"
- mock_config.return_value.azure_openai_api_version = "2023-05-15"
- mock_config.return_value.azure_openai_deployment_model = "gpt-4"
-
- # Test the function
- result = process_rag_response("Sample RAG response with numbers: 10, 20", "Generate a chart")
-
- # Assert the result is as expected
- expected = {"type": "bar", "data": {"labels": ["A", "B"], "datasets": [{"data": [1, 2]}]}}
- assert result == expected
-
- # Verify that the client was called correctly
- mock_client.chat.completions.create.assert_called_once()
- call_args = mock_client.chat.completions.create.call_args[1]
- assert call_args["model"] == "gpt-4"
- assert call_args["temperature"] == 0
- assert len(call_args["messages"]) == 2
- assert call_args["messages"][0]["role"] == "system"
- assert call_args["messages"][1]["role"] == "user"
-
- @patch("helpers.chat_helper.Config")
- @patch("helpers.azure_openai_helper.openai.AzureOpenAI")
- def test_process_rag_response_with_code_blocks(self, mock_azure_openai, mock_config):
- # Mock the Azure OpenAI client and its response
- mock_client = MagicMock()
- mock_azure_openai.return_value = mock_client
-
- # Mock the completion response - test handling of code blocks
- mock_completion = MagicMock()
- mock_completion.choices = [MagicMock()]
- mock_completion.choices[0].message.content = '```json\n{"type": "line", "data": {"labels": ["X", "Y"], "datasets": [{"data": [5, 10]}]}}\n```'
- mock_client.chat.completions.create.return_value = mock_completion
-
- # Mock the config
- mock_config.return_value.azure_openai_endpoint = "https://test-endpoint"
- mock_config.return_value.azure_openai_api_version = "2023-05-15"
- mock_config.return_value.azure_openai_deployment_model = "gpt-4"
-
- # Test the function
- result = process_rag_response("Sample RAG response with data", "Create a line chart")
-
- # Assert the result is as expected (code blocks removed)
- expected = {"type": "line", "data": {"labels": ["X", "Y"], "datasets": [{"data": [5, 10]}]}}
- assert result == expected
-
- @patch("helpers.chat_helper.Config")
- @patch("helpers.azure_openai_helper.openai.AzureOpenAI")
- def test_process_rag_response_error(self, mock_azure_openai, mock_config):
- # Mock the Azure OpenAI client
- mock_client = MagicMock()
- mock_azure_openai.return_value = mock_client
-
- # Make the client raise an exception
- mock_client.chat.completions.create.side_effect = Exception("Test error")
-
- # Mock the config
- mock_config.return_value.azure_openai_endpoint = "https://test-endpoint"
- mock_config.return_value.azure_openai_api_version = "2023-05-15"
-
- # Test the function
- result = process_rag_response("Sample RAG response", "Generate a chart")
-
- # Assert error handling works
- assert "error" in result
- assert result["error"] == "Chart could not be generated from this data. Please ask a different question."
-
- @patch("helpers.chat_helper.Config")
- @patch("helpers.azure_openai_helper.openai.AzureOpenAI")
- def test_process_rag_response_invalid_json(self, mock_azure_openai, mock_config):
- # Mock the Azure OpenAI client
- mock_client = MagicMock()
- mock_azure_openai.return_value = mock_client
-
- # Return invalid JSON
- mock_completion = MagicMock()
- mock_completion.choices = [MagicMock()]
- mock_completion.choices[0].message.content = '{"type": "bar", "invalid": json}'
- mock_client.chat.completions.create.return_value = mock_completion
-
- # Mock the config
- mock_config.return_value.azure_openai_endpoint = "https://test-endpoint"
- mock_config.return_value.azure_openai_api_version = "2023-05-15"
-
- # Test the function
- result = process_rag_response("Sample RAG response", "Generate a chart")
-
- # Assert JSON parsing error is handled
- assert "error" in result
- assert result["error"] == "Chart could not be generated from this data. Please ask a different question."
-
- @pytest.mark.asyncio
- @patch("helpers.chat_helper.process_rag_response")
- @patch("helpers.chat_helper.time.time")
- @patch("helpers.chat_helper.uuid.uuid4")
- async def test_complete_chat_request_success(self, mock_uuid4, mock_time, mock_process_rag):
- # Setup mocks
- mock_uuid4.return_value = "test-uuid"
- mock_time.return_value = 1234567890
-
- # Mock successful chart data generation
- chart_data = {"type": "bar", "data": {"labels": ["A", "B"], "datasets": [{"data": [1, 2]}]}}
- mock_process_rag.return_value = chart_data
-
- # Test the function
- result = await complete_chat_request("Create a chart", "Sample RAG response")
-
- # Assert the result is as expected
- expected = {
- "id": "test-uuid",
- "model": "azure-openai",
- "created": 1234567890,
- "object": chart_data
- }
- assert result == expected
-
- # Verify process_rag_response was called with correct arguments
- mock_process_rag.assert_called_once_with("Sample RAG response", "Create a chart")
-
- @pytest.mark.asyncio
- async def test_complete_chat_request_no_rag_response(self):
- # Test with no RAG response
- result = await complete_chat_request("Create a chart", None)
-
- # Assert proper error handling
- assert "error" in result
- assert result["error"] == "A previous RAG response is required to generate a chart."
-
- @pytest.mark.asyncio
- @patch("helpers.chat_helper.process_rag_response")
- async def test_complete_chat_request_process_error(self, mock_process_rag):
- # Mock process_rag_response to return an error
- mock_process_rag.return_value = {"error": "Some processing error"}
-
- # Test the function
- result = await complete_chat_request("Create a chart", "Sample RAG response")
-
- # Assert error is passed through correctly
- assert "error" in result
- assert result["error"] == "Chart could not be generated from this data. Please ask a different question."
- assert "error_desc" in result
- assert result["error_desc"] == "{'error': 'Some processing error'}"
-
- @pytest.mark.asyncio
- @patch("helpers.chat_helper.process_rag_response")
- async def test_complete_chat_request_empty_result(self, mock_process_rag):
- # Mock process_rag_response to return None
- mock_process_rag.return_value = None
-
- # Test the function
- result = await complete_chat_request("Create a chart", "Sample RAG response")
-
- # Assert error handling for empty results
- assert "error" in result
- assert result["error"] == "Chart could not be generated from this data. Please ask a different question."
\ No newline at end of file
diff --git a/src/tests/api/helpers/test_utils.py b/src/tests/api/helpers/test_utils.py
index 6fcff6c34..fec2b1190 100644
--- a/src/tests/api/helpers/test_utils.py
+++ b/src/tests/api/helpers/test_utils.py
@@ -1,223 +1,223 @@
-import pytest
-import json
-from unittest.mock import patch, AsyncMock, MagicMock
-
-import helpers.utils as utils
-
-
-@pytest.mark.asyncio
-async def test_format_as_ndjson_success():
- mock_data = [{"key": "value"}, {"another": "entry"}]
-
- async def async_gen():
- for item in mock_data:
- yield item
-
- result = []
- async for line in utils.format_as_ndjson(async_gen()):
- result.append(line.strip())
-
- expected = [json.dumps(item) for item in mock_data]
- assert result == expected
-
-
-@pytest.mark.asyncio
-async def test_format_as_ndjson_exception():
- async def async_gen():
- raise Exception("Test error")
- yield
-
- result = []
- async for line in utils.format_as_ndjson(async_gen()):
- result.append(json.loads(line.strip()))
- assert result[0]["error"] == "Test error"
-
-
-def test_parse_multi_columns_pipe():
- assert utils.parse_multi_columns("a|b|c") == ["a", "b", "c"]
-
-
-def test_parse_multi_columns_comma():
- assert utils.parse_multi_columns("a,b,c") == ["a", "b", "c"]
-
-
-@patch("helpers.utils.requests.get")
-def test_fetchUserGroups_success(mock_get):
- mock_response = {
- "value": [{"id": "123"}],
- }
- mock_get.return_value.status_code = 200
- mock_get.return_value.json.return_value = mock_response
-
- result = utils.fetchUserGroups("fake_token")
- assert result == [{"id": "123"}]
-
-
-@patch("helpers.utils.requests.get")
-def test_fetchUserGroups_with_nextLink(mock_get):
- mock_response_1 = {
- "value": [{"id": "123"}],
- "@odata.nextLink": "next_link"
- }
- mock_response_2 = {
- "value": [{"id": "456"}],
- }
-
- def side_effect(url, headers):
- mock = MagicMock()
- if url == "https://graph.microsoft.com/v1.0/me/transitiveMemberOf?$select=id":
- mock.status_code = 200
- mock.json.return_value = mock_response_1
- else:
- mock.status_code = 200
- mock.json.return_value = mock_response_2
- return mock
-
- mock_get.side_effect = side_effect
-
- result = utils.fetchUserGroups("fake_token")
- assert {"id": "123"} in result and {"id": "456"} in result
-
-
-@patch("helpers.utils.requests.get", side_effect=Exception("Request error"))
-def test_fetchUserGroups_exception(mock_get):
- result = utils.fetchUserGroups("fake_token")
- assert result == []
-
-
-@patch("helpers.utils.fetchUserGroups")
-@patch("helpers.utils.AZURE_SEARCH_PERMITTED_GROUPS_COLUMN", "group_column")
-def test_generateFilterString(mock_fetch):
- mock_fetch.return_value = [{"id": "1"}, {"id": "2"}]
- result = utils.generateFilterString("token")
- assert "group_column/any(g:search.in(g, '1, 2'))" in result
-
-
-@patch("helpers.utils.fetchUserGroups", return_value=[])
-@patch("helpers.utils.AZURE_SEARCH_PERMITTED_GROUPS_COLUMN", "group_column")
-def test_generateFilterString_empty_groups(mock_fetch):
- result = utils.generateFilterString("token")
- assert "group_column/any(g:search.in(g, ''))" in result
-
-
-def test_format_non_streaming_response_with_context():
- chatCompletion = MagicMock()
- chatCompletion.id = "1"
- chatCompletion.model = "gpt"
- chatCompletion.created = 123
- chatCompletion.object = "chat"
- message = MagicMock()
- message.context = {"source": "test"}
- message.content = "response"
- choice = MagicMock()
- choice.message = message
- chatCompletion.choices = [choice]
-
- result = utils.format_non_streaming_response(chatCompletion, {"meta": 1}, "req-id")
- assert result["choices"][0]["messages"][0]["role"] == "tool"
- assert result["choices"][0]["messages"][1]["role"] == "assistant"
-
-
-def test_format_non_streaming_response_no_choices():
- chatCompletion = MagicMock()
- chatCompletion.id = "1"
- chatCompletion.model = "gpt"
- chatCompletion.created = 123
- chatCompletion.object = "chat"
- chatCompletion.choices = []
-
- result = utils.format_non_streaming_response(chatCompletion, {}, "req-id")
- assert result == {}
-
-
-def test_format_stream_response_with_context():
- chunk = MagicMock()
- chunk.id = "1"
- chunk.model = "gpt"
- chunk.created = 123
- chunk.object = "chat"
- delta = MagicMock()
- delta.context = {"source": "stream"}
- delta.role = "tool"
- choice = MagicMock()
- choice.delta = delta
- chunk.choices = [choice]
-
- result = utils.format_stream_response(chunk, {"meta": 1}, "req-id")
- assert result["choices"][0]["messages"][0]["role"] == "tool"
-
-
-def test_format_stream_response_with_content():
- chunk = MagicMock()
- chunk.id = "1"
- chunk.model = "gpt"
- chunk.created = 123
- chunk.object = "chat"
-
- delta = MagicMock()
- delta.content = "Hello"
- delta.role = "assistant"
- # Ensure delta does NOT have a context attribute
- del delta.context
-
- choice = MagicMock()
- choice.delta = delta
-
- chunk.choices = [choice]
-
- result = utils.format_stream_response(chunk, {}, "req-id")
- assert result["choices"][0]["messages"][0]["content"] == "Hello"
- assert result["choices"][0]["messages"][0]["role"] == "assistant"
-
-
-def test_format_stream_response_empty():
- chunk = MagicMock()
- chunk.id = "1"
- chunk.model = "gpt"
- chunk.created = 123
- chunk.object = "chat"
- chunk.choices = []
-
- result = utils.format_stream_response(chunk, {}, "req-id")
- assert result == {}
-
-
-def test_format_pf_non_streaming_response_valid():
- chatCompletion = {
- "id": "1",
- "response": "Answer",
- "citations": "Refs"
- }
- result = utils.format_pf_non_streaming_response(
- chatCompletion, {}, "response", "citations"
- )
- assert result["choices"][0]["messages"][0]["content"] == "Answer"
-
-
-def test_format_pf_non_streaming_response_error_key():
- chatCompletion = {"error": "Failure"}
- result = utils.format_pf_non_streaming_response(chatCompletion, {}, "r", "c")
- assert result["error"] == "Failure"
-
-
-def test_format_pf_non_streaming_response_none():
- result = utils.format_pf_non_streaming_response(None, {}, "r", "c")
- assert "error" in result
-
-
-def test_format_pf_non_streaming_response_exception():
- badCompletion = {"id": "1", "invalid": object()}
- result = utils.format_pf_non_streaming_response(badCompletion, {}, "invalid", "c")
- assert isinstance(result, dict)
-
-
-def test_convert_to_pf_format_valid():
- input_json = {
- "messages": [
- {"role": "user", "content": "Hello"},
- {"role": "assistant", "content": "Hi"}
- ]
- }
- result = utils.convert_to_pf_format(input_json, "input", "output")
- assert result[0]["inputs"]["input"] == "Hello"
- assert result[0]["outputs"]["output"] == "Hi"
+import pytest
+import json
+from unittest.mock import patch, MagicMock
+
+import helpers.utils as utils
+
+
+@pytest.mark.asyncio
+async def test_format_as_ndjson_success():
+ mock_data = [{"key": "value"}, {"another": "entry"}]
+
+ async def async_gen():
+ for item in mock_data:
+ yield item
+
+ result = []
+ async for line in utils.format_as_ndjson(async_gen()):
+ result.append(line.strip())
+
+ expected = [json.dumps(item) for item in mock_data]
+ assert result == expected
+
+
+@pytest.mark.asyncio
+async def test_format_as_ndjson_exception():
+ async def async_gen():
+ raise Exception("Test error")
+ yield
+
+ result = []
+ async for line in utils.format_as_ndjson(async_gen()):
+ result.append(json.loads(line.strip()))
+ assert result[0]["error"] == "Test error"
+
+
+def test_parse_multi_columns_pipe():
+ assert utils.parse_multi_columns("a|b|c") == ["a", "b", "c"]
+
+
+def test_parse_multi_columns_comma():
+ assert utils.parse_multi_columns("a,b,c") == ["a", "b", "c"]
+
+
+@patch("helpers.utils.requests.get")
+def test_fetchUserGroups_success(mock_get):
+ mock_response = {
+ "value": [{"id": "123"}],
+ }
+ mock_get.return_value.status_code = 200
+ mock_get.return_value.json.return_value = mock_response
+
+ result = utils.fetchUserGroups("fake_token")
+ assert result == [{"id": "123"}]
+
+
+@patch("helpers.utils.requests.get")
+def test_fetchUserGroups_with_nextLink(mock_get):
+ mock_response_1 = {
+ "value": [{"id": "123"}],
+ "@odata.nextLink": "next_link"
+ }
+ mock_response_2 = {
+ "value": [{"id": "456"}],
+ }
+
+ def side_effect(url, headers):
+ mock = MagicMock()
+ if url == "https://graph.microsoft.com/v1.0/me/transitiveMemberOf?$select=id":
+ mock.status_code = 200
+ mock.json.return_value = mock_response_1
+ else:
+ mock.status_code = 200
+ mock.json.return_value = mock_response_2
+ return mock
+
+ mock_get.side_effect = side_effect
+
+ result = utils.fetchUserGroups("fake_token")
+ assert {"id": "123"} in result and {"id": "456"} in result
+
+
+@patch("helpers.utils.requests.get", side_effect=Exception("Request error"))
+def test_fetchUserGroups_exception(mock_get):
+ result = utils.fetchUserGroups("fake_token")
+ assert result == []
+
+
+@patch("helpers.utils.fetchUserGroups")
+@patch("helpers.utils.AZURE_SEARCH_PERMITTED_GROUPS_COLUMN", "group_column")
+def test_generateFilterString(mock_fetch):
+ mock_fetch.return_value = [{"id": "1"}, {"id": "2"}]
+ result = utils.generateFilterString("token")
+ assert "group_column/any(g:search.in(g, '1, 2'))" in result
+
+
+@patch("helpers.utils.fetchUserGroups", return_value=[])
+@patch("helpers.utils.AZURE_SEARCH_PERMITTED_GROUPS_COLUMN", "group_column")
+def test_generateFilterString_empty_groups(mock_fetch):
+ result = utils.generateFilterString("token")
+ assert "group_column/any(g:search.in(g, ''))" in result
+
+
+def test_format_non_streaming_response_with_context():
+ chatCompletion = MagicMock()
+ chatCompletion.id = "1"
+ chatCompletion.model = "gpt"
+ chatCompletion.created = 123
+ chatCompletion.object = "chat"
+ message = MagicMock()
+ message.context = {"source": "test"}
+ message.content = "response"
+ choice = MagicMock()
+ choice.message = message
+ chatCompletion.choices = [choice]
+
+ result = utils.format_non_streaming_response(chatCompletion, {"meta": 1}, "req-id")
+ assert result["choices"][0]["messages"][0]["role"] == "tool"
+ assert result["choices"][0]["messages"][1]["role"] == "assistant"
+
+
+def test_format_non_streaming_response_no_choices():
+ chatCompletion = MagicMock()
+ chatCompletion.id = "1"
+ chatCompletion.model = "gpt"
+ chatCompletion.created = 123
+ chatCompletion.object = "chat"
+ chatCompletion.choices = []
+
+ result = utils.format_non_streaming_response(chatCompletion, {}, "req-id")
+ assert result == {}
+
+
+def test_format_stream_response_with_context():
+ chunk = MagicMock()
+ chunk.id = "1"
+ chunk.model = "gpt"
+ chunk.created = 123
+ chunk.object = "chat"
+ delta = MagicMock()
+ delta.context = {"source": "stream"}
+ delta.role = "tool"
+ choice = MagicMock()
+ choice.delta = delta
+ chunk.choices = [choice]
+
+ result = utils.format_stream_response(chunk, {"meta": 1}, "req-id")
+ assert result["choices"][0]["messages"][0]["role"] == "tool"
+
+
+def test_format_stream_response_with_content():
+ chunk = MagicMock()
+ chunk.id = "1"
+ chunk.model = "gpt"
+ chunk.created = 123
+ chunk.object = "chat"
+
+ delta = MagicMock()
+ delta.content = "Hello"
+ delta.role = "assistant"
+ # Ensure delta does NOT have a context attribute
+ del delta.context
+
+ choice = MagicMock()
+ choice.delta = delta
+
+ chunk.choices = [choice]
+
+ result = utils.format_stream_response(chunk, {}, "req-id")
+ assert result["choices"][0]["messages"][0]["content"] == "Hello"
+ assert result["choices"][0]["messages"][0]["role"] == "assistant"
+
+
+def test_format_stream_response_empty():
+ chunk = MagicMock()
+ chunk.id = "1"
+ chunk.model = "gpt"
+ chunk.created = 123
+ chunk.object = "chat"
+ chunk.choices = []
+
+ result = utils.format_stream_response(chunk, {}, "req-id")
+ assert result == {}
+
+
+def test_format_pf_non_streaming_response_valid():
+ chatCompletion = {
+ "id": "1",
+ "response": "Answer",
+ "citations": "Refs"
+ }
+ result = utils.format_pf_non_streaming_response(
+ chatCompletion, {}, "response", "citations"
+ )
+ assert result["choices"][0]["messages"][0]["content"] == "Answer"
+
+
+def test_format_pf_non_streaming_response_error_key():
+ chatCompletion = {"error": "Failure"}
+ result = utils.format_pf_non_streaming_response(chatCompletion, {}, "r", "c")
+ assert result["error"] == "Failure"
+
+
+def test_format_pf_non_streaming_response_none():
+ result = utils.format_pf_non_streaming_response(None, {}, "r", "c")
+ assert "error" in result
+
+
+def test_format_pf_non_streaming_response_exception():
+ badCompletion = {"id": "1", "invalid": object()}
+ result = utils.format_pf_non_streaming_response(badCompletion, {}, "invalid", "c")
+ assert isinstance(result, dict)
+
+
+def test_convert_to_pf_format_valid():
+ input_json = {
+ "messages": [
+ {"role": "user", "content": "Hello"},
+ {"role": "assistant", "content": "Hi"}
+ ]
+ }
+ result = utils.convert_to_pf_format(input_json, "input", "output")
+ assert result[0]["inputs"]["input"] == "Hello"
+ assert result[0]["outputs"]["output"] == "Hi"
diff --git a/src/tests/api/services/test_chat_service.py b/src/tests/api/services/test_chat_service.py
index f1373dd1d..9f372483a 100644
--- a/src/tests/api/services/test_chat_service.py
+++ b/src/tests/api/services/test_chat_service.py
@@ -1,4 +1,3 @@
-import asyncio
import json
import time
from unittest.mock import AsyncMock, MagicMock, patch
@@ -14,7 +13,7 @@ def chat_service():
"""Create a ChatService instance for testing."""
with patch("services.chat_service.Config") as mock_config:
mock_config_instance = MagicMock()
- mock_config_instance.azure_openai_deployment_model = "gpt-4o-mini"
+
mock_config_instance.orchestrator_agent_name = "test-orchestrator"
mock_config_instance.azure_client_id = "test-client-id"
mock_config_instance.ai_project_endpoint = "https://test.endpoint.com"
@@ -150,7 +149,6 @@ def test_init(self, mock_config_class):
"""Test ChatService initialization."""
# Configure mock Config
mock_config_instance = MagicMock()
- mock_config_instance.azure_openai_deployment_model = "gpt-4o-mini"
mock_config_instance.orchestrator_agent_name = "test-orchestrator"
mock_config_instance.azure_client_id = "test-client-id"
mock_config_instance.ai_project_endpoint = "https://test.endpoint.com"
@@ -158,7 +156,6 @@ def test_init(self, mock_config_class):
service = ChatService()
- assert service.azure_openai_deployment_name == "gpt-4o-mini"
assert service.orchestrator_agent_name == "test-orchestrator"
assert service.azure_client_id == "test-client-id"
assert service.ai_project_endpoint == "https://test.endpoint.com"
@@ -229,12 +226,16 @@ async def mock_run(*args, **kwargs):
async for chunk in chat_service.stream_openai_text("conv123", "test query"):
result_chunks.append(chunk)
- # Verify
+ # Verify - stream_openai_text now yields (role, content) tuples
assert len(result_chunks) > 0
- full_response = "".join(result_chunks)
- assert "Hello" in full_response
- assert "World" in full_response
- assert "citations" in full_response
+ assistant_content = "".join(content for role, content in result_chunks if role == "assistant")
+ tool_content = "".join(content for role, content in result_chunks if role == "tool")
+
+ assert "Hello" in assistant_content
+ assert "World" in assistant_content
+ # Citations come in tool message as a valid JSON array
+ citations = json.loads(tool_content)
+ assert isinstance(citations, list)
@pytest.mark.asyncio
@patch("services.chat_service.SQLTool")
@@ -356,11 +357,19 @@ async def mock_run(*args, **kwargs):
async for chunk in chat_service.stream_openai_text("conv123", "test query"):
result_chunks.append(chunk)
- # Verify citations are included
- full_response = "".join(result_chunks)
- assert "citations" in full_response
- assert "Test Documentation" in full_response
- assert "http://example.com/doc" in full_response
+ # Verify citations are included - stream_openai_text now yields (role, content) tuples
+ assistant_content = "".join(content for role, content in result_chunks if role == "assistant")
+ tool_content = "".join(content for role, content in result_chunks if role == "tool")
+
+ assert "Answer with citation" in assistant_content
+ # Citations are sent as tool message with JSON; validate structure and contents
+ citations = json.loads(tool_content)
+ assert isinstance(citations, list)
+ assert len(citations) >= 1
+ first_citation = citations[0]
+ assert isinstance(first_citation, dict)
+ assert first_citation.get("title") == "Test Documentation"
+ assert first_citation.get("url") == "http://example.com/doc"
@pytest.mark.asyncio
@patch("services.chat_service.SQLTool")
@@ -415,10 +424,72 @@ async def mock_run(*args, **kwargs):
result_chunks.append(chunk)
# Verify citation markers are replaced with [1], [2], etc.
- full_response = "".join(result_chunks)
- assert "[1]" in full_response
- assert "[2]" in full_response
- assert "【" not in full_response # Original markers should be replaced
+ # stream_openai_text now yields (role, content) tuples
+ assistant_content = "".join(content for role, content in result_chunks if role == "assistant")
+
+ assert "[1]" in assistant_content
+ assert "[2]" in assistant_content
+ assert "【" not in assistant_content # Original markers should be replaced
+
+ @pytest.mark.asyncio
+ @patch("services.chat_service.SQLTool")
+ @patch("services.chat_service.get_sqldb_connection", new_callable=AsyncMock)
+ @patch("services.chat_service.AzureAIProjectAgentProvider")
+ @patch("services.chat_service.AIProjectClient")
+ @patch("services.chat_service.get_azure_credential_async", new_callable=AsyncMock)
+ async def test_stream_openai_text_with_citation_markers_without_dagger(
+ self, mock_credential, mock_project_client_class, mock_provider_class,
+ mock_sqldb_conn, mock_sql_tool, chat_service
+ ):
+ """Test streaming replaces citation markers that lack the † character."""
+ # Setup mocks
+ mock_cred = AsyncMock()
+ mock_cred.__aenter__ = AsyncMock(return_value=mock_cred)
+ mock_cred.__aexit__ = AsyncMock(return_value=None)
+ mock_credential.return_value = mock_cred
+
+ mock_project_client = MagicMock()
+ mock_project_client.__aenter__ = AsyncMock(return_value=mock_project_client)
+ mock_project_client.__aexit__ = AsyncMock(return_value=None)
+ mock_openai_client = MagicMock()
+ mock_conversation = MagicMock()
+ mock_conversation.id = "test-thread-id"
+ mock_openai_client.conversations.create = AsyncMock(return_value=mock_conversation)
+ mock_project_client.get_openai_client.return_value = mock_openai_client
+ mock_project_client_class.return_value = mock_project_client
+
+ # Mock agent with mixed citation markers (with and without †)
+ mock_agent = MagicMock()
+ mock_chunk = MagicMock()
+ mock_chunk.text = "Answer 【4:1†source】 and 【4:3 source】 and 【4:4 source】"
+ mock_chunk.contents = []
+
+ async def mock_run(*args, **kwargs):
+ yield mock_chunk
+
+ mock_agent.run = mock_run
+
+ mock_provider = MagicMock()
+ mock_provider.get_agent = AsyncMock(return_value=mock_agent)
+ mock_provider_class.return_value = mock_provider
+
+ mock_sqldb_conn.return_value = AsyncMock()
+ mock_tool_instance = MagicMock()
+ mock_tool_instance.get_sql_response = MagicMock()
+ mock_sql_tool.return_value = mock_tool_instance
+
+ # Execute
+ result_chunks = []
+ async for chunk in chat_service.stream_openai_text("conv123", "test query"):
+ result_chunks.append(chunk)
+
+ # Verify all citation markers are replaced
+ assistant_content = "".join(content for role, content in result_chunks if role == "assistant")
+
+ assert "[1]" in assistant_content
+ assert "[2]" in assistant_content
+ assert "[3]" in assistant_content
+ assert "【" not in assistant_content # All markers should be replaced
@pytest.mark.asyncio
@patch("services.chat_service.SQLTool")
@@ -636,17 +707,18 @@ async def mock_run(*args, **kwargs):
result_chunks.append(chunk)
# Verify fallback message is provided
- full_response = "".join(result_chunks)
- assert "cannot answer" in full_response.lower() or "citations" in full_response
+ # stream_openai_text now yields (role, content) tuples
+ full_response = "".join(content for role, content in result_chunks if role == "assistant")
+ assert "cannot answer" in full_response.lower()
@pytest.mark.asyncio
async def test_stream_chat_request_success(self, chat_service):
"""Test successful stream_chat_request."""
- # Mock stream_openai_text to return chunks
+ # Mock stream_openai_text to return (role, content) tuples
async def mock_stream(*args, **kwargs):
- yield '{ "answer": "Hello'
- yield ' World'
- yield ', "citations": []}'
+ yield ("assistant", "Hello")
+ yield ("assistant", " World")
+ yield ("tool", '[{"url": "http://example.com", "title": "doc1"}]')
chat_service.stream_openai_text = mock_stream
@@ -657,12 +729,29 @@ async def mock_stream(*args, **kwargs):
async for chunk in generator:
chunks.append(chunk)
- # Verify
- assert len(chunks) > 0
+ # Verify: 2 assistant chunks + 1 tool chunk = 3 total
+ assert len(chunks) == 3
for chunk in chunks:
data = json.loads(chunk.strip())
assert "choices" in data
assert isinstance(data["choices"], list)
+ delta = data["choices"][0]["delta"]
+ assert "content" in delta
+ assert "role" in delta
+
+ # Verify assistant deltas carry answer text
+ d0 = json.loads(chunks[0].strip())["choices"][0]["delta"]
+ assert d0["role"] == "assistant"
+ assert d0["content"] == "Hello"
+
+ d1 = json.loads(chunks[1].strip())["choices"][0]["delta"]
+ assert d1["role"] == "assistant"
+ assert d1["content"] == " World"
+
+ # Verify citations come as role "tool"
+ d2 = json.loads(chunks[2].strip())["choices"][0]["delta"]
+ assert d2["role"] == "tool"
+ assert "doc1" in d2["content"]
@pytest.mark.asyncio
async def test_stream_chat_request_http_exception(self, chat_service):
diff --git a/src/tests/api/services/test_history_service.py b/src/tests/api/services/test_history_service.py
index 92bfdef8c..a598d2f85 100644
--- a/src/tests/api/services/test_history_service.py
+++ b/src/tests/api/services/test_history_service.py
@@ -4,7 +4,6 @@
# ---- Import service under test ----
from services.history_service import HistoryService
-from azure.ai.agents.models import MessageRole
@pytest.fixture
@@ -16,7 +15,6 @@ def mock_config_instance():
config.azure_cosmosdb_conversations_container = "test-container"
config.azure_cosmosdb_enable_feedback = True
# Azure AI Foundry SDK configuration
- config.azure_openai_deployment_model = "gpt-4o-mini" # Still needed for model parameter
config.azure_client_id = "test-client-id"
config.ai_project_endpoint = "https://test-aif.services.ai.azure.com/api/projects/test-project"
config.ai_project_api_version = "2025-05-01"
@@ -47,7 +45,6 @@ def test_init(self, history_service, mock_config_instance):
assert history_service.use_chat_history_enabled == mock_config_instance.use_chat_history_enabled
assert history_service.azure_cosmosdb_database == mock_config_instance.azure_cosmosdb_database
assert history_service.azure_cosmosdb_account == mock_config_instance.azure_cosmosdb_account
- assert history_service.azure_openai_deployment_name == mock_config_instance.azure_openai_deployment_model
assert history_service.ai_project_endpoint == mock_config_instance.ai_project_endpoint
assert history_service.ai_project_api_version == mock_config_instance.ai_project_api_version
assert history_service.solution_name == mock_config_instance.solution_name
@@ -127,7 +124,7 @@ async def test_generate_title_failed_run(self, history_service):
mock_project_client.agents.runs.create_and_process.return_value = mock_run
with patch("services.history_service.AIProjectClient", return_value=mock_project_client):
- with patch("services.history_service.get_azure_credential"):
+ with patch("services.history_service.get_azure_credential_async"):
result = await history_service.generate_title(conversation_messages)
assert result == "Test message" # Should fall back to truncated user message
diff --git a/tests/e2e-test/base/base.py b/tests/e2e-test/base/base.py
index 5114809cc..c14e79924 100644
--- a/tests/e2e-test/base/base.py
+++ b/tests/e2e-test/base/base.py
@@ -1,55 +1,52 @@
-"""
-BasePage Module
-Contains base page object class with common methods
-"""
-from config.constants import *
-import json
-from dotenv import load_dotenv
-import os
-import uuid
-import time
-
-class BasePage:
- def __init__(self, page):
- self.page = page
-
- def scroll_into_view(self,locator):
- reference_list = locator
- locator.nth(reference_list.count()-1).scroll_into_view_if_needed()
-
- def is_visible(self,locator):
- locator.is_visible()
-
- def validate_response_status(self,questions):
- load_dotenv()
- WEB_URL = os.getenv("web_url")
-
- url = f"{API_URL}/api/chat"
-
-
- user_message_id = str(uuid.uuid4())
- assistant_message_id = str(uuid.uuid4())
- conversation_id = str(uuid.uuid4())
-
- payload = {
- "messages": [{"role": "user", "content": questions,
- "id": user_message_id}],
- "conversation_id": conversation_id,
- }
- # Serialize the payload to JSON
- payload_json = json.dumps(payload)
- headers = {
- "Content-Type": "application/json-lines",
- "Accept": "*/*"
- }
- # response = self.page.request.post(url, headers=headers, data=payload_json, timeout=60000)
- start = time.time()
- response = self.page.request.post(url, headers=headers, data=payload_json, timeout=90000)
- duration = time.time() - start
-
- print(f"✅succeeded in {duration:.2f}s")
- # Check the response status code
- assert response.status == 200, "response code is " + str(response.status)
-
- self.page.wait_for_timeout(4000)
-
+"""
+BasePage Module
+Contains base page object class with common methods
+"""
+from config.constants import *
+import json
+from dotenv import load_dotenv
+import uuid
+import time
+
+class BasePage:
+ def __init__(self, page):
+ self.page = page
+
+ def scroll_into_view(self,locator):
+ reference_list = locator
+ locator.nth(reference_list.count()-1).scroll_into_view_if_needed()
+
+ def is_visible(self,locator):
+ locator.is_visible()
+
+ def validate_response_status(self,questions):
+ load_dotenv()
+
+ url = f"{API_URL}/api/chat"
+
+
+ user_message_id = str(uuid.uuid4())
+ conversation_id = str(uuid.uuid4())
+
+ payload = {
+ "messages": [{"role": "user", "content": questions,
+ "id": user_message_id}],
+ "conversation_id": conversation_id,
+ }
+ # Serialize the payload to JSON
+ payload_json = json.dumps(payload)
+ headers = {
+ "Content-Type": "application/json-lines",
+ "Accept": "*/*"
+ }
+ # response = self.page.request.post(url, headers=headers, data=payload_json, timeout=60000)
+ start = time.time()
+ response = self.page.request.post(url, headers=headers, data=payload_json, timeout=90000)
+ duration = time.time() - start
+
+ print(f"✅succeeded in {duration:.2f}s")
+ # Check the response status code
+ assert response.status == 200, "response code is " + str(response.status)
+
+ self.page.wait_for_timeout(4000)
+
diff --git a/tests/e2e-test/config/constants.py b/tests/e2e-test/config/constants.py
index 5dcb76b26..5e3b9819b 100644
--- a/tests/e2e-test/config/constants.py
+++ b/tests/e2e-test/config/constants.py
@@ -1,38 +1,41 @@
-"""
-Constants Module
-Contains configuration constants and loads test data
-"""
-from dotenv import load_dotenv
-import os
-import json
-
-load_dotenv()
-URL = os.getenv('url')
-if URL.endswith('/'):
- URL = URL[:-1]
-
-load_dotenv()
-API_URL = os.getenv('api_url')
-if API_URL.endswith('/'):
- API_URL = API_URL[:-1]
-
-# Get the absolute path to the repository root
-repo_root = os.getenv('GITHUB_WORKSPACE', os.getcwd())
-
-#remove 'tests/e2e-test' from below path if running locally
-
-# Load Telecom prompts
-telecom_json_file_path = os.path.join(repo_root, 'tests/e2e-test', 'testdata', 'telecom_prompts.json')
-with open(telecom_json_file_path, 'r') as file:
- telecom_data = json.load(file)
- telecom_questions = telecom_data['questions']
-
-# Load ITHelpdesk prompts
-ithelpdesk_json_file_path = os.path.join(repo_root, 'tests/e2e-test', 'testdata', 'ithelpdesk_prompts.json')
-with open(ithelpdesk_json_file_path, 'r') as file:
- ithelpdesk_data = json.load(file)
- ithelpdesk_questions = ithelpdesk_data['questions']
-
-# Backward compatibility - keep 'questions' as alias for telecom_questions
-questions = telecom_questions
-
+"""
+Constants Module
+Contains configuration constants and loads test data
+"""
+from dotenv import load_dotenv
+import os
+import json
+
+load_dotenv()
+URL = os.getenv('url')
+if URL.endswith('/'):
+ URL = URL[:-1]
+
+load_dotenv()
+API_URL = os.getenv('api_url')
+if API_URL.endswith('/'):
+ API_URL = API_URL[:-1]
+
+# Get the absolute path to the repository root
+repo_root = os.getenv('GITHUB_WORKSPACE', os.getcwd())
+
+#remove 'tests/e2e-test' from below path if running locally
+
+# Load Telecom prompts
+telecom_json_file_path = os.path.join(repo_root, 'tests/e2e-test', 'testdata', 'telecom_prompts.json')
+with open(telecom_json_file_path, 'r') as file:
+ telecom_data = json.load(file)
+ telecom_questions = telecom_data['questions']
+
+# Load ITHelpdesk prompts
+ithelpdesk_json_file_path = os.path.join(repo_root, 'tests/e2e-test', 'testdata', 'ithelpdesk_prompts.json')
+with open(ithelpdesk_json_file_path, 'r') as file:
+ ithelpdesk_data = json.load(file)
+ ithelpdesk_questions = ithelpdesk_data['questions']
+
+# Backward compatibility - keep 'questions' as alias for telecom_questions
+questions = telecom_questions
+
+# Public exports
+__all__ = ['URL', 'API_URL', 'telecom_questions', 'ithelpdesk_questions', 'questions']
+
diff --git a/tests/e2e-test/pages/HomePage.py b/tests/e2e-test/pages/HomePage.py
index 775de27ff..0722d9e71 100644
--- a/tests/e2e-test/pages/HomePage.py
+++ b/tests/e2e-test/pages/HomePage.py
@@ -23,6 +23,7 @@ class HomePage(BasePage):
def __init__(self, page):
+ super().__init__(page)
self.page = page
def home_page_load(self):
diff --git a/tests/e2e-test/pages/KMGenericPage.py b/tests/e2e-test/pages/KMGenericPage.py
index 8a3957b74..493e81592 100644
--- a/tests/e2e-test/pages/KMGenericPage.py
+++ b/tests/e2e-test/pages/KMGenericPage.py
@@ -7,6 +7,7 @@
class KMGenericPage(BasePage):
def __init__(self, page):
+ super().__init__(page)
self.page = page
def open_url(self):
diff --git a/tests/e2e-test/pages/loginPage.py b/tests/e2e-test/pages/loginPage.py
index 0ee59f779..b3205b8e4 100644
--- a/tests/e2e-test/pages/loginPage.py
+++ b/tests/e2e-test/pages/loginPage.py
@@ -11,6 +11,7 @@ class LoginPage(BasePage):
PERMISSION_ACCEPT_BUTTON = "//input[@type='submit']"
def __init__(self, page):
+ super().__init__(page)
self.page = page
def authenticate(self, username,password):
diff --git a/tests/e2e-test/tests/test_ithelpdesk_smoke_tc.py b/tests/e2e-test/tests/test_ithelpdesk_smoke_tc.py
index 100348ec3..e24cb38f9 100644
--- a/tests/e2e-test/tests/test_ithelpdesk_smoke_tc.py
+++ b/tests/e2e-test/tests/test_ithelpdesk_smoke_tc.py
@@ -1,362 +1,361 @@
-"""
-KM Generic Smoke Test Module - ITHelpdesk
-Tests the complete smoke testing workflow for KM Generic application
-"""
-from pages.KMGenericPage import KMGenericPage
-import logging
-from pages.HomePage import HomePage
-from playwright.sync_api import expect
-import time
-from config.constants import ithelpdesk_questions
-import io
-
-logger = logging.getLogger(__name__)
-
-
-def test_user_filter_functioning(login_logout, request):
- """
- KM Generic Smoke Test - ITHelpdesk:
- 1. Open KM Generic URL
- 2. Validate charts, labels, chat & history panels
- 3. Confirm user filter is visible
- 4. Change filter combinations
- 5. Click Apply
- 6. Verify screen blur + chart update
- """
-
- # Set custom test name for pytest HTML report
- request.node._nodeid = "14480 - KM Generic - ITHelpdesk - Validate filter functionality should work as per filtered data selected"
-
- page = login_logout
- km_page = KMGenericPage(page)
-
- logger.info("Step 1: Open KM Generic URL")
- km_page.open_url()
-
- logger.info("Step 2: Validate charts, labels, chat & history panels")
- km_page.validate_dashboard_ui()
-
- logger.info("Step 3: Confirm user filter is visible")
- km_page.validate_user_filter_visible()
-
- logger.info("Step 4: Change filter combinations")
- km_page.update_filters()
-
- logger.info("Step 5: Click Apply")
- km_page.click_apply_button()
-
- logger.info("Step 6: Verify screen blur + chart update")
- km_page.verify_blur_and_chart_update()
-
-
-def test_after_filter_functioning(login_logout, request):
- """
- KM Generic Smoke Test - ITHelpdesk:
- 1. Open KM Generic URL
- 2. Changes the value of user filter
- 3. Notice the value/data change in the chart/graphs tables
- """
-
- # Remove custom test name logic for pytest HTML report
- request.node._nodeid = "14482 - KM Generic - ITHelpdesk - Validate after applying filter charts/graphs should show filtered data"
-
- page = login_logout
- km_page = KMGenericPage(page)
-
- logger.info("Step 1: Open KM Generic URL")
- km_page.open_url()
-
- logger.info("Step 2: Changes the value of user filter")
- km_page.update_filters()
-
- logger.info("Step 3: Click Apply")
- km_page.click_apply_button()
-
- logger.info("Step 4: Validate filter data is reflecting in charts/graphs")
- performance_issue_data = km_page.validate_trending_topics_entry("Laptop Performance Issues")
- logger.info(f"Laptop performance issues data validated: {performance_issue_data}")
-
- km_page.validate_dashboard_charts()
-
-
-def test_hide_dashboard_and_chat_buttons(login_logout, request):
- """
- KM Generic Smoke Test - ITHelpdesk:
- 1. Open KM Generic URL
- 2. Changes the value of user filter
- 3. Notice the value/data change in the chart/graphs tables
- """
-
- # Set custom test name for pytest HTML report
- request.node._nodeid = "14485 - KM Generic - ITHelpdesk - Validate Hide Dashboard and Hide Chat buttons"
-
- page = login_logout
- km_page = KMGenericPage(page)
-
- logger.info("Step 1: Open KM Generic URL")
- km_page.open_url()
-
- logger.info("Step 2: On the left side of profile icon observe two buttons are present, Hide Dashboard & Hide Chat")
- km_page.verify_hide_dashboard_and_chat_buttons()
-
-
-def test_refine_chat_chart_output(login_logout, request):
- """
- KM Generic Smoke Test - ITHelpdesk:
- 1. Open KM Generic URL
- 2. On chat window enter the prompt which provides chat info: EX: Average handling time by topic
- 3. On chat window enter the prompt which provides chat info: EX: Generate Chart
- """
-
- # Set custom test name for pytest HTML report
- request.node._nodeid = "14526 - US_12962_KM Generic - ITHelpdesk - Improve Chart Generation Experience in Chat"
-
- page = login_logout
- km_page = KMGenericPage(page)
- home_page = HomePage(page)
-
- logger.info("Step 1: Open KM Generic URL")
- km_page.open_url()
-
- logger.info("Step 2: Verify chat response generation")
- logger.info("Step 3: On chat window enter the prompt which provides chat info: EX: Average handling time by topic")
- home_page.validate_chat_response('Average handling time by topic')
- home_page.validate_response_status('Average handling time by topic')
-
- logger.info("Step 4: On chat window enter the prompt which provides chat info: EX: Generate chart")
- home_page.validate_chat_response('Generate chart', True)
- home_page.validate_response_status('Generate chart')
-
-
-def test_chat_greeting_responses(login_logout, request):
-
- """
- KM Generic Smoke Test - ITHelpdesk:
- 1. Deploy KM Generic
- 2. Open KM Generic URL
- 3. On chat window enter the Greeting related info: EX: Hi, Good morning, Hello.
- """
-
- # Set custom test name for pytest HTML report
- request.node._nodeid = "21426 - US_20054_KM Generic - ITHelpdesk - Greeting related experience in Chat"
-
- page = login_logout
- km_page = KMGenericPage(page)
- home_page = HomePage(page)
-
- logger.info("Step 1: Open KM Generic URL")
- km_page.open_url()
-
- greetings = ["Hi, Good morning", "Hello"]
- logger.info("Step 2: On chat window enter the Greeting related info: EX: Hi, Good morning, Hello.")
- for greeting in greetings:
- home_page.enter_chat_question(greeting)
- home_page.click_send_button()
-
- # Check last assistant message for a greeting-style reply
- assistant_messages = home_page.page.locator("div.chat-message.assistant")
- last_message = assistant_messages.last
-
- # Validate greeting response
- p = last_message.locator("p")
- message_text = p.inner_text().lower()
-
- if any(keyword in message_text for keyword in ["how can i assist", "how can i help", "hello again"]):
- logger.info(f"Valid greeting response received for: {greeting}")
- else:
- raise AssertionError(f"Unexpected greeting response for '{greeting}': {message_text}")
-
- # Optional wait between messages
- home_page.page.wait_for_timeout(1000)
-
-
-def test_chat_history_panel(login_logout, request):
- """
- KM Generic Smoke Test - ITHelpdesk:
- Refactored to reuse golden path logic plus additional chat history operations
- 1. Reuse golden path test execution (load home page, delete history, execute questions)
- 2. Edit chat thread title
- 3. Verify chat history operations (delete thread, create new chat, clear all history)
- """
-
- # Set custom test name for pytest HTML report
- request.node._nodeid = "14483 - KM Generic - ITHelpdesk - Validate Chat History- user able to edit, save, delete and delete all chat history"
-
- page = login_logout
- home_page = HomePage(page)
- home_page.page = page
-
- log_capture = io.StringIO()
- handler = logging.StreamHandler(log_capture)
- logger.addHandler(handler)
-
- try:
- # Reuse golden path logic - Steps 1-2: Load home page and clear chat history
- logger.info("Step 1: Validate home page is loaded")
- start = time.time()
- home_page.home_page_load()
- duration = time.time() - start
- logger.info(f"Execution Time for 'Validate home page is loaded': {duration:.2f}s")
-
- logger.info("Step 2: Validate delete chat history")
- start = time.time()
- home_page.delete_chat_history()
- duration = time.time() - start
- logger.info(f"Execution Time for 'Validate delete chat history': {duration:.2f}s")
-
- # Reuse golden path logic - Execute all golden path questions
- failed_questions = [] # Track failed questions for final reporting
-
- for i, question in enumerate(ithelpdesk_questions, start=1):
- logger.info(f"Step {i+2}: Validate response for GP Prompt: {question}")
- start = time.time()
-
- # Retry logic: attempt up to 2 times if response is invalid
- max_retries = 2
- question_passed = False
-
- for attempt in range(max_retries):
- try:
- # Enter question and get response
- home_page.enter_chat_question(question)
- home_page.click_send_button()
- home_page.page.wait_for_timeout(8000) # Wait before validating response status
- home_page.validate_response_status(question)
- home_page.page.wait_for_timeout(5000) # Wait after validating response status
- home_page.validate_response_text(question)
-
- # If we reach here, the response was valid - break out of retry loop
- logger.info(f"[{question}] Valid response received on attempt {attempt + 1}")
- question_passed = True
- break
-
- except Exception as e:
- if attempt < max_retries - 1: # Not the last attempt
- logger.warning(f"[{question}] Attempt {attempt + 1} failed: {str(e)}")
- logger.info(f"[{question}] Retrying... (attempt {attempt + 2}/{max_retries})")
- # Wait a bit before retrying
- home_page.page.wait_for_timeout(10000)
- else: # Last attempt failed
- logger.error(f"[{question}] All {max_retries} attempts failed. Last error: {str(e)}")
- failed_questions.append({"question": question, "error": str(e)})
-
- # Only handle citations if the question passed
- if question_passed and home_page.has_reference_link():
- logger.info(f"[{question}] Reference link found. Opening citation.")
- home_page.click_reference_link_in_response()
- logger.info(f"[{question}] Closing citation.")
- home_page.close_citation()
-
- duration = time.time() - start
- logger.info(f"Execution Time for 'Validate response for GP Prompt: {question}': {duration:.2f}s")
-
- # Log summary of failed questions
- if failed_questions:
- logger.warning(f"Chat history test completed with {len(failed_questions)} failed questions out of {len(ithelpdesk_questions)} total")
- for failed in failed_questions:
- logger.error(f"Failed question: '{failed['question']}' - {failed['error']}")
- else:
- logger.info("All golden path questions passed successfully")
-
- # Additional chat history specific operations
- logger.info("Step 7: Try editing the title of chat thread")
- home_page.edit_chat_title("Updated Title")
-
- home_page.page.wait_for_timeout(2000)
-
- logger.info("Step 8: Verify the chat history is getting stored properly or not")
- logger.info("Step 9: Try deleting the chat thread from chat history panel")
- home_page.delete_first_chat_thread()
-
- home_page.page.wait_for_timeout(2000)
-
- logger.info("Step 10: Try clicking on + icon present before chat box")
- home_page.create_new_chat()
-
- home_page.page.wait_for_timeout(2000)
-
- home_page.close_chat_history()
-
- logger.info("Step 11: Click on eclipse (3 dots) and select Clear all chat history")
- home_page.delete_chat_history()
-
- finally:
- logger.removeHandler(handler)
-
-
-def test_clear_citations_on_chat_delete(login_logout, request):
- """
- KM Generic Smoke Test - ITHelpdesk:
- 1. Open KM Generic URL
- 2. Ask questions in the chat area, where the citations are provided.
- 3. Click on the any citation link.
- 4. Open Chat history panel.
- 5. In chat history panel delete complete chat history.
- 6. Observe Citation Section.
- """
-
- # Set custom test name for pytest HTML report
- request.node._nodeid = "18631 - Bug 17326 - KM Generic - ITHelpdesk - Citation should get cleared after deleting complete chat history"
-
- page = login_logout
- km_page = KMGenericPage(page)
- home_page = HomePage(page)
-
- logger.info("Step 2: Send a query to trigger a citation")
- question= "Provide a summary of performance issues users reported this week"
- home_page.enter_chat_question(question)
- home_page.click_send_button()
- # home_page.validate_chat_response(question)
- home_page.page.wait_for_timeout(3000)
-
- logger.info("Step 3: Validate citation link appears in response")
- logger.info("Step 4: Click on the citation link to open the panel")
- home_page.click_reference_link_in_response()
- home_page.page.wait_for_timeout(5000)
-
- # 6. Delete entire chat history
- home_page.delete_chat_history()
-
- # 7. Check citation section is not visible after chat history deletion
- citations_locator = page.locator("//div[contains(text(),'Citations')]")
- expect(citations_locator).not_to_be_visible(timeout=3000)
- logger.info("Citations section is not visible after chat history deletion")
-
-
-def test_citation_panel_closes_with_chat(login_logout, request):
- """
- Test to ensure citation panel closes when chat section is hidden.
- """
-
- # Set custom test name for pytest HTML report
- request.node._nodeid = "19433 - KM Generic - ITHelpdesk - Citation panel should close after hiding chat"
-
- page = login_logout
- km_page = KMGenericPage(page)
- home_page = HomePage(page)
-
- logger.info("Step 1: Navigate to KM Generic URL")
- home_page.page.reload(wait_until="networkidle")
- home_page.page.wait_for_timeout(2000)
-
- logger.info("Step 2: Send a query to trigger a citation")
- question= "Provide a summary of performance issues users reported this week"
- home_page.enter_chat_question(question)
- home_page.click_send_button()
- # home_page.validate_chat_response(question)
- home_page.page.wait_for_timeout(3000)
-
- logger.info("Step 3: Validate citation link appears in response")
- logger.info("Step 4: Click on the citation link to open the panel")
- home_page.click_reference_link_in_response()
- home_page.page.wait_for_timeout(3000)
-
- logger.info("Step 5: Click on 'Hide Chat' button")
- km_page.verify_hide_dashboard_and_chat_buttons()
- home_page.page.wait_for_timeout(3000)
-
- logger.info("Step 6: Verify citation panel is closed after hiding chat")
- citation_panel = km_page.page.locator("div.citationPanel")
- expect(citation_panel).not_to_be_visible(timeout=3000)
-
- logger.info("✅ Citation panel successfully closed with chat.")
+"""
+KM Generic Smoke Test Module - ITHelpdesk
+Tests the complete smoke testing workflow for KM Generic application
+"""
+from pages.KMGenericPage import KMGenericPage
+import logging
+from pages.HomePage import HomePage
+from playwright.sync_api import expect
+import time
+from config.constants import ithelpdesk_questions
+import io
+
+logger = logging.getLogger(__name__)
+
+
+def test_user_filter_functioning(login_logout, request):
+ """
+ KM Generic Smoke Test - ITHelpdesk:
+ 1. Open KM Generic URL
+ 2. Validate charts, labels, chat & history panels
+ 3. Confirm user filter is visible
+ 4. Change filter combinations
+ 5. Click Apply
+ 6. Verify screen blur + chart update
+ """
+
+ # Set custom test name for pytest HTML report
+ request.node._nodeid = "14480 - KM Generic - ITHelpdesk - Validate filter functionality should work as per filtered data selected"
+
+ page = login_logout
+ km_page = KMGenericPage(page)
+
+ logger.info("Step 1: Open KM Generic URL")
+ km_page.open_url()
+
+ logger.info("Step 2: Validate charts, labels, chat & history panels")
+ km_page.validate_dashboard_ui()
+
+ logger.info("Step 3: Confirm user filter is visible")
+ km_page.validate_user_filter_visible()
+
+ logger.info("Step 4: Change filter combinations")
+ km_page.update_filters()
+
+ logger.info("Step 5: Click Apply")
+ km_page.click_apply_button()
+
+ logger.info("Step 6: Verify screen blur + chart update")
+ km_page.verify_blur_and_chart_update()
+
+
+def test_after_filter_functioning(login_logout, request):
+ """
+ KM Generic Smoke Test - ITHelpdesk:
+ 1. Open KM Generic URL
+ 2. Changes the value of user filter
+ 3. Notice the value/data change in the chart/graphs tables
+ """
+
+ # Remove custom test name logic for pytest HTML report
+ request.node._nodeid = "14482 - KM Generic - ITHelpdesk - Validate after applying filter charts/graphs should show filtered data"
+
+ page = login_logout
+ km_page = KMGenericPage(page)
+
+ logger.info("Step 1: Open KM Generic URL")
+ km_page.open_url()
+
+ logger.info("Step 2: Changes the value of user filter")
+ km_page.update_filters()
+
+ logger.info("Step 3: Click Apply")
+ km_page.click_apply_button()
+
+ logger.info("Step 4: Validate filter data is reflecting in charts/graphs")
+ performance_issue_data = km_page.validate_trending_topics_entry("Laptop Performance Issues")
+ logger.info(f"Laptop performance issues data validated: {performance_issue_data}")
+
+ km_page.validate_dashboard_charts()
+
+
+def test_hide_dashboard_and_chat_buttons(login_logout, request):
+ """
+ KM Generic Smoke Test - ITHelpdesk:
+ 1. Open KM Generic URL
+ 2. Changes the value of user filter
+ 3. Notice the value/data change in the chart/graphs tables
+ """
+
+ # Set custom test name for pytest HTML report
+ request.node._nodeid = "14485 - KM Generic - ITHelpdesk - Validate Hide Dashboard and Hide Chat buttons"
+
+ page = login_logout
+ km_page = KMGenericPage(page)
+
+ logger.info("Step 1: Open KM Generic URL")
+ km_page.open_url()
+
+ logger.info("Step 2: On the left side of profile icon observe two buttons are present, Hide Dashboard & Hide Chat")
+ km_page.verify_hide_dashboard_and_chat_buttons()
+
+
+def test_refine_chat_chart_output(login_logout, request):
+ """
+ KM Generic Smoke Test - ITHelpdesk:
+ 1. Open KM Generic URL
+ 2. On chat window enter the prompt which provides chat info: EX: Average handling time by topic
+ 3. On chat window enter the prompt which provides chat info: EX: Generate Chart
+ """
+
+ # Set custom test name for pytest HTML report
+ request.node._nodeid = "14526 - US_12962_KM Generic - ITHelpdesk - Improve Chart Generation Experience in Chat"
+
+ page = login_logout
+ km_page = KMGenericPage(page)
+ home_page = HomePage(page)
+
+ logger.info("Step 1: Open KM Generic URL")
+ km_page.open_url()
+
+ logger.info("Step 2: Verify chat response generation")
+ logger.info("Step 3: On chat window enter the prompt which provides chat info: EX: Average handling time by topic")
+ home_page.validate_chat_response('Average handling time by topic')
+ home_page.validate_response_status('Average handling time by topic')
+
+ logger.info("Step 4: On chat window enter the prompt which provides chat info: EX: Generate chart")
+ home_page.validate_chat_response('Generate chart', True)
+ home_page.validate_response_status('Generate chart')
+
+
+def test_chat_greeting_responses(login_logout, request):
+
+ """
+ KM Generic Smoke Test - ITHelpdesk:
+ 1. Deploy KM Generic
+ 2. Open KM Generic URL
+ 3. On chat window enter the Greeting related info: EX: Hi, Good morning, Hello.
+ """
+
+ # Set custom test name for pytest HTML report
+ request.node._nodeid = "21426 - US_20054_KM Generic - ITHelpdesk - Greeting related experience in Chat"
+
+ page = login_logout
+ km_page = KMGenericPage(page)
+ home_page = HomePage(page)
+
+ logger.info("Step 1: Open KM Generic URL")
+ km_page.open_url()
+
+ greetings = ["Hi, Good morning", "Hello"]
+ logger.info("Step 2: On chat window enter the Greeting related info: EX: Hi, Good morning, Hello.")
+ for greeting in greetings:
+ home_page.enter_chat_question(greeting)
+ home_page.click_send_button()
+
+ # Check last assistant message for a greeting-style reply
+ assistant_messages = home_page.page.locator("div.chat-message.assistant")
+ last_message = assistant_messages.last
+
+ # Validate greeting response
+ p = last_message.locator("p")
+ message_text = p.inner_text().lower()
+
+ if any(keyword in message_text for keyword in ["how can i assist", "how can i help", "hello again"]):
+ logger.info(f"Valid greeting response received for: {greeting}")
+ else:
+ raise AssertionError(f"Unexpected greeting response for '{greeting}': {message_text}")
+
+ # Optional wait between messages
+ home_page.page.wait_for_timeout(1000)
+
+
+def test_chat_history_panel(login_logout, request):
+ """
+ KM Generic Smoke Test - ITHelpdesk:
+ Refactored to reuse golden path logic plus additional chat history operations
+ 1. Reuse golden path test execution (load home page, delete history, execute questions)
+ 2. Edit chat thread title
+ 3. Verify chat history operations (delete thread, create new chat, clear all history)
+ """
+
+ # Set custom test name for pytest HTML report
+ request.node._nodeid = "14483 - KM Generic - ITHelpdesk - Validate Chat History- user able to edit, save, delete and delete all chat history"
+
+ page = login_logout
+ home_page = HomePage(page)
+ home_page.page = page
+
+ log_capture = io.StringIO()
+ handler = logging.StreamHandler(log_capture)
+ logger.addHandler(handler)
+
+ try:
+ # Reuse golden path logic - Steps 1-2: Load home page and clear chat history
+ logger.info("Step 1: Validate home page is loaded")
+ start = time.time()
+ home_page.home_page_load()
+ duration = time.time() - start
+ logger.info(f"Execution Time for 'Validate home page is loaded': {duration:.2f}s")
+
+ logger.info("Step 2: Validate delete chat history")
+ start = time.time()
+ home_page.delete_chat_history()
+ duration = time.time() - start
+ logger.info(f"Execution Time for 'Validate delete chat history': {duration:.2f}s")
+
+ # Reuse golden path logic - Execute all golden path questions
+ failed_questions = [] # Track failed questions for final reporting
+
+ for i, question in enumerate(ithelpdesk_questions, start=1):
+ logger.info(f"Step {i+2}: Validate response for GP Prompt: {question}")
+ start = time.time()
+
+ # Retry logic: attempt up to 2 times if response is invalid
+ max_retries = 2
+ question_passed = False
+
+ for attempt in range(max_retries):
+ try:
+ # Enter question and get response
+ home_page.enter_chat_question(question)
+ home_page.click_send_button()
+ home_page.page.wait_for_timeout(8000) # Wait before validating response status
+ home_page.validate_response_status(question)
+ home_page.page.wait_for_timeout(5000) # Wait after validating response status
+ home_page.validate_response_text(question)
+
+ # If we reach here, the response was valid - break out of retry loop
+ logger.info(f"[{question}] Valid response received on attempt {attempt + 1}")
+ question_passed = True
+ break
+
+ except Exception as e:
+ if attempt < max_retries - 1: # Not the last attempt
+ logger.warning(f"[{question}] Attempt {attempt + 1} failed: {str(e)}")
+ logger.info(f"[{question}] Retrying... (attempt {attempt + 2}/{max_retries})")
+ # Wait a bit before retrying
+ home_page.page.wait_for_timeout(10000)
+ else: # Last attempt failed
+ logger.error(f"[{question}] All {max_retries} attempts failed. Last error: {str(e)}")
+ failed_questions.append({"question": question, "error": str(e)})
+
+ # Only handle citations if the question passed
+ if question_passed and home_page.has_reference_link():
+ logger.info(f"[{question}] Reference link found. Opening citation.")
+ home_page.click_reference_link_in_response()
+ logger.info(f"[{question}] Closing citation.")
+ home_page.close_citation()
+
+ duration = time.time() - start
+ logger.info(f"Execution Time for 'Validate response for GP Prompt: {question}': {duration:.2f}s")
+
+ # Log summary of failed questions
+ if failed_questions:
+ logger.warning(f"Chat history test completed with {len(failed_questions)} failed questions out of {len(ithelpdesk_questions)} total")
+ for failed in failed_questions:
+ logger.error(f"Failed question: '{failed['question']}' - {failed['error']}")
+ else:
+ logger.info("All golden path questions passed successfully")
+
+ # Additional chat history specific operations
+ logger.info("Step 7: Try editing the title of chat thread")
+ home_page.edit_chat_title("Updated Title")
+
+ home_page.page.wait_for_timeout(2000)
+
+ logger.info("Step 8: Verify the chat history is getting stored properly or not")
+ logger.info("Step 9: Try deleting the chat thread from chat history panel")
+ home_page.delete_first_chat_thread()
+
+ home_page.page.wait_for_timeout(2000)
+
+ logger.info("Step 10: Try clicking on + icon present before chat box")
+ home_page.create_new_chat()
+
+ home_page.page.wait_for_timeout(2000)
+
+ home_page.close_chat_history()
+
+ logger.info("Step 11: Click on eclipse (3 dots) and select Clear all chat history")
+ home_page.delete_chat_history()
+
+ finally:
+ logger.removeHandler(handler)
+
+
+def test_clear_citations_on_chat_delete(login_logout, request):
+ """
+ KM Generic Smoke Test - ITHelpdesk:
+ 1. Open KM Generic URL
+ 2. Ask questions in the chat area, where the citations are provided.
+ 3. Click on the any citation link.
+ 4. Open Chat history panel.
+ 5. In chat history panel delete complete chat history.
+ 6. Observe Citation Section.
+ """
+
+ # Set custom test name for pytest HTML report
+ request.node._nodeid = "18631 - Bug 17326 - KM Generic - ITHelpdesk - Citation should get cleared after deleting complete chat history"
+
+ page = login_logout
+ home_page = HomePage(page)
+
+ logger.info("Step 2: Send a query to trigger a citation")
+ question= "Provide a summary of performance issues users reported this week"
+ home_page.enter_chat_question(question)
+ home_page.click_send_button()
+ # home_page.validate_chat_response(question)
+ home_page.page.wait_for_timeout(3000)
+
+ logger.info("Step 3: Validate citation link appears in response")
+ logger.info("Step 4: Click on the citation link to open the panel")
+ home_page.click_reference_link_in_response()
+ home_page.page.wait_for_timeout(5000)
+
+ # 6. Delete entire chat history
+ home_page.delete_chat_history()
+
+ # 7. Check citation section is not visible after chat history deletion
+ citations_locator = page.locator("//div[contains(text(),'Citations')]")
+ expect(citations_locator).not_to_be_visible(timeout=3000)
+ logger.info("Citations section is not visible after chat history deletion")
+
+
+def test_citation_panel_closes_with_chat(login_logout, request):
+ """
+ Test to ensure citation panel closes when chat section is hidden.
+ """
+
+ # Set custom test name for pytest HTML report
+ request.node._nodeid = "19433 - KM Generic - ITHelpdesk - Citation panel should close after hiding chat"
+
+ page = login_logout
+ km_page = KMGenericPage(page)
+ home_page = HomePage(page)
+
+ logger.info("Step 1: Navigate to KM Generic URL")
+ home_page.page.reload(wait_until="networkidle")
+ home_page.page.wait_for_timeout(2000)
+
+ logger.info("Step 2: Send a query to trigger a citation")
+ question= "Provide a summary of performance issues users reported this week"
+ home_page.enter_chat_question(question)
+ home_page.click_send_button()
+ # home_page.validate_chat_response(question)
+ home_page.page.wait_for_timeout(3000)
+
+ logger.info("Step 3: Validate citation link appears in response")
+ logger.info("Step 4: Click on the citation link to open the panel")
+ home_page.click_reference_link_in_response()
+ home_page.page.wait_for_timeout(3000)
+
+ logger.info("Step 5: Click on 'Hide Chat' button")
+ km_page.verify_hide_dashboard_and_chat_buttons()
+ home_page.page.wait_for_timeout(3000)
+
+ logger.info("Step 6: Verify citation panel is closed after hiding chat")
+ citation_panel = km_page.page.locator("div.citationPanel")
+ expect(citation_panel).not_to_be_visible(timeout=3000)
+
+ logger.info("✅ Citation panel successfully closed with chat.")
diff --git a/tests/e2e-test/tests/test_telecom_smoke_tc.py b/tests/e2e-test/tests/test_telecom_smoke_tc.py
index 44a4f11e8..b5aee1a3c 100644
--- a/tests/e2e-test/tests/test_telecom_smoke_tc.py
+++ b/tests/e2e-test/tests/test_telecom_smoke_tc.py
@@ -73,7 +73,7 @@ def test_after_filter_functioning(login_logout, request):
km_page.click_apply_button()
logger.info("Step 4: Validate filter data is reflecting in charts/graphs")
- billing_data = km_page.validate_trending_topics_entry("Billing Issues")
+ km_page.validate_trending_topics_entry("Billing Issues")
logger.info("Billing issues data validation completed")
km_page.validate_dashboard_charts()
@@ -299,7 +299,6 @@ def test_clear_citations_on_chat_delete(login_logout, request):
request.node._nodeid = "17631 - Bug 17326 - KM Generic - Telecom - Citation should get cleared after deleting complete chat history"
page = login_logout
- km_page = KMGenericPage(page)
home_page = HomePage(page)
logger.info("Step 2: Send a query to trigger a citation")