diff --git a/.github/workflows/deploy-v2.yml b/.github/workflows/deploy-v2.yml index 57253a073..40ad505ce 100644 --- a/.github/workflows/deploy-v2.yml +++ b/.github/workflows/deploy-v2.yml @@ -11,7 +11,7 @@ on: - completed branches: - main - - dev-v4 + - dev - hotfix workflow_dispatch: inputs: diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3ebb794d9..8b2d7aba2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -11,7 +11,7 @@ on: - completed branches: - main - - dev-v4 + - dev - hotfix schedule: - cron: "0 11,23 * * *" # Runs at 11:00 AM and 11:00 PM GMT @@ -129,13 +129,13 @@ jobs: id: deploy run: | if [[ "${{ env.BRANCH_NAME }}" == "main" ]]; then - IMAGE_TAG="latest_v4" - elif [[ "${{ env.BRANCH_NAME }}" == "dev-v4" ]]; then - IMAGE_TAG="dev_v4" + IMAGE_TAG="latest_v5" + elif [[ "${{ env.BRANCH_NAME }}" == "dev" ]]; then + IMAGE_TAG="dev_v5" elif [[ "${{ env.BRANCH_NAME }}" == "hotfix" ]]; then IMAGE_TAG="hotfix" else - IMAGE_TAG="latest_v4" + IMAGE_TAG="latest_v5" fi # Generate current timestamp in desired format: YYYY-MM-DDTHH:MM:SS.SSSSSSSZ diff --git a/.github/workflows/docker-build-and-push.yml b/.github/workflows/docker-build-and-push.yml index 6e247cd5a..9872e4622 100644 --- a/.github/workflows/docker-build-and-push.yml +++ b/.github/workflows/docker-build-and-push.yml @@ -4,7 +4,7 @@ on: push: branches: - main - - dev-v4 + - dev - demo-v4 - hotfix paths: @@ -86,11 +86,11 @@ jobs: id: determine_tag run: | if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then - echo "TAG=latest_v4" >> $GITHUB_ENV - elif [[ "${{ github.ref }}" == "refs/heads/dev-v4" ]]; then - echo "TAG=dev_v4" >> $GITHUB_ENV + echo "TAG=latest_v5" >> $GITHUB_ENV + elif [[ "${{ github.ref }}" == "refs/heads/dev" ]]; then + echo "TAG=dev_v5" >> $GITHUB_ENV elif [[ "${{ github.ref }}" == "refs/heads/demo-v4" ]]; then - echo "TAG=demo_v4" >> $GITHUB_ENV + echo "TAG=demo_v5" >> $GITHUB_ENV elif [[ "${{ github.ref }}" == "refs/heads/hotfix" ]]; then echo "TAG=hotfix" >> $GITHUB_ENV else diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index 0c8a05ce7..bde88eac5 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -431,13 +431,13 @@ jobs: echo "Current branch: $BRANCH_NAME" if [[ "$BRANCH_NAME" == "main" ]]; then - IMAGE_TAG="latest_v4" - elif [[ "$BRANCH_NAME" == "dev-v4" ]]; then - IMAGE_TAG="dev_v4" + IMAGE_TAG="latest_v5" + elif [[ "$BRANCH_NAME" == "dev" ]]; then + IMAGE_TAG="dev_v5" elif [[ "$BRANCH_NAME" == "hotfix" ]]; then IMAGE_TAG="hotfix" else - IMAGE_TAG="latest_v4" + IMAGE_TAG="latest_v5" fi echo "Using existing Docker image tag: $IMAGE_TAG" fi diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8044ba8d0..c4b21b382 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,7 +5,7 @@ on: branches: - main - demo-v4 - - dev-v4 + - dev paths: - 'src/backend/**/*.py' - 'src/tests/**/*.py' @@ -24,7 +24,7 @@ on: branches: - main - demo-v4 - - dev-v4 + - dev paths: - 'src/backend/**/*.py' - 'src/tests/**/*.py' diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md index a9d95ef95..99212e998 100644 --- a/docs/CustomizingAzdParameters.md +++ b/docs/CustomizingAzdParameters.md @@ -23,7 +23,7 @@ By default this template will use the environment name as the prefix to prevent | `AZURE_ENV_REASONING_MODEL_NAME` | string | `o4-mini` | Specifies the name of the reasoning GPT model to be deployed. | | `AZURE_ENV_REASONING_MODEL_VERSION` | string | `2025-04-16` | Version of the reasoning GPT model to be used for deployment. | | `AZURE_ENV_REASONING_MODEL_CAPACITY` | int | `50` | Sets the reasoning GPT model capacity. | -| `AZURE_ENV_IMAGE_TAG` | string | `latest_v4` | Docker image tag used for container deployments. | +| `AZURE_ENV_IMAGE_TAG` | string | `latest_v5` | Docker image tag used for container deployments. | | `AZURE_ENV_ENABLE_TELEMETRY` | bool | `true` | Enables telemetry for monitoring and diagnostics. | | `AZURE_EXISTING_AIPROJECT_RESOURCE_ID` | string | `` | Set this if you want to reuse an AI Foundry Project instead of creating a new one. | | `AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID` | string | Guide to get your [Existing Workspace ID](re-use-log-analytics.md) | Set this if you want to reuse an existing Log Analytics Workspace instead of creating a new one. | diff --git a/infra/avm/main.bicep b/infra/avm/main.bicep index 2e0db602d..2265086ca 100644 --- a/infra/avm/main.bicep +++ b/infra/avm/main.bicep @@ -153,7 +153,7 @@ param backendContainerRegistryHostname string = 'biabcontainerreg.azurecr.io' param backendContainerImageName string = 'macaebackend' @description('Optional. The Container Image Tag to deploy on the backend.') -param backendContainerImageTag string = 'latest_v4' +param backendContainerImageTag string = 'latest_v5' @description('Optional. The Container Registry hostname where the docker images for the frontend are located.') param frontendContainerRegistryHostname string = 'biabcontainerreg.azurecr.io' @@ -162,7 +162,7 @@ param frontendContainerRegistryHostname string = 'biabcontainerreg.azurecr.io' param frontendContainerImageName string = 'macaefrontend' @description('Optional. The Container Image Tag to deploy on the frontend.') -param frontendContainerImageTag string = 'latest_v4' +param frontendContainerImageTag string = 'latest_v5' @description('Optional. The Container Registry hostname where the docker images for the MCP are located.') param MCPContainerRegistryHostname string = 'biabcontainerreg.azurecr.io' @@ -171,7 +171,7 @@ param MCPContainerRegistryHostname string = 'biabcontainerreg.azurecr.io' param MCPContainerImageName string = 'macaemcp' @description('Optional. The Container Image Tag to deploy on the MCP.') -param MCPContainerImageTag string = 'latest_v4' +param MCPContainerImageTag string = 'latest_v5' // ============================================================================ // Parameters — Feature Flags / WAF diff --git a/infra/avm/main.json b/infra/avm/main.json index 5c257d5b0..6e447fa51 100644 --- a/infra/avm/main.json +++ b/infra/avm/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "14106208564257644349" + "templateHash": "8714866940320286579" }, "name": "Multi-Agent Custom Automation Engine - AVM", "description": "AVM orchestrator for the Multi-Agent Custom Automation Engine accelerator. Deploys the same logical resources and preserves the same outputs as infra\\main.bicep using local AVM wrapper modules." @@ -256,7 +256,7 @@ }, "backendContainerImageTag": { "type": "string", - "defaultValue": "latest_v4", + "defaultValue": "latest_v5", "metadata": { "description": "Optional. The Container Image Tag to deploy on the backend." } @@ -277,7 +277,7 @@ }, "frontendContainerImageTag": { "type": "string", - "defaultValue": "latest_v4", + "defaultValue": "latest_v5", "metadata": { "description": "Optional. The Container Image Tag to deploy on the frontend." } @@ -298,7 +298,7 @@ }, "MCPContainerImageTag": { "type": "string", - "defaultValue": "latest_v4", + "defaultValue": "latest_v5", "metadata": { "description": "Optional. The Container Image Tag to deploy on the MCP." } @@ -71145,7 +71145,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "8435839870481505832" + "templateHash": "16605313702382917585" } }, "parameters": { @@ -71188,7 +71188,14 @@ "type": "string", "defaultValue": "", "metadata": { - "description": "Principal ID of the user-assigned managed identity (empty if not deployed)." + "description": "Principal ID of the user-assigned managed identity (empty if not deployed). Kept for backward compatibility; prefer workloadPrincipalIds." + } + }, + "workloadPrincipalIds": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Optional. List of workload identity principal IDs (e.g. system-assigned identities of backend, MCP, and frontend hosts) that should receive the same data-plane roles previously granted to the UAMI. When non-empty, this list takes precedence over userAssignedManagedIdentityPrincipalId." } }, "deployerPrincipalId": { @@ -71231,6 +71238,7 @@ "existingAIFoundryName": "[if(parameters('useExistingAIProject'), split(parameters('existingFoundryProjectResourceId'), '/')[8], '')]", "existingAIFoundrySubscription": "[if(parameters('useExistingAIProject'), split(parameters('existingFoundryProjectResourceId'), '/')[2], subscription().subscriptionId)]", "existingAIFoundryResourceGroup": "[if(parameters('useExistingAIProject'), split(parameters('existingFoundryProjectResourceId'), '/')[4], resourceGroup().name)]", + "workloadPrincipals": "[if(not(empty(parameters('workloadPrincipalIds'))), parameters('workloadPrincipalIds'), if(empty(parameters('userAssignedManagedIdentityPrincipalId')), createArray(), createArray(parameters('userAssignedManagedIdentityPrincipalId'))))]", "roleDefinitions": { "azureAiUser": "53ca6127-db72-4b80-b1b0-d745d6d5456d", "cognitiveServicesUser": "a97b65f3-24c7-4388-baec-2e87135dc908", @@ -71257,84 +71265,48 @@ } }, { - "condition": "[and(and(not(parameters('useExistingAIProject')), not(empty(parameters('aiFoundryResourceId')))), not(empty(parameters('userAssignedManagedIdentityPrincipalId'))))]", + "copy": { + "name": "workloadAiUserAssignment", + "count": "[length(variables('workloadPrincipals'))]" + }, + "condition": "[and(and(not(parameters('useExistingAIProject')), not(empty(parameters('aiFoundryResourceId')))), not(empty(variables('workloadPrincipals')[copyIndex()])))]", "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", "scope": "[resourceId('Microsoft.CognitiveServices/accounts', last(split(parameters('aiFoundryResourceId'), '/')))]", - "name": "[guid(parameters('solutionName'), resourceId('Microsoft.CognitiveServices/accounts', last(split(parameters('aiFoundryResourceId'), '/'))), parameters('userAssignedManagedIdentityPrincipalId'), variables('roleDefinitions').azureAiUser)]", + "name": "[guid(parameters('solutionName'), resourceId('Microsoft.CognitiveServices/accounts', last(split(parameters('aiFoundryResourceId'), '/'))), variables('workloadPrincipals')[copyIndex()], variables('roleDefinitions').azureAiUser)]", "properties": { - "principalId": "[parameters('userAssignedManagedIdentityPrincipalId')]", + "principalId": "[variables('workloadPrincipals')[copyIndex()]]", "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('roleDefinitions').azureAiUser)]", "principalType": "ServicePrincipal" } }, { - "condition": "[and(and(not(parameters('useExistingAIProject')), not(empty(parameters('aiFoundryResourceId')))), not(empty(parameters('userAssignedManagedIdentityPrincipalId'))))]", + "copy": { + "name": "workloadOpenAIContributor", + "count": "[length(variables('workloadPrincipals'))]" + }, + "condition": "[and(and(not(parameters('useExistingAIProject')), not(empty(parameters('aiFoundryResourceId')))), not(empty(variables('workloadPrincipals')[copyIndex()])))]", "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", "scope": "[resourceId('Microsoft.CognitiveServices/accounts', last(split(parameters('aiFoundryResourceId'), '/')))]", - "name": "[guid(parameters('solutionName'), resourceId('Microsoft.CognitiveServices/accounts', last(split(parameters('aiFoundryResourceId'), '/'))), parameters('userAssignedManagedIdentityPrincipalId'), variables('roleDefinitions').cognitiveServicesOpenAIContributor)]", + "name": "[guid(parameters('solutionName'), resourceId('Microsoft.CognitiveServices/accounts', last(split(parameters('aiFoundryResourceId'), '/'))), variables('workloadPrincipals')[copyIndex()], variables('roleDefinitions').cognitiveServicesOpenAIContributor)]", "properties": { - "principalId": "[parameters('userAssignedManagedIdentityPrincipalId')]", + "principalId": "[variables('workloadPrincipals')[copyIndex()]]", "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('roleDefinitions').cognitiveServicesOpenAIContributor)]", "principalType": "ServicePrincipal" } }, { - "condition": "[and(not(empty(parameters('aiSearchResourceId'))), not(empty(parameters('userAssignedManagedIdentityPrincipalId'))))]", - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[resourceId('Microsoft.Search/searchServices', last(split(parameters('aiSearchResourceId'), '/')))]", - "name": "[guid(parameters('solutionName'), parameters('aiSearchResourceId'), parameters('userAssignedManagedIdentityPrincipalId'), variables('roleDefinitions').searchIndexDataReader)]", - "properties": { - "principalId": "[parameters('userAssignedManagedIdentityPrincipalId')]", - "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('roleDefinitions').searchIndexDataReader)]", - "principalType": "ServicePrincipal" - } - }, - { - "condition": "[and(not(empty(parameters('storageAccountResourceId'))), not(empty(parameters('aiProjectPrincipalId'))))]", - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[resourceId('Microsoft.Storage/storageAccounts', last(split(parameters('storageAccountResourceId'), '/')))]", - "name": "[guid(parameters('solutionName'), parameters('storageAccountResourceId'), parameters('aiProjectPrincipalId'), variables('roleDefinitions').storageBlobDataContributor)]", - "properties": { - "principalId": "[parameters('aiProjectPrincipalId')]", - "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('roleDefinitions').storageBlobDataContributor)]", - "principalType": "ServicePrincipal" - } - }, - { - "condition": "[and(not(empty(parameters('storageAccountResourceId'))), not(empty(parameters('aiProjectPrincipalId'))))]", - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[resourceId('Microsoft.Storage/storageAccounts', last(split(parameters('storageAccountResourceId'), '/')))]", - "name": "[guid(parameters('solutionName'), parameters('storageAccountResourceId'), parameters('aiProjectPrincipalId'), variables('roleDefinitions').storageBlobDataReader)]", - "properties": { - "principalId": "[parameters('aiProjectPrincipalId')]", - "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('roleDefinitions').storageBlobDataReader)]", - "principalType": "ServicePrincipal" - } - }, - { - "condition": "[and(not(empty(parameters('storageAccountResourceId'))), not(empty(parameters('aiSearchPrincipalId'))))]", - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[resourceId('Microsoft.Storage/storageAccounts', last(split(parameters('storageAccountResourceId'), '/')))]", - "name": "[guid(parameters('solutionName'), parameters('storageAccountResourceId'), parameters('aiSearchPrincipalId'), variables('roleDefinitions').storageBlobDataReader)]", - "properties": { - "principalId": "[parameters('aiSearchPrincipalId')]", - "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('roleDefinitions').storageBlobDataReader)]", - "principalType": "ServicePrincipal" - } - }, - { - "condition": "[and(not(empty(parameters('cosmosDbAccountName'))), not(empty(parameters('userAssignedManagedIdentityPrincipalId'))))]", + "copy": { + "name": "workloadCosmosRoleAssignment", + "count": "[length(variables('workloadPrincipals'))]" + }, + "condition": "[and(not(empty(parameters('cosmosDbAccountName'))), not(empty(variables('workloadPrincipals')[copyIndex()])))]", "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments", "apiVersion": "2025-10-15", - "name": "[format('{0}/{1}', parameters('cosmosDbAccountName'), guid(parameters('solutionName'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbAccountName')), parameters('userAssignedManagedIdentityPrincipalId'), resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', parameters('cosmosDbAccountName'), '00000000-0000-0000-0000-000000000002')))]", + "name": "[format('{0}/{1}', parameters('cosmosDbAccountName'), guid(parameters('solutionName'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbAccountName')), variables('workloadPrincipals')[copyIndex()], resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', parameters('cosmosDbAccountName'), '00000000-0000-0000-0000-000000000002')))]", "properties": { - "principalId": "[parameters('userAssignedManagedIdentityPrincipalId')]", + "principalId": "[variables('workloadPrincipals')[copyIndex()]]", "roleDefinitionId": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', parameters('cosmosDbAccountName'), '00000000-0000-0000-0000-000000000002')]", "scope": "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbAccountName'))]" } @@ -71464,10 +71436,14 @@ } }, { - "condition": "[and(parameters('useExistingAIProject'), not(empty(parameters('userAssignedManagedIdentityPrincipalId'))))]", + "copy": { + "name": "workloadAiUserExisting", + "count": "[length(variables('workloadPrincipals'))]" + }, + "condition": "[and(parameters('useExistingAIProject'), not(empty(variables('workloadPrincipals')[copyIndex()])))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "assignAiUserRoleToBackendExisting", + "name": "[format('assignAiUserRoleToWorkloadExisting-{0}', copyIndex())]", "subscriptionId": "[variables('existingAIFoundrySubscription')]", "resourceGroup": "[variables('existingAIFoundryResourceGroup')]", "properties": { @@ -71477,13 +71453,13 @@ "mode": "Incremental", "parameters": { "principalId": { - "value": "[parameters('userAssignedManagedIdentityPrincipalId')]" + "value": "[variables('workloadPrincipals')[copyIndex()]]" }, "roleDefinitionId": { "value": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('roleDefinitions').azureAiUser)]" }, "roleAssignmentName": { - "value": "[guid(parameters('solutionName'), variables('existingAIFoundryName'), parameters('userAssignedManagedIdentityPrincipalId'), variables('roleDefinitions').azureAiUser)]" + "value": "[guid(parameters('solutionName'), variables('existingAIFoundryName'), variables('workloadPrincipals')[copyIndex()], variables('roleDefinitions').azureAiUser)]" }, "aiFoundryName": { "value": "[variables('existingAIFoundryName')]" @@ -71553,10 +71529,14 @@ } }, { - "condition": "[and(parameters('useExistingAIProject'), not(empty(parameters('userAssignedManagedIdentityPrincipalId'))))]", + "copy": { + "name": "workloadOpenAIContributorExisting", + "count": "[length(variables('workloadPrincipals'))]" + }, + "condition": "[and(parameters('useExistingAIProject'), not(empty(variables('workloadPrincipals')[copyIndex()])))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "assignOpenAIContributorRoleToBackendExisting", + "name": "[format('assignOpenAIContributorRoleToWorkloadExisting-{0}', copyIndex())]", "subscriptionId": "[variables('existingAIFoundrySubscription')]", "resourceGroup": "[variables('existingAIFoundryResourceGroup')]", "properties": { @@ -71566,13 +71546,13 @@ "mode": "Incremental", "parameters": { "principalId": { - "value": "[parameters('userAssignedManagedIdentityPrincipalId')]" + "value": "[variables('workloadPrincipals')[copyIndex()]]" }, "roleDefinitionId": { "value": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('roleDefinitions').cognitiveServicesOpenAIContributor)]" }, "roleAssignmentName": { - "value": "[guid(parameters('solutionName'), variables('existingAIFoundryName'), parameters('userAssignedManagedIdentityPrincipalId'), variables('roleDefinitions').cognitiveServicesOpenAIContributor)]" + "value": "[guid(parameters('solutionName'), variables('existingAIFoundryName'), variables('workloadPrincipals')[copyIndex()], variables('roleDefinitions').cognitiveServicesOpenAIContributor)]" }, "aiFoundryName": { "value": "[variables('existingAIFoundryName')]" @@ -71671,186 +71651,324 @@ }, "AZURE_STORAGE_BLOB_URL": { "type": "string", + "metadata": { + "description": "The blob service endpoint of the deployed storage account." + }, "value": "[reference('storage_account').outputs.serviceEndpoints.value.blob]" }, "AZURE_STORAGE_ACCOUNT_NAME": { "type": "string", + "metadata": { + "description": "The name of the deployed storage account used for content pack datasets and runtime artifacts." + }, "value": "[variables('storageAccountName')]" }, "AZURE_AI_SEARCH_ENDPOINT": { "type": "string", + "metadata": { + "description": "The endpoint URL of the deployed Azure AI Search service." + }, "value": "[reference('ai_search').outputs.endpoint.value]" }, "AZURE_AI_SEARCH_NAME": { "type": "string", + "metadata": { + "description": "The name of the deployed Azure AI Search service." + }, "value": "[reference('ai_search').outputs.name.value]" }, "COSMOSDB_ENDPOINT": { "type": "string", + "metadata": { + "description": "The document endpoint of the deployed Cosmos DB account used for agent memory and session state." + }, "value": "[reference('cosmosDBModule').outputs.endpoint.value]" }, "COSMOSDB_DATABASE": { "type": "string", + "metadata": { + "description": "The name of the Cosmos DB SQL database used by the backend." + }, "value": "[variables('cosmosDbDatabaseName')]" }, "COSMOSDB_CONTAINER": { "type": "string", + "metadata": { + "description": "The name of the Cosmos DB container used to persist agent memory." + }, "value": "[variables('cosmosDbDatabaseMemoryContainerName')]" }, "AZURE_OPENAI_ENDPOINT": { "type": "string", + "metadata": { + "description": "The Azure OpenAI endpoint exposed by the AI Foundry account." + }, "value": "[variables('aiFoundryOpenAIEndpoint')]" }, "AZURE_OPENAI_DEPLOYMENT_NAME": { "type": "string", + "metadata": { + "description": "The default GPT chat-completion deployment name used by the backend." + }, "value": "[parameters('gptModelName')]" }, "AZURE_OPENAI_RAI_DEPLOYMENT_NAME": { "type": "string", + "metadata": { + "description": "The deployment name of the GPT-4.1 model used for Responsible AI / higher-quality completions." + }, "value": "[parameters('gpt4_1ModelName')]" }, "AZURE_OPENAI_API_VERSION": { "type": "string", + "metadata": { + "description": "The Azure OpenAI REST API version used by the backend SDK clients." + }, "value": "[parameters('azureOpenaiAPIVersion')]" }, "AZURE_AI_SUBSCRIPTION_ID": { "type": "string", + "metadata": { + "description": "The subscription ID hosting the AI Foundry / AI Services resource." + }, "value": "[subscription().subscriptionId]" }, "AZURE_AI_RESOURCE_GROUP": { "type": "string", + "metadata": { + "description": "The resource group hosting the AI Foundry / AI Services resource." + }, "value": "[resourceGroup().name]" }, "AZURE_AI_PROJECT_NAME": { "type": "string", + "metadata": { + "description": "The name of the Azure AI Foundry project used by the backend." + }, "value": "[if(variables('useExistingAIProject'), reference('existing_project_setup').outputs.projectName.value, reference('ai_foundry_project').outputs.projectName.value)]" }, "AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME": { "type": "string", + "metadata": { + "description": "The model deployment name used by the AI Foundry agent runtime." + }, "value": "[parameters('gptModelName')]" }, "APP_ENV": { "type": "string", + "metadata": { + "description": "The application environment label propagated to runtime container settings." + }, "value": "Prod" }, "AI_FOUNDRY_RESOURCE_ID": { "type": "string", + "metadata": { + "description": "The resource ID of the AI Foundry (AI Services) account backing this deployment." + }, "value": "[if(variables('useExistingAIProject'), reference('existing_project_setup').outputs.resourceId.value, reference('ai_foundry_project').outputs.resourceId.value)]" }, "COSMOSDB_ACCOUNT_NAME": { "type": "string", + "metadata": { + "description": "The name of the deployed Cosmos DB account." + }, "value": "[variables('cosmosDbResourceName')]" }, "AZURE_SEARCH_ENDPOINT": { "type": "string", + "metadata": { + "description": "Alias for AZURE_AI_SEARCH_ENDPOINT — kept for backward compatibility with seed scripts and the backend." + }, "value": "[reference('ai_search').outputs.endpoint.value]" }, "AZURE_CLIENT_ID": { "type": "string", + "metadata": { + "description": "The client ID of the user-assigned managed identity used by backend, MCP, and frontend workloads." + }, "value": "[reference('managed_identity').outputs.clientId.value]" }, "AZURE_TENANT_ID": { "type": "string", + "metadata": { + "description": "The Microsoft Entra ID tenant ID used for token acquisition by all workloads." + }, "value": "[tenant().tenantId]" }, "AZURE_COGNITIVE_SERVICES": { "type": "string", + "metadata": { + "description": "The default scope used when requesting tokens for Azure Cognitive Services / AI Services." + }, "value": "https://cognitiveservices.azure.com/.default" }, "ORCHESTRATOR_MODEL_NAME": { "type": "string", + "metadata": { + "description": "The deployment name of the reasoning model used by the orchestrator/manager agent." + }, "value": "[parameters('gptReasoningModelName')]" }, "MCP_SERVER_NAME": { "type": "string", + "metadata": { + "description": "The configured name of the MCP server exposed by the deployment." + }, "value": "MacaeMcpServer" }, "MCP_SERVER_DESCRIPTION": { "type": "string", + "metadata": { + "description": "The human-readable description of the MCP server exposed by the deployment." + }, "value": "MCP server with greeting, HR, and planning tools" }, "SUPPORTED_MODELS": { "type": "string", + "metadata": { + "description": "JSON-serialized list of model deployment names supported by this deployment." + }, "value": "[string(variables('supportedModels'))]" }, "BACKEND_URL": { "type": "string", + "metadata": { + "description": "The base URL of the backend Container App (used by the frontend reverse proxy)." + }, "value": "[format('https://{0}', reference('containerApp').outputs.fqdn.value)]" }, "AZURE_AI_PROJECT_ENDPOINT": { "type": "string", + "metadata": { + "description": "The endpoint of the AI Foundry project used by backend SDK clients." + }, "value": "[if(variables('useExistingAIProject'), reference('existing_project_setup').outputs.projectEndpoint.value, reference('ai_foundry_project').outputs.projectEndpoint.value)]" }, "AZURE_AI_AGENT_ENDPOINT": { "type": "string", + "metadata": { + "description": "The endpoint used by the AI Foundry agent runtime — same value as the project endpoint." + }, "value": "[if(variables('useExistingAIProject'), reference('existing_project_setup').outputs.projectEndpoint.value, reference('ai_foundry_project').outputs.projectEndpoint.value)]" }, "AI_SERVICE_NAME": { "type": "string", + "metadata": { + "description": "The name of the AI Foundry / AI Services account resource." + }, "value": "[variables('aiFoundryAiServicesResourceName')]" }, "AZURE_STORAGE_CONTAINER_NAME_RETAIL_CUSTOMER": { "type": "string", + "metadata": { + "description": "Blob container name used to upload the retail customer dataset." + }, "value": "[parameters('storageContainerNameRetailCustomer')]" }, "AZURE_STORAGE_CONTAINER_NAME_RETAIL_ORDER": { "type": "string", + "metadata": { + "description": "Blob container name used to upload the retail order dataset." + }, "value": "[parameters('storageContainerNameRetailOrder')]" }, "AZURE_STORAGE_CONTAINER_NAME_RFP_SUMMARY": { "type": "string", + "metadata": { + "description": "Blob container name used to upload the RFP summary dataset." + }, "value": "[parameters('storageContainerNameRFPSummary')]" }, "AZURE_STORAGE_CONTAINER_NAME_RFP_RISK": { "type": "string", + "metadata": { + "description": "Blob container name used to upload the RFP risk dataset." + }, "value": "[parameters('storageContainerNameRFPRisk')]" }, "AZURE_STORAGE_CONTAINER_NAME_RFP_COMPLIANCE": { "type": "string", + "metadata": { + "description": "Blob container name used to upload the RFP compliance dataset." + }, "value": "[parameters('storageContainerNameRFPCompliance')]" }, "AZURE_STORAGE_CONTAINER_NAME_CONTRACT_SUMMARY": { "type": "string", + "metadata": { + "description": "Blob container name used to upload the contract summary dataset." + }, "value": "[parameters('storageContainerNameContractSummary')]" }, "AZURE_STORAGE_CONTAINER_NAME_CONTRACT_RISK": { "type": "string", + "metadata": { + "description": "Blob container name used to upload the contract risk dataset." + }, "value": "[parameters('storageContainerNameContractRisk')]" }, "AZURE_STORAGE_CONTAINER_NAME_CONTRACT_COMPLIANCE": { "type": "string", + "metadata": { + "description": "Blob container name used to upload the contract compliance dataset." + }, "value": "[parameters('storageContainerNameContractCompliance')]" }, "AZURE_AI_SEARCH_INDEX_NAME_RETAIL_CUSTOMER": { "type": "string", + "metadata": { + "description": "AI Search index name used by the retail customer knowledge base." + }, "value": "[variables('aiSearchIndexNameForRetailCustomer')]" }, "AZURE_AI_SEARCH_INDEX_NAME_RETAIL_ORDER": { "type": "string", + "metadata": { + "description": "AI Search index name used by the retail order knowledge base." + }, "value": "[variables('aiSearchIndexNameForRetailOrder')]" }, "AZURE_AI_SEARCH_INDEX_NAME_RFP_SUMMARY": { "type": "string", + "metadata": { + "description": "AI Search index name used by the RFP summary knowledge base." + }, "value": "[variables('aiSearchIndexNameForRFPSummary')]" }, "AZURE_AI_SEARCH_INDEX_NAME_RFP_RISK": { "type": "string", + "metadata": { + "description": "AI Search index name used by the RFP risk knowledge base." + }, "value": "[variables('aiSearchIndexNameForRFPRisk')]" }, "AZURE_AI_SEARCH_INDEX_NAME_RFP_COMPLIANCE": { "type": "string", + "metadata": { + "description": "AI Search index name used by the RFP compliance knowledge base." + }, "value": "[variables('aiSearchIndexNameForRFPCompliance')]" }, "AZURE_AI_SEARCH_INDEX_NAME_CONTRACT_SUMMARY": { "type": "string", + "metadata": { + "description": "AI Search index name used by the contract summary knowledge base." + }, "value": "[variables('aiSearchIndexNameForContractSummary')]" }, "AZURE_AI_SEARCH_INDEX_NAME_CONTRACT_RISK": { "type": "string", + "metadata": { + "description": "AI Search index name used by the contract risk knowledge base." + }, "value": "[variables('aiSearchIndexNameForContractRisk')]" }, "AZURE_AI_SEARCH_INDEX_NAME_CONTRACT_COMPLIANCE": { "type": "string", + "metadata": { + "description": "AI Search index name used by the contract compliance knowledge base." + }, "value": "[variables('aiSearchIndexNameForContractCompliance')]" } } diff --git a/infra/bicep/main.bicep b/infra/bicep/main.bicep index 55c7e1a87..ef613a6f9 100644 --- a/infra/bicep/main.bicep +++ b/infra/bicep/main.bicep @@ -124,7 +124,7 @@ param backendContainerRegistryHostname string = 'biabcontainerreg.azurecr.io' param backendContainerImageName string = 'macaebackend' @description('Optional. The Container Image Tag to deploy on the backend.') -param backendContainerImageTag string = 'latest_v4' +param backendContainerImageTag string = 'latest_v5' @description('Optional. The Container Registry hostname where the docker images for the frontend are located.') param frontendContainerRegistryHostname string = 'biabcontainerreg.azurecr.io' @@ -133,7 +133,7 @@ param frontendContainerRegistryHostname string = 'biabcontainerreg.azurecr.io' param frontendContainerImageName string = 'macaefrontend' @description('Optional. The Container Image Tag to deploy on the frontend.') -param frontendContainerImageTag string = 'latest_v4' +param frontendContainerImageTag string = 'latest_v5' @description('Optional. The Container Registry hostname where the docker images for the MCP are located.') param MCPContainerRegistryHostname string = 'biabcontainerreg.azurecr.io' @@ -142,7 +142,7 @@ param MCPContainerRegistryHostname string = 'biabcontainerreg.azurecr.io' param MCPContainerImageName string = 'macaemcp' @description('Optional. The Container Image Tag to deploy on the MCP.') -param MCPContainerImageTag string = 'latest_v4' +param MCPContainerImageTag string = 'latest_v5' @description('Optional. Resource ID of an existing Log Analytics Workspace.') param existingLogAnalyticsWorkspaceId string = '' diff --git a/infra/bicep/main.json b/infra/bicep/main.json index ec310dbe9..e12b1b483 100644 --- a/infra/bicep/main.json +++ b/infra/bicep/main.json @@ -1,11 +1,12 @@ { "$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.44.1.10279", - "templateHash": "17693244970226019294" + "templateHash": "4324294973711463501" }, "name": "Multi-Agent Custom Automation Engine - Vanilla Bicep", "description": "Vanilla Bicep orchestrator for the Multi-Agent Custom Automation Engine accelerator. This deployment intentionally excludes WAF features such as private networking, scale-out, redundancy, bastion, and VM resources while keeping router-compatible outputs." @@ -231,7 +232,7 @@ }, "backendContainerImageTag": { "type": "string", - "defaultValue": "latest_v4", + "defaultValue": "latest_v5", "metadata": { "description": "Optional. The Container Image Tag to deploy on the backend." } @@ -252,7 +253,7 @@ }, "frontendContainerImageTag": { "type": "string", - "defaultValue": "latest_v4", + "defaultValue": "latest_v5", "metadata": { "description": "Optional. The Container Image Tag to deploy on the frontend." } @@ -273,7 +274,7 @@ }, "MCPContainerImageTag": { "type": "string", - "defaultValue": "latest_v4", + "defaultValue": "latest_v5", "metadata": { "description": "Optional. The Container Image Tag to deploy on the MCP." } @@ -368,6 +369,13 @@ "metadata": { "description": "Tag. Created by user name." } + }, + "isCustom": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Flag to indicate if this is a custom code deployment. If true, some resources may be skipped or configured differently." + } } }, "variables": { @@ -441,8 +449,8 @@ "mcpServerDescription": "MCP server with greeting, HR, and planning tools", "useExistingLogAnalytics": "[not(empty(parameters('existingLogAnalyticsWorkspaceId')))]" }, - "resources": [ - { + "resources": { + "resourceGroupTags": { "type": "Microsoft.Resources/tags", "apiVersion": "2023-07-01", "name": "default", @@ -450,7 +458,7 @@ "tags": "[union(variables('existingTags'), variables('allTags'), createObject('TemplateName', 'MACAE', 'Type', 'Non-WAF', 'CreatedBy', parameters('createdBy'), 'DeploymentName', deployment().name, 'SolutionSuffix', variables('solutionSuffix')))]" } }, - { + "log_analytics": { "condition": "[not(variables('useExistingLogAnalytics'))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", @@ -571,7 +579,7 @@ } } }, - { + "app_insights": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "[take(format('module.app-insights.{0}', variables('solutionSuffix')), 64)]", @@ -590,7 +598,7 @@ "tags": { "value": "[variables('allTags')]" }, - "workspaceResourceId": "[if(variables('useExistingLogAnalytics'), createObject('value', parameters('existingLogAnalyticsWorkspaceId')), createObject('value', reference(resourceId('Microsoft.Resources/deployments', take(format('module.log-analytics.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.resourceId.value))]" + "workspaceResourceId": "[if(variables('useExistingLogAnalytics'), createObject('value', parameters('existingLogAnalyticsWorkspaceId')), createObject('value', reference('log_analytics').outputs.resourceId.value))]" }, "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", @@ -730,10 +738,10 @@ } }, "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', take(format('module.log-analytics.{0}', variables('solutionSuffix')), 64))]" + "log_analytics" ] }, - { + "userAssignedIdentity": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "[take(format('module.user-assigned-identity.{0}', variables('solutionSuffix')), 64)]", @@ -836,7 +844,7 @@ } } }, - { + "ai_foundry_project": { "condition": "[not(variables('useExistingAiFoundryAiProject'))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", @@ -1075,7 +1083,7 @@ } } }, - { + "existing_project_setup": { "condition": "[variables('useExistingAiFoundryAiProject')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", @@ -1195,7 +1203,7 @@ } } }, - { + "ai_model_deployment": { "copy": { "name": "ai_model_deployment", "count": "[length(variables('modelDeployments'))]", @@ -1336,11 +1344,11 @@ } }, "dependsOn": [ - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('aiFoundryAiServicesSubscriptionId'), variables('aiFoundryAiServicesResourceGroupName')), 'Microsoft.Resources/deployments', take(format('module.ai-foundry-project.{0}', variables('solutionSuffix')), 64))]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('aiFoundryAiServicesSubscriptionId'), variables('aiFoundryAiServicesResourceGroupName')), 'Microsoft.Resources/deployments', take(format('module.existing-project-setup.{0}', variables('solutionSuffix')), 64))]" + "ai_foundry_project", + "existing_project_setup" ] }, - { + "ai_search": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "[take(format('module.ai-search.{0}', variables('solutionSuffix')), 64)]", @@ -1747,7 +1755,7 @@ } } }, - { + "storage_account": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "[take(format('module.storage-account.{0}', variables('solutionSuffix')), 64)]", @@ -1992,7 +2000,7 @@ } } }, - { + "cosmosDBModule": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "[take(format('module.cosmos-db.{0}', variables('solutionSuffix')), 64)]", @@ -2190,7 +2198,7 @@ } } }, - { + "container_app_environment": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "[take(format('module.container-app-environment.{0}', variables('solutionSuffix')), 64)]", @@ -2209,7 +2217,7 @@ "tags": { "value": "[variables('allTags')]" }, - "logAnalyticsWorkspaceResourceId": "[if(variables('useExistingLogAnalytics'), createObject('value', parameters('existingLogAnalyticsWorkspaceId')), createObject('value', reference(resourceId('Microsoft.Resources/deployments', take(format('module.log-analytics.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.resourceId.value))]", + "logAnalyticsWorkspaceResourceId": "[if(variables('useExistingLogAnalytics'), createObject('value', parameters('existingLogAnalyticsWorkspaceId')), createObject('value', reference('log_analytics').outputs.resourceId.value))]", "workloadProfiles": { "value": [ { @@ -2335,10 +2343,10 @@ } }, "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', take(format('module.log-analytics.{0}', variables('solutionSuffix')), 64))]" + "log_analytics" ] }, - { + "foundry_search_connection": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "[take(format('module.foundry-search-connection.{0}', variables('solutionSuffix')), 64)]", @@ -2369,7 +2377,7 @@ "value": "CognitiveSearch" }, "target": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.ai-search.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.endpoint.value]" + "value": "[reference('ai_search').outputs.endpoint.value]" }, "authType": { "value": "AAD" @@ -2377,7 +2385,7 @@ "metadata": { "value": { "ApiType": "Azure", - "ResourceId": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.ai-search.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.resourceId.value]" + "ResourceId": "[reference('ai_search').outputs.resourceId.value]" } } }, @@ -2511,21 +2519,25 @@ }, "dependsOn": [ "ai_model_deployment", - "[resourceId('Microsoft.Resources/deployments', take(format('module.ai-search.{0}', variables('solutionSuffix')), 64))]" + "ai_search" ] }, - { + "container_registry": { + "condition": "[parameters('isCustom')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[take(format('module.backend-container-app.{0}', variables('solutionSuffix')), 64)]", + "name": "[take(format('module.container-registry.{0}', variables('solutionSuffix')), 64)]", "properties": { "expressionEvaluationOptions": { "scope": "inner" }, "mode": "Incremental", "parameters": { + "solutionName": { + "value": "[variables('solutionSuffix')]" + }, "name": { - "value": "[variables('backendContainerAppName')]" + "value": "[format('cr{0}', variables('solutionSuffix'))]" }, "location": { "value": "[variables('solutionLocation')]" @@ -2533,8 +2545,181 @@ "tags": { "value": "[variables('allTags')]" }, + "sku": { + "value": "Basic" + }, + "adminUserEnabled": { + "value": false + }, + "publicNetworkAccess": { + "value": "Enabled" + }, + "exportPolicyStatus": { + "value": "enabled" + }, + "retentionPolicyStatus": { + "value": "disabled" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "9689542388193984627" + } + }, + "parameters": { + "solutionName": { + "type": "string", + "metadata": { + "description": "Solution name used for naming convention." + } + }, + "name": { + "type": "string", + "defaultValue": "[replace(format('cr{0}', parameters('solutionName')), '-', '')]", + "metadata": { + "description": "Name of the container registry." + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Azure region for deployment." + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Resource tags." + } + }, + "sku": { + "type": "string", + "defaultValue": "Premium", + "allowedValues": [ + "Basic", + "Standard", + "Premium" + ], + "metadata": { + "description": "SKU for the container registry." + } + }, + "adminUserEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Enable admin user." + } + }, + "publicNetworkAccess": { + "type": "string", + "defaultValue": "Enabled", + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Public network access setting." + } + }, + "exportPolicyStatus": { + "type": "string", + "defaultValue": "enabled", + "metadata": { + "description": "Export policy status." + } + }, + "retentionPolicyStatus": { + "type": "string", + "defaultValue": "disabled", + "metadata": { + "description": "Retention policy status." + } + } + }, + "resources": [ + { + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2025-04-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "sku": { + "name": "[parameters('sku')]" + }, + "properties": { + "adminUserEnabled": "[parameters('adminUserEnabled')]", + "publicNetworkAccess": "[parameters('publicNetworkAccess')]", + "dataEndpointEnabled": false, + "networkRuleBypassOptions": "AzureServices", + "policies": { + "exportPolicy": { + "status": "[parameters('exportPolicyStatus')]" + }, + "retentionPolicy": { + "status": "[parameters('retentionPolicyStatus')]", + "days": 7 + }, + "trustPolicy": { + "status": "disabled", + "type": "Notary" + } + }, + "zoneRedundancy": "Disabled" + } + } + ], + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the container registry." + }, + "value": "[parameters('name')]" + }, + "loginServer": { + "type": "string", + "metadata": { + "description": "The login server URL." + }, + "value": "[reference(resourceId('Microsoft.ContainerRegistry/registries', parameters('name')), '2025-04-01').loginServer]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the container registry." + }, + "value": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('name'))]" + } + } + } + } + }, + "backend_container_app": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('module.backend-container-app.{0}', variables('solutionSuffix')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('backendContainerAppName')]" + }, + "location": { + "value": "[variables('solutionLocation')]" + }, + "tags": "[if(parameters('isCustom'), createObject('value', union(variables('allTags'), createObject('azd-service-name', 'backend'))), createObject('value', variables('allTags')))]", "environmentResourceId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.container-app-environment.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.resourceId.value]" + "value": "[reference('container_app_environment').outputs.resourceId.value]" }, "ingressExternal": { "value": true @@ -2545,7 +2730,7 @@ "managedIdentities": { "value": { "userAssignedResourceIds": [ - "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.user-assigned-identity.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.resourceId.value]" + "[reference('userAssignedIdentity').outputs.resourceId.value]" ] } }, @@ -2570,6 +2755,7 @@ "maxReplicas": 1 } }, + "registries": "[if(parameters('isCustom'), createObject('value', createArray(createObject('server', reference('container_registry').outputs.loginServer.value, 'identity', reference('userAssignedIdentity').outputs.resourceId.value))), createObject('value', createArray()))]", "containers": { "value": [ { @@ -2582,7 +2768,7 @@ "env": [ { "name": "COSMOSDB_ENDPOINT", - "value": "[format('https://{0}.documents.azure.com:443/', reference(resourceId('Microsoft.Resources/deployments', take(format('module.cosmos-db.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.name.value)]" + "value": "[format('https://{0}.documents.azure.com:443/', reference('cosmosDBModule').outputs.name.value)]" }, { "name": "COSMOSDB_DATABASE", @@ -2610,11 +2796,11 @@ }, { "name": "APPLICATIONINSIGHTS_INSTRUMENTATION_KEY", - "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.app-insights.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.instrumentationKey.value]" + "value": "[reference('app_insights').outputs.instrumentationKey.value]" }, { "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", - "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.app-insights.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.connectionString.value]" + "value": "[reference('app_insights').outputs.connectionString.value]" }, { "name": "AZURE_AI_SUBSCRIPTION_ID", @@ -2638,7 +2824,7 @@ }, { "name": "AZURE_AI_SEARCH_ENDPOINT", - "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.ai-search.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.endpoint.value]" + "value": "[reference('ai_search').outputs.endpoint.value]" }, { "name": "AZURE_COGNITIVE_SERVICES", @@ -2654,7 +2840,7 @@ }, { "name": "MCP_SERVER_ENDPOINT", - "value": "[format('https://{0}/mcp', reference(resourceId('Microsoft.Resources/deployments', take(format('module.mcp-container-app.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.fqdn.value)]" + "value": "[format('https://{0}/mcp', reference('mcp_container_app').outputs.fqdn.value)]" }, { "name": "MCP_SERVER_NAME", @@ -2670,7 +2856,7 @@ }, { "name": "AZURE_CLIENT_ID", - "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.user-assigned-identity.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.clientId.value]" + "value": "[reference('userAssignedIdentity').outputs.clientId.value]" }, { "name": "SUPPORTED_MODELS", @@ -2678,7 +2864,7 @@ }, { "name": "AZURE_STORAGE_BLOB_URL", - "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.storage-account.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.blobEndpoint.value]" + "value": "[reference('storage_account').outputs.blobEndpoint.value]" }, { "name": "AZURE_AI_PROJECT_ENDPOINT", @@ -2911,16 +3097,17 @@ } }, "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', take(format('module.ai-search.{0}', variables('solutionSuffix')), 64))]", - "[resourceId('Microsoft.Resources/deployments', take(format('module.app-insights.{0}', variables('solutionSuffix')), 64))]", - "[resourceId('Microsoft.Resources/deployments', take(format('module.container-app-environment.{0}', variables('solutionSuffix')), 64))]", - "[resourceId('Microsoft.Resources/deployments', take(format('module.cosmos-db.{0}', variables('solutionSuffix')), 64))]", - "[resourceId('Microsoft.Resources/deployments', take(format('module.mcp-container-app.{0}', variables('solutionSuffix')), 64))]", - "[resourceId('Microsoft.Resources/deployments', take(format('module.storage-account.{0}', variables('solutionSuffix')), 64))]", - "[resourceId('Microsoft.Resources/deployments', take(format('module.user-assigned-identity.{0}', variables('solutionSuffix')), 64))]" + "ai_search", + "app_insights", + "container_app_environment", + "container_registry", + "cosmosDBModule", + "mcp_container_app", + "storage_account", + "userAssignedIdentity" ] }, - { + "mcp_container_app": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "[take(format('module.mcp-container-app.{0}', variables('solutionSuffix')), 64)]", @@ -2936,11 +3123,9 @@ "location": { "value": "[variables('solutionLocation')]" }, - "tags": { - "value": "[variables('allTags')]" - }, + "tags": "[if(parameters('isCustom'), createObject('value', union(variables('allTags'), createObject('azd-service-name', 'mcp'))), createObject('value', variables('allTags')))]", "environmentResourceId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.container-app-environment.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.resourceId.value]" + "value": "[reference('container_app_environment').outputs.resourceId.value]" }, "ingressExternal": { "value": true @@ -2951,7 +3136,7 @@ "managedIdentities": { "value": { "userAssignedResourceIds": [ - "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.user-assigned-identity.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.resourceId.value]" + "[reference('userAssignedIdentity').outputs.resourceId.value]" ] } }, @@ -2969,6 +3154,7 @@ "maxReplicas": 1 } }, + "registries": "[if(parameters('isCustom'), createObject('value', createArray(createObject('server', reference('container_registry').outputs.loginServer.value, 'identity', reference('userAssignedIdentity').outputs.resourceId.value))), createObject('value', createArray()))]", "containers": { "value": [ { @@ -3005,7 +3191,7 @@ }, { "name": "CLIENT_ID", - "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.user-assigned-identity.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.clientId.value]" + "value": "[reference('userAssignedIdentity').outputs.clientId.value]" }, { "name": "JWKS_URI", @@ -3017,7 +3203,7 @@ }, { "name": "AUDIENCE", - "value": "[format('api://{0}', reference(resourceId('Microsoft.Resources/deployments', take(format('module.user-assigned-identity.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.clientId.value)]" + "value": "[format('api://{0}', reference('userAssignedIdentity').outputs.clientId.value)]" }, { "name": "DATASET_PATH", @@ -3025,7 +3211,7 @@ }, { "name": "AZURE_CLIENT_ID", - "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.user-assigned-identity.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.clientId.value]" + "value": "[reference('userAssignedIdentity').outputs.clientId.value]" }, { "name": "AZURE_OPENAI_ENDPOINT", @@ -3037,11 +3223,11 @@ }, { "name": "AZURE_STORAGE_BLOB_URL", - "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.storage-account.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.blobEndpoint.value]" + "value": "[reference('storage_account').outputs.blobEndpoint.value]" }, { "name": "BACKEND_URL", - "value": "[format('https://{0}.{1}', variables('backendContainerAppName'), reference(resourceId('Microsoft.Resources/deployments', take(format('module.container-app-environment.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.defaultDomain.value)]" + "value": "[format('https://{0}.{1}', variables('backendContainerAppName'), reference('container_app_environment').outputs.defaultDomain.value)]" } ] } @@ -3254,12 +3440,13 @@ } }, "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', take(format('module.container-app-environment.{0}', variables('solutionSuffix')), 64))]", - "[resourceId('Microsoft.Resources/deployments', take(format('module.storage-account.{0}', variables('solutionSuffix')), 64))]", - "[resourceId('Microsoft.Resources/deployments', take(format('module.user-assigned-identity.{0}', variables('solutionSuffix')), 64))]" + "container_app_environment", + "container_registry", + "storage_account", + "userAssignedIdentity" ] }, - { + "app_service_plan": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "[take(format('module.app-service-plan.{0}', variables('solutionSuffix')), 64)]", @@ -3415,7 +3602,7 @@ } } }, - { + "frontend_app": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "[take(format('module.frontend-app.{0}', variables('solutionSuffix')), 64)]", @@ -3431,28 +3618,13 @@ "location": { "value": "[variables('solutionLocation')]" }, - "tags": { - "value": "[variables('allTags')]" - }, + "tags": "[if(parameters('isCustom'), createObject('value', union(variables('allTags'), createObject('azd-service-name', 'frontend'))), createObject('value', variables('allTags')))]", "serverFarmResourceId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.app-service-plan.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.resourceId.value]" - }, - "linuxFxVersion": { - "value": "[format('DOCKER|{0}/{1}:{2}', parameters('frontendContainerRegistryHostname'), parameters('frontendContainerImageName'), parameters('frontendContainerImageTag'))]" + "value": "[reference('app_service_plan').outputs.resourceId.value]" }, - "appSettings": { - "value": { - "SCM_DO_BUILD_DURING_DEPLOYMENT": "true", - "DOCKER_REGISTRY_SERVER_URL": "[format('https://{0}', parameters('frontendContainerRegistryHostname'))]", - "WEBSITES_PORT": "3000", - "WEBSITES_CONTAINER_START_TIME_LIMIT": "1800", - "BACKEND_API_URL": "[format('https://{0}', reference(resourceId('Microsoft.Resources/deployments', take(format('module.backend-container-app.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.fqdn.value)]", - "AUTH_ENABLED": "false", - "PROXY_API_REQUESTS": "false", - "APPLICATIONINSIGHTS_CONNECTION_STRING": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.app-insights.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.connectionString.value]", - "APPINSIGHTS_INSTRUMENTATIONKEY": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.app-insights.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.instrumentationKey.value]" - } - } + "linuxFxVersion": "[if(parameters('isCustom'), createObject('value', 'python|3.11'), createObject('value', format('DOCKER|{0}/{1}:{2}', parameters('frontendContainerRegistryHostname'), parameters('frontendContainerImageName'), parameters('frontendContainerImageTag'))))]", + "appCommandLine": "[if(parameters('isCustom'), createObject('value', 'python3 -m uvicorn frontend_server:app --host 0.0.0.0 --port 8000'), createObject('value', ''))]", + "appSettings": "[if(parameters('isCustom'), createObject('value', createObject('SCM_DO_BUILD_DURING_DEPLOYMENT', 'True', 'WEBSITES_PORT', '8000', 'BACKEND_API_URL', format('https://{0}', reference('backend_container_app').outputs.fqdn.value), 'AUTH_ENABLED', 'false', 'PROXY_API_REQUESTS', 'false', 'ENABLE_ORYX_BUILD', 'True', 'APPLICATIONINSIGHTS_CONNECTION_STRING', reference('app_insights').outputs.connectionString.value, 'APPINSIGHTS_INSTRUMENTATIONKEY', reference('app_insights').outputs.instrumentationKey.value)), createObject('value', createObject('SCM_DO_BUILD_DURING_DEPLOYMENT', 'true', 'DOCKER_REGISTRY_SERVER_URL', format('https://{0}', parameters('frontendContainerRegistryHostname')), 'WEBSITES_PORT', '3000', 'WEBSITES_CONTAINER_START_TIME_LIMIT', '1800', 'BACKEND_API_URL', format('https://{0}', reference('backend_container_app').outputs.fqdn.value), 'AUTH_ENABLED', 'false', 'PROXY_API_REQUESTS', 'false', 'APPLICATIONINSIGHTS_CONNECTION_STRING', reference('app_insights').outputs.connectionString.value, 'APPINSIGHTS_INSTRUMENTATIONKEY', reference('app_insights').outputs.instrumentationKey.value)))]" }, "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", @@ -3694,12 +3866,12 @@ } }, "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', take(format('module.app-insights.{0}', variables('solutionSuffix')), 64))]", - "[resourceId('Microsoft.Resources/deployments', take(format('module.app-service-plan.{0}', variables('solutionSuffix')), 64))]", - "[resourceId('Microsoft.Resources/deployments', take(format('module.backend-container-app.{0}', variables('solutionSuffix')), 64))]" + "app_insights", + "app_service_plan", + "backend_container_app" ] }, - { + "role_assignments": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "[take(format('module.role-assignments.{0}', variables('solutionSuffix')), 64)]", @@ -3718,16 +3890,16 @@ "existingFoundryProjectResourceId": { "value": "[parameters('existingFoundryProjectResourceId')]" }, - "aiFoundryResourceId": "[if(variables('useExistingAiFoundryAiProject'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('aiFoundryAiServicesSubscriptionId'), variables('aiFoundryAiServicesResourceGroupName')), 'Microsoft.Resources/deployments', take(format('module.existing-project-setup.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.resourceId.value), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('aiFoundryAiServicesSubscriptionId'), variables('aiFoundryAiServicesResourceGroupName')), 'Microsoft.Resources/deployments', take(format('module.ai-foundry-project.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.resourceId.value))]", + "aiFoundryResourceId": "[if(variables('useExistingAiFoundryAiProject'), createObject('value', reference('existing_project_setup').outputs.resourceId.value), createObject('value', reference('ai_foundry_project').outputs.resourceId.value))]", "aiSearchResourceId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.ai-search.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.resourceId.value]" + "value": "[reference('ai_search').outputs.resourceId.value]" }, "storageAccountResourceId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.storage-account.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.resourceId.value]" + "value": "[reference('storage_account').outputs.resourceId.value]" }, - "aiProjectPrincipalId": "[if(variables('useExistingAiFoundryAiProject'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('aiFoundryAiServicesSubscriptionId'), variables('aiFoundryAiServicesResourceGroupName')), 'Microsoft.Resources/deployments', take(format('module.existing-project-setup.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.projectIdentityPrincipalId.value), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('aiFoundryAiServicesSubscriptionId'), variables('aiFoundryAiServicesResourceGroupName')), 'Microsoft.Resources/deployments', take(format('module.ai-foundry-project.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.projectIdentityPrincipalId.value))]", + "aiProjectPrincipalId": "[if(variables('useExistingAiFoundryAiProject'), createObject('value', reference('existing_project_setup').outputs.projectIdentityPrincipalId.value), createObject('value', reference('ai_foundry_project').outputs.projectIdentityPrincipalId.value))]", "aiSearchPrincipalId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.ai-search.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.identityPrincipalId.value]" + "value": "[reference('ai_search').outputs.identityPrincipalId.value]" }, "deployerPrincipalId": { "value": "[variables('deployingUserPrincipalId')]" @@ -3736,11 +3908,12 @@ "value": "[variables('deployerPrincipalType')]" }, "userAssignedManagedIdentityPrincipalId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.user-assigned-identity.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.principalId.value]" + "value": "[reference('userAssignedIdentity').outputs.principalId.value]" }, "cosmosDbAccountName": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.cosmos-db.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.name.value]" - } + "value": "[reference('cosmosDBModule').outputs.name.value]" + }, + "containerRegistryResourceId": "[if(parameters('isCustom'), createObject('value', reference('container_registry').outputs.resourceId.value), createObject('value', ''))]" }, "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", @@ -3749,7 +3922,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "9862074830347932153" + "templateHash": "12073010712449995872" } }, "parameters": { @@ -3792,7 +3965,14 @@ "type": "string", "defaultValue": "", "metadata": { - "description": "Principal ID of the user-assigned managed identity (empty if not deployed)." + "description": "Principal ID of the user-assigned managed identity (empty if not deployed). Kept for backward compatibility; prefer workloadPrincipalIds." + } + }, + "workloadPrincipalIds": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Optional. List of workload identity principal IDs (e.g. system-assigned identities of backend, MCP, and frontend hosts) that should receive the same data-plane roles previously granted to the UAMI. When non-empty, this list takes precedence over userAssignedManagedIdentityPrincipalId." } }, "deployerPrincipalId": { @@ -3840,12 +4020,20 @@ "metadata": { "description": "Name of the Cosmos DB account (empty if not deployed)." } + }, + "containerRegistryResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Resource ID of the Container Registry (empty if not deployed)." + } } }, "variables": { "existingAIFoundryName": "[if(parameters('useExistingAIProject'), split(parameters('existingFoundryProjectResourceId'), '/')[8], '')]", "existingAIFoundrySubscription": "[if(parameters('useExistingAIProject'), split(parameters('existingFoundryProjectResourceId'), '/')[2], subscription().subscriptionId)]", "existingAIFoundryResourceGroup": "[if(parameters('useExistingAIProject'), split(parameters('existingFoundryProjectResourceId'), '/')[4], resourceGroup().name)]", + "workloadPrincipals": "[if(not(empty(parameters('workloadPrincipalIds'))), parameters('workloadPrincipalIds'), if(empty(parameters('userAssignedManagedIdentityPrincipalId')), createArray(), createArray(parameters('userAssignedManagedIdentityPrincipalId'))))]", "roleDefinitions": { "azureAiUser": "53ca6127-db72-4b80-b1b0-d745d6d5456d", "cognitiveServicesUser": "a97b65f3-24c7-4388-baec-2e87135dc908", @@ -3855,7 +4043,8 @@ "searchIndexDataContributor": "8ebe5a00-799e-43f5-93ac-243d3dce84a7", "searchServiceContributor": "7ca78c08-252a-4471-8644-bb5ff32d4ba0", "storageBlobDataContributor": "ba92f5b4-2d11-453d-a403-e96b0029c9fe", - "storageBlobDataReader": "2a2b9908-6ea1-4ae2-8e65-a410df84e7d1" + "storageBlobDataReader": "2a2b9908-6ea1-4ae2-8e65-a410df84e7d1", + "acrPull": "7f951dda-4ed3-4680-a7ca-43fe172d538d" } }, "resources": [ @@ -3872,25 +4061,33 @@ } }, { - "condition": "[and(and(not(parameters('useExistingAIProject')), not(empty(parameters('aiFoundryResourceId')))), not(empty(parameters('userAssignedManagedIdentityPrincipalId'))))]", + "copy": { + "name": "workloadAiUserAssignment", + "count": "[length(variables('workloadPrincipals'))]" + }, + "condition": "[and(and(not(parameters('useExistingAIProject')), not(empty(parameters('aiFoundryResourceId')))), not(empty(variables('workloadPrincipals')[copyIndex()])))]", "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", "scope": "[resourceId('Microsoft.CognitiveServices/accounts', last(split(parameters('aiFoundryResourceId'), '/')))]", - "name": "[guid(parameters('solutionName'), resourceId('Microsoft.CognitiveServices/accounts', last(split(parameters('aiFoundryResourceId'), '/'))), parameters('userAssignedManagedIdentityPrincipalId'), variables('roleDefinitions').azureAiUser)]", + "name": "[guid(parameters('solutionName'), resourceId('Microsoft.CognitiveServices/accounts', last(split(parameters('aiFoundryResourceId'), '/'))), variables('workloadPrincipals')[copyIndex()], variables('roleDefinitions').azureAiUser)]", "properties": { - "principalId": "[parameters('userAssignedManagedIdentityPrincipalId')]", + "principalId": "[variables('workloadPrincipals')[copyIndex()]]", "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('roleDefinitions').azureAiUser)]", "principalType": "ServicePrincipal" } }, { - "condition": "[and(and(not(parameters('useExistingAIProject')), not(empty(parameters('aiFoundryResourceId')))), not(empty(parameters('userAssignedManagedIdentityPrincipalId'))))]", + "copy": { + "name": "workloadOpenAIContributor", + "count": "[length(variables('workloadPrincipals'))]" + }, + "condition": "[and(and(not(parameters('useExistingAIProject')), not(empty(parameters('aiFoundryResourceId')))), not(empty(variables('workloadPrincipals')[copyIndex()])))]", "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", "scope": "[resourceId('Microsoft.CognitiveServices/accounts', last(split(parameters('aiFoundryResourceId'), '/')))]", - "name": "[guid(parameters('solutionName'), resourceId('Microsoft.CognitiveServices/accounts', last(split(parameters('aiFoundryResourceId'), '/'))), parameters('userAssignedManagedIdentityPrincipalId'), variables('roleDefinitions').cognitiveServicesOpenAIContributor)]", + "name": "[guid(parameters('solutionName'), resourceId('Microsoft.CognitiveServices/accounts', last(split(parameters('aiFoundryResourceId'), '/'))), variables('workloadPrincipals')[copyIndex()], variables('roleDefinitions').cognitiveServicesOpenAIContributor)]", "properties": { - "principalId": "[parameters('userAssignedManagedIdentityPrincipalId')]", + "principalId": "[variables('workloadPrincipals')[copyIndex()]]", "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('roleDefinitions').cognitiveServicesOpenAIContributor)]", "principalType": "ServicePrincipal" } @@ -3920,96 +4117,64 @@ } }, { - "condition": "[and(not(empty(parameters('aiSearchResourceId'))), not(empty(parameters('userAssignedManagedIdentityPrincipalId'))))]", - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[resourceId('Microsoft.Search/searchServices', last(split(parameters('aiSearchResourceId'), '/')))]", - "name": "[guid(parameters('solutionName'), parameters('aiSearchResourceId'), parameters('userAssignedManagedIdentityPrincipalId'), variables('roleDefinitions').searchIndexDataReader)]", - "properties": { - "principalId": "[parameters('userAssignedManagedIdentityPrincipalId')]", - "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('roleDefinitions').searchIndexDataReader)]", - "principalType": "ServicePrincipal" - } - }, - { - "condition": "[and(not(empty(parameters('aiSearchResourceId'))), not(empty(parameters('userAssignedManagedIdentityPrincipalId'))))]", + "copy": { + "name": "workloadSearchIndexContributor", + "count": "[length(variables('workloadPrincipals'))]" + }, + "condition": "[and(not(empty(parameters('aiSearchResourceId'))), not(empty(variables('workloadPrincipals')[copyIndex()])))]", "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", "scope": "[resourceId('Microsoft.Search/searchServices', last(split(parameters('aiSearchResourceId'), '/')))]", - "name": "[guid(parameters('solutionName'), parameters('aiSearchResourceId'), parameters('userAssignedManagedIdentityPrincipalId'), variables('roleDefinitions').searchIndexDataContributor)]", + "name": "[guid(parameters('solutionName'), parameters('aiSearchResourceId'), variables('workloadPrincipals')[copyIndex()], variables('roleDefinitions').searchIndexDataContributor)]", "properties": { - "principalId": "[parameters('userAssignedManagedIdentityPrincipalId')]", + "principalId": "[variables('workloadPrincipals')[copyIndex()]]", "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('roleDefinitions').searchIndexDataContributor)]", "principalType": "ServicePrincipal" } }, { - "condition": "[and(not(empty(parameters('aiSearchResourceId'))), not(empty(parameters('userAssignedManagedIdentityPrincipalId'))))]", + "copy": { + "name": "workloadSearchServiceContributor", + "count": "[length(variables('workloadPrincipals'))]" + }, + "condition": "[and(not(empty(parameters('aiSearchResourceId'))), not(empty(variables('workloadPrincipals')[copyIndex()])))]", "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", "scope": "[resourceId('Microsoft.Search/searchServices', last(split(parameters('aiSearchResourceId'), '/')))]", - "name": "[guid(parameters('solutionName'), parameters('aiSearchResourceId'), parameters('userAssignedManagedIdentityPrincipalId'), variables('roleDefinitions').searchServiceContributor)]", + "name": "[guid(parameters('solutionName'), parameters('aiSearchResourceId'), variables('workloadPrincipals')[copyIndex()], variables('roleDefinitions').searchServiceContributor)]", "properties": { - "principalId": "[parameters('userAssignedManagedIdentityPrincipalId')]", + "principalId": "[variables('workloadPrincipals')[copyIndex()]]", "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('roleDefinitions').searchServiceContributor)]", "principalType": "ServicePrincipal" } }, { - "condition": "[and(not(empty(parameters('storageAccountResourceId'))), not(empty(parameters('aiProjectPrincipalId'))))]", - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[resourceId('Microsoft.Storage/storageAccounts', last(split(parameters('storageAccountResourceId'), '/')))]", - "name": "[guid(parameters('solutionName'), parameters('storageAccountResourceId'), parameters('aiProjectPrincipalId'), variables('roleDefinitions').storageBlobDataContributor)]", - "properties": { - "principalId": "[parameters('aiProjectPrincipalId')]", - "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('roleDefinitions').storageBlobDataContributor)]", - "principalType": "ServicePrincipal" - } - }, - { - "condition": "[and(not(empty(parameters('storageAccountResourceId'))), not(empty(parameters('userAssignedManagedIdentityPrincipalId'))))]", + "copy": { + "name": "workloadStorageContributor", + "count": "[length(variables('workloadPrincipals'))]" + }, + "condition": "[and(not(empty(parameters('storageAccountResourceId'))), not(empty(variables('workloadPrincipals')[copyIndex()])))]", "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", "scope": "[resourceId('Microsoft.Storage/storageAccounts', last(split(parameters('storageAccountResourceId'), '/')))]", - "name": "[guid(parameters('solutionName'), parameters('storageAccountResourceId'), parameters('userAssignedManagedIdentityPrincipalId'), variables('roleDefinitions').storageBlobDataContributor)]", + "name": "[guid(parameters('solutionName'), parameters('storageAccountResourceId'), variables('workloadPrincipals')[copyIndex()], variables('roleDefinitions').storageBlobDataContributor)]", "properties": { - "principalId": "[parameters('userAssignedManagedIdentityPrincipalId')]", + "principalId": "[variables('workloadPrincipals')[copyIndex()]]", "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('roleDefinitions').storageBlobDataContributor)]", "principalType": "ServicePrincipal" } }, { - "condition": "[and(not(empty(parameters('storageAccountResourceId'))), not(empty(parameters('aiProjectPrincipalId'))))]", - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[resourceId('Microsoft.Storage/storageAccounts', last(split(parameters('storageAccountResourceId'), '/')))]", - "name": "[guid(parameters('solutionName'), parameters('storageAccountResourceId'), parameters('aiProjectPrincipalId'), variables('roleDefinitions').storageBlobDataReader)]", - "properties": { - "principalId": "[parameters('aiProjectPrincipalId')]", - "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('roleDefinitions').storageBlobDataReader)]", - "principalType": "ServicePrincipal" - } - }, - { - "condition": "[and(not(empty(parameters('storageAccountResourceId'))), not(empty(parameters('aiSearchPrincipalId'))))]", - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[resourceId('Microsoft.Storage/storageAccounts', last(split(parameters('storageAccountResourceId'), '/')))]", - "name": "[guid(parameters('solutionName'), parameters('storageAccountResourceId'), parameters('aiSearchPrincipalId'), variables('roleDefinitions').storageBlobDataReader)]", - "properties": { - "principalId": "[parameters('aiSearchPrincipalId')]", - "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('roleDefinitions').storageBlobDataReader)]", - "principalType": "ServicePrincipal" - } - }, - { - "condition": "[and(not(empty(parameters('cosmosDbAccountName'))), not(empty(parameters('userAssignedManagedIdentityPrincipalId'))))]", + "copy": { + "name": "workloadCosmosRoleAssignment", + "count": "[length(variables('workloadPrincipals'))]" + }, + "condition": "[and(not(empty(parameters('cosmosDbAccountName'))), not(empty(variables('workloadPrincipals')[copyIndex()])))]", "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments", "apiVersion": "2025-10-15", - "name": "[format('{0}/{1}', parameters('cosmosDbAccountName'), guid(parameters('solutionName'), resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', parameters('cosmosDbAccountName'), '00000000-0000-0000-0000-000000000002'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbAccountName')), parameters('userAssignedManagedIdentityPrincipalId')))]", + "name": "[format('{0}/{1}', parameters('cosmosDbAccountName'), guid(parameters('solutionName'), resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', parameters('cosmosDbAccountName'), '00000000-0000-0000-0000-000000000002'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbAccountName')), variables('workloadPrincipals')[copyIndex()]))]", "properties": { - "principalId": "[parameters('userAssignedManagedIdentityPrincipalId')]", + "principalId": "[variables('workloadPrincipals')[copyIndex()]]", "roleDefinitionId": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', parameters('cosmosDbAccountName'), '00000000-0000-0000-0000-000000000002')]", "scope": "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbAccountName'))]" } @@ -4085,6 +4250,22 @@ "scope": "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbAccountName'))]" } }, + { + "copy": { + "name": "workloadAcrPull", + "count": "[length(variables('workloadPrincipals'))]" + }, + "condition": "[and(not(empty(parameters('containerRegistryResourceId'))), not(empty(variables('workloadPrincipals')[copyIndex()])))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.ContainerRegistry/registries', last(split(parameters('containerRegistryResourceId'), '/')))]", + "name": "[guid(parameters('solutionName'), resourceId('Microsoft.ContainerRegistry/registries', last(split(parameters('containerRegistryResourceId'), '/'))), variables('workloadPrincipals')[copyIndex()], variables('roleDefinitions').acrPull)]", + "properties": { + "principalId": "[variables('workloadPrincipals')[copyIndex()]]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('roleDefinitions').acrPull)]", + "principalType": "ServicePrincipal" + } + }, { "condition": "[and(parameters('useExistingAIProject'), not(empty(parameters('aiSearchPrincipalId'))))]", "type": "Microsoft.Resources/deployments", @@ -4175,10 +4356,14 @@ } }, { - "condition": "[and(parameters('useExistingAIProject'), not(empty(parameters('userAssignedManagedIdentityPrincipalId'))))]", + "copy": { + "name": "workloadAiUserExisting", + "count": "[length(variables('workloadPrincipals'))]" + }, + "condition": "[and(parameters('useExistingAIProject'), not(empty(variables('workloadPrincipals')[copyIndex()])))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "assignAiUserRoleToBackendExisting", + "name": "[format('assignAiUserRoleToWorkloadExisting-{0}', copyIndex())]", "subscriptionId": "[variables('existingAIFoundrySubscription')]", "resourceGroup": "[variables('existingAIFoundryResourceGroup')]", "properties": { @@ -4188,13 +4373,13 @@ "mode": "Incremental", "parameters": { "principalId": { - "value": "[parameters('userAssignedManagedIdentityPrincipalId')]" + "value": "[variables('workloadPrincipals')[copyIndex()]]" }, "roleDefinitionId": { "value": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('roleDefinitions').azureAiUser)]" }, "roleAssignmentName": { - "value": "[guid(parameters('solutionName'), variables('existingAIFoundryName'), parameters('userAssignedManagedIdentityPrincipalId'), variables('roleDefinitions').azureAiUser)]" + "value": "[guid(parameters('solutionName'), variables('existingAIFoundryName'), variables('workloadPrincipals')[copyIndex()], variables('roleDefinitions').azureAiUser)]" }, "aiFoundryName": { "value": "[variables('existingAIFoundryName')]" @@ -4264,10 +4449,14 @@ } }, { - "condition": "[and(parameters('useExistingAIProject'), not(empty(parameters('userAssignedManagedIdentityPrincipalId'))))]", + "copy": { + "name": "workloadOpenAIContributorExisting", + "count": "[length(variables('workloadPrincipals'))]" + }, + "condition": "[and(parameters('useExistingAIProject'), not(empty(variables('workloadPrincipals')[copyIndex()])))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "assignOpenAIContributorRoleToBackendExisting", + "name": "[format('assignOpenAIContributorRoleToWorkloadExisting-{0}', copyIndex())]", "subscriptionId": "[variables('existingAIFoundrySubscription')]", "resourceGroup": "[variables('existingAIFoundryResourceGroup')]", "properties": { @@ -4277,13 +4466,13 @@ "mode": "Incremental", "parameters": { "principalId": { - "value": "[parameters('userAssignedManagedIdentityPrincipalId')]" + "value": "[variables('workloadPrincipals')[copyIndex()]]" }, "roleDefinitionId": { "value": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('roleDefinitions').cognitiveServicesOpenAIContributor)]" }, "roleAssignmentName": { - "value": "[guid(parameters('solutionName'), variables('existingAIFoundryName'), parameters('userAssignedManagedIdentityPrincipalId'), variables('roleDefinitions').cognitiveServicesOpenAIContributor)]" + "value": "[guid(parameters('solutionName'), variables('existingAIFoundryName'), variables('workloadPrincipals')[copyIndex()], variables('roleDefinitions').cognitiveServicesOpenAIContributor)]" }, "aiFoundryName": { "value": "[variables('existingAIFoundryName')]" @@ -4356,15 +4545,16 @@ } }, "dependsOn": [ - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('aiFoundryAiServicesSubscriptionId'), variables('aiFoundryAiServicesResourceGroupName')), 'Microsoft.Resources/deployments', take(format('module.ai-foundry-project.{0}', variables('solutionSuffix')), 64))]", - "[resourceId('Microsoft.Resources/deployments', take(format('module.ai-search.{0}', variables('solutionSuffix')), 64))]", - "[resourceId('Microsoft.Resources/deployments', take(format('module.cosmos-db.{0}', variables('solutionSuffix')), 64))]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('aiFoundryAiServicesSubscriptionId'), variables('aiFoundryAiServicesResourceGroupName')), 'Microsoft.Resources/deployments', take(format('module.existing-project-setup.{0}', variables('solutionSuffix')), 64))]", - "[resourceId('Microsoft.Resources/deployments', take(format('module.storage-account.{0}', variables('solutionSuffix')), 64))]", - "[resourceId('Microsoft.Resources/deployments', take(format('module.user-assigned-identity.{0}', variables('solutionSuffix')), 64))]" + "ai_foundry_project", + "ai_search", + "container_registry", + "cosmosDBModule", + "existing_project_setup", + "storage_account", + "userAssignedIdentity" ] } - ], + }, "outputs": { "resourceGroupName": { "type": "string", @@ -4378,187 +4568,338 @@ "metadata": { "description": "The default hostname of the frontend web app." }, - "value": "[replace(reference(resourceId('Microsoft.Resources/deployments', take(format('module.frontend-app.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.appUrl.value, 'https://', '')]" + "value": "[replace(reference('frontend_app').outputs.appUrl.value, 'https://', '')]" }, "AZURE_STORAGE_BLOB_URL": { "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.storage-account.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.blobEndpoint.value]" + "metadata": { + "description": "The blob service endpoint of the deployed storage account." + }, + "value": "[reference('storage_account').outputs.blobEndpoint.value]" }, "AZURE_STORAGE_ACCOUNT_NAME": { "type": "string", + "metadata": { + "description": "The name of the deployed storage account." + }, "value": "[variables('storageAccountName')]" }, "AZURE_AI_SEARCH_ENDPOINT": { "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.ai-search.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.endpoint.value]" + "metadata": { + "description": "The endpoint URL of the deployed Azure AI Search service." + }, + "value": "[reference('ai_search').outputs.endpoint.value]" }, "AZURE_AI_SEARCH_NAME": { "type": "string", + "metadata": { + "description": "The name of the deployed Azure AI Search service." + }, "value": "[variables('aiSearchServiceName')]" }, "COSMOSDB_ENDPOINT": { "type": "string", + "metadata": { + "description": "The endpoint URL of the deployed Cosmos DB account." + }, "value": "[format('https://{0}.documents.azure.com:443/', variables('cosmosDbResourceName'))]" }, "COSMOSDB_DATABASE": { "type": "string", + "metadata": { + "description": "The Cosmos DB database name used by the application." + }, "value": "[variables('cosmosDbDatabaseName')]" }, "COSMOSDB_CONTAINER": { "type": "string", + "metadata": { + "description": "The Cosmos DB container name used to persist agent memory." + }, "value": "[variables('cosmosDbDatabaseMemoryContainerName')]" }, "AZURE_OPENAI_ENDPOINT": { "type": "string", + "metadata": { + "description": "The Azure OpenAI endpoint URL for the AI Foundry / AI Services account." + }, "value": "[variables('aiFoundryOpenAIEndpoint')]" }, "AZURE_OPENAI_DEPLOYMENT_NAME": { "type": "string", + "metadata": { + "description": "The default GPT model deployment name." + }, "value": "[parameters('gptModelName')]" }, "AZURE_OPENAI_RAI_DEPLOYMENT_NAME": { "type": "string", + "metadata": { + "description": "The RAI (Responsible AI) GPT model deployment name." + }, "value": "[parameters('gpt4_1ModelName')]" }, "AZURE_OPENAI_API_VERSION": { "type": "string", + "metadata": { + "description": "The Azure OpenAI API version used by the application." + }, "value": "[parameters('azureOpenaiAPIVersion')]" }, "AZURE_AI_SUBSCRIPTION_ID": { "type": "string", + "metadata": { + "description": "The subscription id that hosts the AI Foundry / AI Services resources." + }, "value": "[subscription().subscriptionId]" }, "AZURE_AI_RESOURCE_GROUP": { "type": "string", + "metadata": { + "description": "The resource group that hosts the AI Foundry / AI Services resources." + }, "value": "[resourceGroup().name]" }, "AZURE_AI_PROJECT_NAME": { "type": "string", + "metadata": { + "description": "The name of the AI Foundry project resource." + }, "value": "[variables('aiFoundryAiProjectResourceName')]" }, "APP_ENV": { "type": "string", + "metadata": { + "description": "The application environment label (e.g. Dev, Prod)." + }, "value": "Prod" }, "AI_FOUNDRY_RESOURCE_ID": { "type": "string", - "value": "[if(variables('useExistingAiFoundryAiProject'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('aiFoundryAiServicesSubscriptionId'), variables('aiFoundryAiServicesResourceGroupName')), 'Microsoft.Resources/deployments', take(format('module.existing-project-setup.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.resourceId.value, reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('aiFoundryAiServicesSubscriptionId'), variables('aiFoundryAiServicesResourceGroupName')), 'Microsoft.Resources/deployments', take(format('module.ai-foundry-project.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.resourceId.value)]" + "metadata": { + "description": "The AI Foundry resource id (existing project resource id when reusing, otherwise the newly created project)." + }, + "value": "[if(variables('useExistingAiFoundryAiProject'), reference('existing_project_setup').outputs.resourceId.value, reference('ai_foundry_project').outputs.resourceId.value)]" }, "COSMOSDB_ACCOUNT_NAME": { "type": "string", + "metadata": { + "description": "The name of the deployed Cosmos DB account." + }, "value": "[variables('cosmosDbResourceName')]" }, "AZURE_SEARCH_ENDPOINT": { "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.ai-search.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.endpoint.value]" + "metadata": { + "description": "Alias for AZURE_AI_SEARCH_ENDPOINT — kept for backward compatibility with seed scripts and the backend." + }, + "value": "[reference('ai_search').outputs.endpoint.value]" }, "AZURE_CLIENT_ID": { "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.user-assigned-identity.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.clientId.value]" + "metadata": { + "description": "The client id of the user-assigned managed identity used by backend and MCP container apps." + }, + "value": "[reference('userAssignedIdentity').outputs.clientId.value]" }, "AZURE_TENANT_ID": { "type": "string", + "metadata": { + "description": "The Azure AD tenant id of the deployment." + }, "value": "[tenant().tenantId]" }, "AZURE_COGNITIVE_SERVICES": { "type": "string", + "metadata": { + "description": "The default Cognitive Services resource scope used to acquire AAD tokens." + }, "value": "https://cognitiveservices.azure.com/.default" }, "ORCHESTRATOR_MODEL_NAME": { "type": "string", + "metadata": { + "description": "The model deployment name used by the orchestrator/manager (reasoning model)." + }, "value": "[parameters('gptReasoningModelName')]" }, "MCP_SERVER_NAME": { "type": "string", + "metadata": { + "description": "The display name of the MCP server." + }, "value": "[variables('mcpServerName')]" }, "MCP_SERVER_DESCRIPTION": { "type": "string", + "metadata": { + "description": "Human-readable description of the MCP server." + }, "value": "[variables('mcpServerDescription')]" }, "SUPPORTED_MODELS": { "type": "string", + "metadata": { + "description": "JSON-serialized list of model deployment names supported by this deployment." + }, "value": "[string(variables('supportedModels'))]" }, "BACKEND_URL": { "type": "string", - "value": "[format('https://{0}', reference(resourceId('Microsoft.Resources/deployments', take(format('module.backend-container-app.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.fqdn.value)]" + "metadata": { + "description": "Public HTTPS URL of the backend Container App." + }, + "value": "[format('https://{0}', reference('backend_container_app').outputs.fqdn.value)]" }, "AZURE_AI_PROJECT_ENDPOINT": { "type": "string", + "metadata": { + "description": "AI Foundry project endpoint URL used by the agents." + }, "value": "[variables('aiFoundryAiProjectEndpoint')]" }, "AZURE_AI_AGENT_ENDPOINT": { "type": "string", + "metadata": { + "description": "Alias for AZURE_AI_PROJECT_ENDPOINT — kept for backward compatibility with the agent SDK." + }, "value": "[variables('aiFoundryAiProjectEndpoint')]" }, "AI_SERVICE_NAME": { "type": "string", + "metadata": { + "description": "The name of the AI Foundry / AI Services account hosting the project." + }, "value": "[variables('aiFoundryAiServicesResourceName')]" }, "AZURE_STORAGE_CONTAINER_NAME_RETAIL_CUSTOMER": { "type": "string", + "metadata": { + "description": "Storage container name for retail customer data." + }, "value": "[parameters('storageContainerNameRetailCustomer')]" }, "AZURE_STORAGE_CONTAINER_NAME_RETAIL_ORDER": { "type": "string", + "metadata": { + "description": "Storage container name for retail order data." + }, "value": "[parameters('storageContainerNameRetailOrder')]" }, "AZURE_STORAGE_CONTAINER_NAME_RFP_SUMMARY": { "type": "string", + "metadata": { + "description": "Storage container name for RFP summary documents." + }, "value": "[parameters('storageContainerNameRFPSummary')]" }, "AZURE_STORAGE_CONTAINER_NAME_RFP_RISK": { "type": "string", + "metadata": { + "description": "Storage container name for RFP risk documents." + }, "value": "[parameters('storageContainerNameRFPRisk')]" }, "AZURE_STORAGE_CONTAINER_NAME_RFP_COMPLIANCE": { "type": "string", + "metadata": { + "description": "Storage container name for RFP compliance documents." + }, "value": "[parameters('storageContainerNameRFPCompliance')]" }, "AZURE_STORAGE_CONTAINER_NAME_CONTRACT_SUMMARY": { "type": "string", + "metadata": { + "description": "Storage container name for contract summary documents." + }, "value": "[parameters('storageContainerNameContractSummary')]" }, "AZURE_STORAGE_CONTAINER_NAME_CONTRACT_RISK": { "type": "string", + "metadata": { + "description": "Storage container name for contract risk documents." + }, "value": "[parameters('storageContainerNameContractRisk')]" }, "AZURE_STORAGE_CONTAINER_NAME_CONTRACT_COMPLIANCE": { "type": "string", + "metadata": { + "description": "Storage container name for contract compliance documents." + }, "value": "[parameters('storageContainerNameContractCompliance')]" }, "AZURE_AI_SEARCH_INDEX_NAME_RETAIL_CUSTOMER": { "type": "string", + "metadata": { + "description": "AI Search index name for retail customer data." + }, "value": "[variables('aiSearchIndexNameForRetailCustomer')]" }, "AZURE_AI_SEARCH_INDEX_NAME_RETAIL_ORDER": { "type": "string", + "metadata": { + "description": "AI Search index name for retail order data." + }, "value": "[variables('aiSearchIndexNameForRetailOrder')]" }, "AZURE_AI_SEARCH_INDEX_NAME_RFP_SUMMARY": { "type": "string", + "metadata": { + "description": "AI Search index name for RFP summary documents." + }, "value": "[variables('aiSearchIndexNameForRFPSummary')]" }, "AZURE_AI_SEARCH_INDEX_NAME_RFP_RISK": { "type": "string", + "metadata": { + "description": "AI Search index name for RFP risk documents." + }, "value": "[variables('aiSearchIndexNameForRFPRisk')]" }, "AZURE_AI_SEARCH_INDEX_NAME_RFP_COMPLIANCE": { "type": "string", + "metadata": { + "description": "AI Search index name for RFP compliance documents." + }, "value": "[variables('aiSearchIndexNameForRFPCompliance')]" }, "AZURE_AI_SEARCH_INDEX_NAME_CONTRACT_SUMMARY": { "type": "string", + "metadata": { + "description": "AI Search index name for contract summary documents." + }, "value": "[variables('aiSearchIndexNameForContractSummary')]" }, "AZURE_AI_SEARCH_INDEX_NAME_CONTRACT_RISK": { "type": "string", + "metadata": { + "description": "AI Search index name for contract risk documents." + }, "value": "[variables('aiSearchIndexNameForContractRisk')]" }, "AZURE_AI_SEARCH_INDEX_NAME_CONTRACT_COMPLIANCE": { "type": "string", + "metadata": { + "description": "AI Search index name for contract compliance documents." + }, "value": "[variables('aiSearchIndexNameForContractCompliance')]" + }, + "AZURE_CONTAINER_REGISTRY_ENDPOINT": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Login server (endpoint) of the Azure Container Registry. Only populated when isCustom is true." + }, + "value": "[if(parameters('isCustom'), reference('container_registry').outputs.loginServer.value, null())]" + }, + "AZURE_CONTAINER_REGISTRY_NAME": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Name of the Azure Container Registry. Only populated when isCustom is true." + }, + "value": "[if(parameters('isCustom'), reference('container_registry').outputs.name.value, null())]" } } } \ No newline at end of file diff --git a/infra/main.bicep b/infra/main.bicep index ae2e16f0a..63ceeae07 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -152,7 +152,7 @@ param backendContainerRegistryHostname string = 'biabcontainerreg.azurecr.io' param backendContainerImageName string = 'macaebackend' @description('Optional. The Container Image Tag to deploy on the backend.') -param backendContainerImageTag string = 'latest_v4' +param backendContainerImageTag string = 'latest_v5' @description('Optional. The Container Registry hostname where the docker images for the frontend are located.') param frontendContainerRegistryHostname string = 'biabcontainerreg.azurecr.io' @@ -161,7 +161,7 @@ param frontendContainerRegistryHostname string = 'biabcontainerreg.azurecr.io' param frontendContainerImageName string = 'macaefrontend' @description('Optional. The Container Image Tag to deploy on the frontend.') -param frontendContainerImageTag string = 'latest_v4' +param frontendContainerImageTag string = 'latest_v5' @description('Optional. The Container Registry hostname where the docker images for the MCP are located.') param MCPContainerRegistryHostname string = 'biabcontainerreg.azurecr.io' @@ -170,7 +170,7 @@ param MCPContainerRegistryHostname string = 'biabcontainerreg.azurecr.io' param MCPContainerImageName string = 'macaemcp' @description('Optional. The Container Image Tag to deploy on the MCP.') -param MCPContainerImageTag string = 'latest_v4' +param MCPContainerImageTag string = 'latest_v5' // ============================================================================ // Parameters — Existing Resources and Governance diff --git a/infra/main.json b/infra/main.json index 05a38286b..722afb1ed 100644 --- a/infra/main.json +++ b/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "2385366416019154937" + "templateHash": "11285983946751655624" }, "name": "Multi-Agent Custom Automation Engine - Deployment Router", "description": "Deployment router for the Multi-Agent Custom Automation Engine accelerator. Routes to either the AVM or vanilla Bicep orchestrator and preserves a unified deployment contract." @@ -245,7 +245,7 @@ }, "backendContainerImageTag": { "type": "string", - "defaultValue": "latest_v4", + "defaultValue": "latest_v5", "metadata": { "description": "Optional. The Container Image Tag to deploy on the backend." } @@ -266,7 +266,7 @@ }, "frontendContainerImageTag": { "type": "string", - "defaultValue": "latest_v4", + "defaultValue": "latest_v5", "metadata": { "description": "Optional. The Container Image Tag to deploy on the frontend." } @@ -287,7 +287,7 @@ }, "MCPContainerImageTag": { "type": "string", - "defaultValue": "latest_v4", + "defaultValue": "latest_v5", "metadata": { "description": "Optional. The Container Image Tag to deploy on the MCP." } @@ -598,7 +598,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "4988818440358366122" + "templateHash": "8714866940320286579" }, "name": "Multi-Agent Custom Automation Engine - AVM", "description": "AVM orchestrator for the Multi-Agent Custom Automation Engine accelerator. Deploys the same logical resources and preserves the same outputs as infra\\main.bicep using local AVM wrapper modules." @@ -848,7 +848,7 @@ }, "backendContainerImageTag": { "type": "string", - "defaultValue": "latest_v4", + "defaultValue": "latest_v5", "metadata": { "description": "Optional. The Container Image Tag to deploy on the backend." } @@ -869,7 +869,7 @@ }, "frontendContainerImageTag": { "type": "string", - "defaultValue": "latest_v4", + "defaultValue": "latest_v5", "metadata": { "description": "Optional. The Container Image Tag to deploy on the frontend." } @@ -890,7 +890,7 @@ }, "MCPContainerImageTag": { "type": "string", - "defaultValue": "latest_v4", + "defaultValue": "latest_v5", "metadata": { "description": "Optional. The Container Image Tag to deploy on the MCP." } @@ -72710,7 +72710,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "5034404004928514756" + "templateHash": "4324294973711463501" }, "name": "Multi-Agent Custom Automation Engine - Vanilla Bicep", "description": "Vanilla Bicep orchestrator for the Multi-Agent Custom Automation Engine accelerator. This deployment intentionally excludes WAF features such as private networking, scale-out, redundancy, bastion, and VM resources while keeping router-compatible outputs." @@ -72936,7 +72936,7 @@ }, "backendContainerImageTag": { "type": "string", - "defaultValue": "latest_v4", + "defaultValue": "latest_v5", "metadata": { "description": "Optional. The Container Image Tag to deploy on the backend." } @@ -72957,7 +72957,7 @@ }, "frontendContainerImageTag": { "type": "string", - "defaultValue": "latest_v4", + "defaultValue": "latest_v5", "metadata": { "description": "Optional. The Container Image Tag to deploy on the frontend." } @@ -72978,7 +72978,7 @@ }, "MCPContainerImageTag": { "type": "string", - "defaultValue": "latest_v4", + "defaultValue": "latest_v5", "metadata": { "description": "Optional. The Container Image Tag to deploy on the MCP." } diff --git a/infra/main.parameters.json b/infra/main.parameters.json index 76e6612b9..860d98dbf 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -75,19 +75,19 @@ "value": "${AZURE_ENV_CONTAINER_REGISTRY_ENDPOINT}" }, "backendContainerImageTag": { - "value": "${AZURE_ENV_IMAGE_TAG=latest_v4}" + "value": "${AZURE_ENV_IMAGE_TAG=latest_v5}" }, "frontendContainerRegistryHostname": { "value": "${AZURE_ENV_CONTAINER_REGISTRY_ENDPOINT}" }, "frontendContainerImageTag": { - "value": "${AZURE_ENV_IMAGE_TAG=latest_v4}" + "value": "${AZURE_ENV_IMAGE_TAG=latest_v5}" }, "MCPContainerRegistryHostname": { "value": "${AZURE_ENV_CONTAINER_REGISTRY_ENDPOINT}" }, "MCPContainerImageTag": { - "value": "${AZURE_ENV_IMAGE_TAG=latest_v4}" + "value": "${AZURE_ENV_IMAGE_TAG=latest_v5}" } } } \ No newline at end of file diff --git a/infra/main.waf.parameters.json b/infra/main.waf.parameters.json index c5fdfd2df..33d14d3a5 100644 --- a/infra/main.waf.parameters.json +++ b/infra/main.waf.parameters.json @@ -102,13 +102,13 @@ "value": "${AZURE_ENV_CONTAINER_REGISTRY_ENDPOINT}" }, "backendContainerImageTag": { - "value": "${AZURE_ENV_IMAGE_TAG=latest_v4}" + "value": "${AZURE_ENV_IMAGE_TAG=latest_v5}" }, "frontendContainerImageTag": { - "value": "${AZURE_ENV_IMAGE_TAG=latest_v4}" + "value": "${AZURE_ENV_IMAGE_TAG=latest_v5}" }, "MCPContainerImageTag": { - "value": "${AZURE_ENV_IMAGE_TAG=latest_v4}" + "value": "${AZURE_ENV_IMAGE_TAG=latest_v5}" } } } \ No newline at end of file diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index 7d2a43c47..d75c72d7f 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -150,7 +150,7 @@ param backendContainerRegistryHostname string = 'biabcontainerreg.azurecr.io' param backendContainerImageName string = 'macaebackend' @description('Optional. The Container Image Tag to deploy on the backend.') -param backendContainerImageTag string = 'latest_v4' +param backendContainerImageTag string = 'latest_v5' @description('Optional. The Container Registry hostname where the docker images for the frontend are located.') param frontendContainerRegistryHostname string = 'biabcontainerreg.azurecr.io' @@ -159,7 +159,7 @@ param frontendContainerRegistryHostname string = 'biabcontainerreg.azurecr.io' param frontendContainerImageName string = 'macaefrontend' @description('Optional. The Container Image Tag to deploy on the frontend.') -param frontendContainerImageTag string = 'latest_v4' +param frontendContainerImageTag string = 'latest_v5' @description('Optional. The Container Registry hostname where the docker images for the MCP are located.') param MCPContainerRegistryHostname string = 'biabcontainerreg.azurecr.io' @@ -168,7 +168,7 @@ param MCPContainerRegistryHostname string = 'biabcontainerreg.azurecr.io' param MCPContainerImageName string = 'macaemcp' @description('Optional. The Container Image Tag to deploy on the MCP.') -param MCPContainerImageTag string = 'latest_v4' +param MCPContainerImageTag string = 'latest_v5' // ============================================================================ // Parameters — Existing Resources and Governance