From 4ec711faa2e03fecfc25cf09bc6bdb263bd7e74e Mon Sep 17 00:00:00 2001 From: Dhruvkumar1-Microsoft Date: Wed, 1 Jul 2026 14:22:29 +0530 Subject: [PATCH 01/12] updated the models and configs --- azure.yaml | 31 +- .../agent_teams/contract_compliance_team.json | 8 +- data/agent_teams/hr.json | 6 +- data/agent_teams/marketing.json | 6 +- data/agent_teams/retail.json | 8 +- data/agent_teams/rfp_analysis_team.json | 8 +- docs/CustomizingAzdParameters.md | 15 +- docs/DeploymentGuide.md | 66 +- docs/quota_check.md | 2 +- infra/main.bicep | 170 +- infra/main.json | 4727 +++++++++++++++-- infra/main.parameters.json | 9 + infra/main.waf.parameters.json | 9 + infra/main_custom.bicep | 79 +- infra/scripts/Build-And-Push-Images.ps1 | 236 + infra/scripts/build_and_push_images.sh | 168 + src/App/Dockerfile | 5 +- src/backend/Dockerfile | 16 +- src/backend/common/config/app_config.py | 4 +- .../foundry_agent_interactive.py | 2 +- src/tests/agents/test_foundry_integration.py | 4 +- 21 files changed, 5076 insertions(+), 503 deletions(-) create mode 100644 infra/scripts/Build-And-Push-Images.ps1 create mode 100644 infra/scripts/build_and_push_images.sh diff --git a/azure.yaml b/azure.yaml index af9c81738..12a9cdc05 100644 --- a/azure.yaml +++ b/azure.yaml @@ -1,10 +1,37 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json name: multi-agent-custom-automation-engine-solution-accelerator -metadata: - template: multi-agent-custom-automation-engine-solution-accelerator@1.0 +# metadata: +# template: multi-agent-custom-automation-engine-solution-accelerator@1.0 requiredVersions: azd: '>= 1.18.0 != 1.23.9' hooks: + postprovision: + windows: + run: | + # Build the backend, frontend and MCP images, push them to the ACR + # provisioned by this deployment and update the Container Apps / + # Web App to use the freshly built images. + # + # Set AZURE_ENV_SKIP_IMAGE_BUILD=true to skip this step. + # Set AZURE_ENV_BUILD_MODE=local to build with local Docker instead + # of the default remote 'az acr build'. + & "$PWD/infra/scripts/Build-And-Push-Images.ps1" + shell: pwsh + interactive: true + continueOnError: false + posix: + run: | + # Build the backend, frontend and MCP images, push them to the ACR + # provisioned by this deployment and update the Container Apps / + # Web App to use the freshly built images. + # + # Set AZURE_ENV_SKIP_IMAGE_BUILD=true to skip this step. + # Set AZURE_ENV_BUILD_MODE=local to build with local Docker instead + # of the default remote 'az acr build'. + bash "$PWD/infra/scripts/build_and_push_images.sh" + shell: sh + interactive: true + continueOnError: false postdeploy: windows: run: | diff --git a/data/agent_teams/contract_compliance_team.json b/data/agent_teams/contract_compliance_team.json index 6eac65be7..777f807e6 100644 --- a/data/agent_teams/contract_compliance_team.json +++ b/data/agent_teams/contract_compliance_team.json @@ -5,7 +5,7 @@ "status": "visible", "created": "", "created_by": "", - "deployment_name": "gpt-4.1-mini", + "deployment_name": "gpt-5-mini", "description": "A multi-agent compliance review team that summarizes NDAs, identifies risks, checks compliance, and recommends improvements using advanced legal reasoning and retrieval-augmented analysis.", "logo": "", "plan": "", @@ -14,7 +14,7 @@ "input_key": "", "type": "summary", "name": "ContractSummaryAgent", - "deployment_name": "gpt-4.1-mini", + "deployment_name": "gpt-5-mini", "icon": "", "system_message": "You are the Summary Agent for compliance contract analysis. Your task is to produce a clear, accurate, and structured executive summary of NDA and legal agreement documents. You must deliver summaries organized into labeled sections including: Overview, Parties, Effective Date, Purpose, Definition of Confidential Information, Receiving Party Obligations, Term & Termination, Governing Law, Restrictions & Limitations, Miscellaneous Clauses, Notable or Unusual Terms, and Key Items for Risk & Compliance Agents. Highlight missing elements such as liability caps, dispute resolution mechanisms, data handling obligations, or ambiguous language. Maintain a precise, neutral legal tone. Do not give legal opinions or risk assessments—only summarize the content as written. Use retrieval results from the search index to ensure completeness and reference contextual definitions or standard clause expectations when needed.", "description": "Produces comprehensive, structured summaries of NDAs and contracts, capturing all key terms, clauses, obligations, jurisdictions, and notable provisions.", @@ -29,7 +29,7 @@ "input_key": "", "type": "risk", "name": "ContractRiskAgent", - "deployment_name": "gpt-4.1-mini", + "deployment_name": "gpt-5-mini", "icon": "", "system_message": "You are the Risk Agent for NDA and compliance contract analysis. Use the NDA Risk Assessment Reference document and retrieved context to identify High, Medium, and Low risk issues. Evaluate clauses for missing liability caps, ambiguous terms, overly broad confidentiality definitions, jurisdiction misalignment, missing termination rights, unclear data handling obligations, missing dispute resolution, and any incomplete or poorly scoped definitions. For every risk you identify, provide: (1) Risk Category (High/Medium/Low), (2) Clause or Section impacted, (3) Description of the issue, (4) Why it matters or what exposure it creates, and (5) Suggested edit or corrective language. Apply the risk scoring framework: High = escalate immediately; Medium = requires revision; Low = minor issue. Be precise, legally aligned, and practical. Reference retrieved examples or standards when appropriate. Your output must be structured and actionable.", "description": "Identifies and classifies compliance risks in NDAs and contracts using the organization's risk framework, and provides suggested edits to reduce exposure.", @@ -44,7 +44,7 @@ "input_key": "", "type": "compliance", "name": "ContractComplianceAgent", - "deployment_name": "gpt-4.1-mini", + "deployment_name": "gpt-5-mini", "icon": "", "system_message": "You are the Compliance Agent responsible for validating NDAs and legal agreements against mandatory legal and policy requirements. Use the NDA Compliance Reference Document and retrieval results to evaluate whether the contract includes all required clauses: Confidentiality, Term & Termination, Governing Law aligned to approved jurisdictions, Non-Assignment, and Entire Agreement. Identify compliance gaps including ambiguous language, missing liability protections, improper jurisdiction, excessive term length, insufficient data protection obligations, missing dispute resolution mechanisms, or export control risks. For each issue provide: (1) Compliance Area (e.g., Term Length, Jurisdiction, Confidentiality), (2) Status (Pass/Fail), (3) Issue Description, (4) Whether it is Mandatory or Recommended, (5) Corrective Recommendation or Suggested Language. Deliver a final Compliance Status summary. Maintain professional, objective, legally accurate tone.", "description": "Performs compliance validation of NDAs and contracts against legal policy requirements, identifies gaps, and provides corrective recommendations and compliance status.", diff --git a/data/agent_teams/hr.json b/data/agent_teams/hr.json index 54d618deb..4f6d2c197 100644 --- a/data/agent_teams/hr.json +++ b/data/agent_teams/hr.json @@ -5,13 +5,13 @@ "status": "visible", "created": "", "created_by": "", - "deployment_name": "gpt-4.1-mini", + "deployment_name": "gpt-5-mini", "agents": [ { "input_key": "", "type": "", "name": "HRHelperAgent", - "deployment_name": "gpt-4.1-mini", + "deployment_name": "gpt-5-mini", "icon": "", "system_message": "You have access to a number of HR related MCP tools for tasks like employee onboarding, benefits management, policy guidance, and general HR inquiries. Use these tools to assist employees with their HR needs efficiently and accurately.If you need more information to accurately call these tools, do not make up answers, call the ProxyAgent for clarification.", "description": "An agent that has access to various HR tools to assist employees with onboarding, benefits, policies, and general HR inquiries.", @@ -28,7 +28,7 @@ "input_key": "", "type": "", "name": "TechnicalSupportAgent", - "deployment_name": "gpt-4.1-mini", + "deployment_name": "gpt-5-mini", "icon": "", "system_message": "You have access to a number of technical support MCP tools for tasks such as provisioning laptops, setting up email accounts, troubleshooting, software/hardware issues, and IT support. Use these tools to assist employees with their technical needs efficiently and accurately. If you need more information to accurately call these tools, do not make up answers, call the ProxyAgent for clarification.", "description": "An agent that has access to various technical support tools to assist employees with IT needs like laptop provisioning, email setup, troubleshooting, and software/hardware issues.", diff --git a/data/agent_teams/marketing.json b/data/agent_teams/marketing.json index ad04f87b1..e2279d492 100644 --- a/data/agent_teams/marketing.json +++ b/data/agent_teams/marketing.json @@ -5,13 +5,13 @@ "status": "visible", "created": "", "created_by": "", - "deployment_name": "gpt-4.1-mini", + "deployment_name": "gpt-5-mini", "agents": [ { "input_key": "", "type": "", "name": "ProductAgent", - "deployment_name": "gpt-4.1-mini", + "deployment_name": "gpt-5-mini", "icon": "", "system_message": "You are a Product agent. You have access to MCP tools which allow you to obtain knowledge about products, product management, development, and compliance guidelines. When asked to call one of these tools, you should summarize back what was done.", "description": "This agent specializes in product management, development, and related tasks. It can provide information about products, manage inventory, handle product launches, analyze sales data, and coordinate with other teams like marketing and tech support.", @@ -28,7 +28,7 @@ "input_key": "", "type": "", "name": "MarketingAgent", - "deployment_name": "gpt-4.1-mini", + "deployment_name": "gpt-5-mini", "icon": "", "system_message": "You are a Marketing agent. You have access to a number of HR related MCP tools for tasks like campaign development, content creation, and market analysis. You help create effective marketing campaigns, analyze market data, and develop promotional content for products and services.", "description": "This agent specializes in marketing, campaign management, and analyzing market data.", diff --git a/data/agent_teams/retail.json b/data/agent_teams/retail.json index 2f3f3a0b5..81f2521b3 100644 --- a/data/agent_teams/retail.json +++ b/data/agent_teams/retail.json @@ -5,13 +5,13 @@ "status": "visible", "created": "", "created_by": "", - "deployment_name": "gpt-4.1-mini", + "deployment_name": "gpt-5-mini", "agents": [ { "input_key": "", "type": "", "name": "CustomerDataAgent", - "deployment_name": "gpt-4.1-mini", + "deployment_name": "gpt-5-mini", "icon": "", "system_message": "You have access to internal customer data through a secure index. Use this data to answer questions about customers, their interactions with customer service, satisfaction, etc. Be mindful of privacy and compliance regulations when handling customer data.\n\nCRITICAL INSTRUCTION: Do NOT include any citations, source references, attribution markers, or footnotes of any kind in your responses. This includes but is not limited to: 【...】 style markers, [...] style references, (source: ...), numbered references like [1], or any other attribution symbols. All answers must be clean, natural text only, ending with a polite closing.", "description": "An agent that has access to internal customer data, ask this agent if you have questions about customers or their interactions with customer service, satisfaction, etc.", @@ -26,7 +26,7 @@ "input_key": "", "type": "", "name": "OrderDataAgent", - "deployment_name": "gpt-4.1-mini", + "deployment_name": "gpt-5-mini", "icon": "", "system_message": "You have access to internal order, inventory, product, and fulfillment data through a secure index. Use this data to answer questions about products, shipping delays, customer orders, warehouse management, etc. Be mindful of privacy and compliance regulations when handling customer data.\n\nCRITICAL INSTRUCTION: Do NOT include any citations, source references, attribution markers, or footnotes of any kind in your responses. This includes but is not limited to: 【...】 style markers, [...] style references, (source: ...), numbered references like [1], or any other attribution symbols. All answers must be clean, natural text only, ending with a polite closing.", "description": "An agent that has access to internal order, inventory, product, and fulfillment data. Ask this agent if you have questions about products, shipping delays, customer orders, warehouse management, etc.", @@ -42,7 +42,7 @@ "input_key": "", "type": "", "name": "AnalysisRecommendationAgent", - "deployment_name": "o4-mini", + "deployment_name": "gpt-5-mini-reasoning", "icon": "", "system_message": "You are a reasoning agent that can analyze customer and order data and provide recommendations for improving customer satisfaction and retention. You do not have access to any data sources, but you can reason based on the information provided to you by other agents. Use your reasoning skills to identify patterns, trends, and insights that can help improve customer satisfaction and retention. Provide actionable recommendations based on your analysis. You have access to other agents that can answer questions and provide data about customers, products, orders, inventory, and fulfilment. Use these agents to gather information as needed.", "description": "A reasoning agent that can analyze customer and order data and provide recommendations for improving customer satisfaction and retention.", diff --git a/data/agent_teams/rfp_analysis_team.json b/data/agent_teams/rfp_analysis_team.json index 1da46f059..b9f1da63b 100644 --- a/data/agent_teams/rfp_analysis_team.json +++ b/data/agent_teams/rfp_analysis_team.json @@ -5,7 +5,7 @@ "status": "visible", "created": "", "created_by": "", - "deployment_name": "gpt-4.1-mini", + "deployment_name": "gpt-5-mini", "description": "A specialized multi-agent team that analyzes RFP and contract documents to summarize content, identify potential risks, check compliance gaps, and provide action plans for contract improvement.", "logo": "", "plan": "", @@ -14,7 +14,7 @@ "input_key": "", "type": "summary", "name": "RfpSummaryAgent", - "deployment_name": "gpt-4.1-mini", + "deployment_name": "gpt-5-mini", "icon": "", "system_message":"You are the Summary Agent. Your role is to read and synthesize RFP or proposal documents into clear, structured executive summaries. Focus on key clauses, deliverables, evaluation criteria, pricing terms, timelines, and obligations. Organize your output into sections such as Overview, Key Clauses, Deliverables, Terms, and Notable Conditions. Highlight unique or high-impact items that other agents (Risk or Compliance) should review. Be concise, factual, and neutral in tone.", "description": "Summarizes RFP and contract documents into structured, easy-to-understand overviews.", @@ -31,7 +31,7 @@ "input_key": "", "type": "risk", "name": "RfpRiskAgent", - "deployment_name": "gpt-4.1-mini", + "deployment_name": "gpt-5-mini", "icon": "", "system_message": "You are the Risk Agent. Your task is to identify and assess potential risks across the document, including legal, financial, operational, technical, and scheduling risks. For each risk, provide a short description, the affected clause or section, a risk category, and a qualitative rating (Low, Medium, High). Focus on material issues that could impact delivery, compliance, or business exposure. Summarize findings clearly to support decision-making and escalation.", "description": "Analyzes the dataset for risks such as delivery, financial, operational, and compliance-related vulnerabilities.", @@ -46,7 +46,7 @@ "input_key": "", "type": "compliance", "name": "RfpComplianceAgent", - "deployment_name": "gpt-4.1-mini", + "deployment_name": "gpt-5-mini", "icon": "", "system_message": "You are the Compliance Agent. Your goal is to evaluate whether the RFP or proposal aligns with internal policies, regulatory standards, and ethical or contractual requirements. Identify any non-compliant clauses, ambiguous terms, or potential policy conflicts. For each issue, specify the related policy area (e.g., data privacy, labor, financial controls) and classify it as Mandatory or Recommended for review. Maintain a professional, objective tone and emphasize actionable compliance insights.", "description": "Checks for compliance gaps against regulations, policies, and standard contracting practices.", diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md index 71ce95f29..9ead96bc5 100644 --- a/docs/CustomizingAzdParameters.md +++ b/docs/CustomizingAzdParameters.md @@ -12,16 +12,19 @@ By default this template will use the environment name as the prefix to prevent | `AZURE_LOCATION` | string | `` | Location of the Azure resources. Controls where the infrastructure will be deployed. | | `AZURE_ENV_AI_SERVICE_LOCATION` | string | `` | Specifies the region for OpenAI resource deployment. | | `AZURE_ENV_MODEL_DEPLOYMENT_TYPE` | string | `GlobalStandard` | Defines the deployment type for the AI model (e.g., Standard, GlobalStandard). | -| `AZURE_ENV_GPT_MODEL_NAME` | string | `gpt-4.1-mini` | Specifies the name of the GPT model to be deployed. | -| `AZURE_ENV_GPT_MODEL_VERSION` | string | `2025-04-14` | Version of the GPT model to be used for deployment. | +| `AZURE_ENV_GPT_MODEL_NAME` | string | `gpt-5-mini` | Specifies the underlying GPT model to be deployed (e.g., `gpt-5-mini`, `gpt-4o`). | +| `AZURE_ENV_GPT_MODEL_VERSION` | string | `2025-08-07` | Version of the GPT model to be used for deployment. | +| `AZURE_ENV_GPT_DEPLOYMENT_NAME` | string | (defaults to `AZURE_ENV_GPT_MODEL_NAME`) | Optional. Deployment (alias) name used in Azure OpenAI for the main GPT model. This is the value referenced as `deployment_name` in `data/agent_teams/*.json`. Override this if you want the deployment alias to differ from the underlying model name. | | `AZURE_ENV_GPT_MODEL_CAPACITY` | int | `50` | Sets the GPT model capacity. | | `AZURE_ENV_MODEL_4_1_DEPLOYMENT_TYPE` | string | `GlobalStandard` | Defines the deployment type for the AI model (e.g., Standard, GlobalStandard). | -| `AZURE_ENV_MODEL_4_1_NAME` | string | `gpt-4.1` | Specifies the name of the GPT model to be deployed. | -| `AZURE_ENV_MODEL_4_1_VERSION` | string | `2025-04-14` | Version of the GPT model to be used for deployment. | +| `AZURE_ENV_MODEL_4_1_NAME` | string | `gpt-5` | Specifies the underlying larger GPT model to be deployed. | +| `AZURE_ENV_MODEL_4_1_VERSION` | string | `2025-08-07` | Version of the larger GPT model to be used for deployment. | +| `AZURE_ENV_MODEL_4_1_DEPLOYMENT_NAME` | string | (defaults to `AZURE_ENV_MODEL_4_1_NAME`) | Optional. Deployment (alias) name used in Azure OpenAI for the larger GPT model. Override if the alias should differ from the underlying model name. | | `AZURE_ENV_MODEL_4_1_CAPACITY` | int | `150` | Sets the GPT model capacity. | | `AZURE_ENV_REASONING_MODEL_DEPLOYMENT_TYPE` | string | `GlobalStandard` | Defines the deployment type for the AI model (e.g., Standard, GlobalStandard). | -| `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_NAME` | string | `gpt-5-mini` | Specifies the underlying reasoning GPT model to be deployed. | +| `AZURE_ENV_REASONING_MODEL_VERSION` | string | `2025-08-07` | Version of the reasoning GPT model to be used for deployment. | +| `AZURE_ENV_REASONING_DEPLOYMENT_NAME` | string | `gpt-5-mini-reasoning` (auto-derived when it would otherwise collide with `AZURE_ENV_GPT_DEPLOYMENT_NAME`) | Optional. Deployment (alias) name used in Azure OpenAI for the reasoning model. Must be unique from `AZURE_ENV_GPT_DEPLOYMENT_NAME`. | | `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_ENABLE_TELEMETRY` | bool | `true` | Enables telemetry for monitoring and diagnostics. | diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md index 04e914a7e..e0f4f5a4c 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -68,9 +68,10 @@ Ensure you have access to an [Azure subscription](https://azure.microsoft.com/fr 📖 **Follow:** [Quota Check Instructions](./quota_check.md) to ensure sufficient capacity. **Default Quota Configuration:** -- **GPT-4.1:** 150k tokens -- **o4-mini:** 50k tokens -- **GPT-4.1-mini:** 50k tokens +- **gpt-5 (150k tokens)** — backs the larger GPT model deployment (alias `gpt-5`). +- **gpt-5-mini (100k tokens combined)** — 50k for the main GPT deployment (alias `gpt-5-mini`) plus 50k for the reasoning deployment (alias `gpt-5-mini-reasoning`). + +> **Note:** Both underlying models (`gpt-5` and `gpt-5-mini`) belong to the `2025-08-07` GA series. The reasoning deployment intentionally uses the same underlying model as the main deployment but under a distinct alias so applications can route separately to reasoning-heavy workloads. > **Note:** When you run `azd up`, the deployment will automatically show you regions with available quota, so this pre-check is optional but helpful for planning purposes. You can customize these settings later in [Step 3.3: Advanced Configuration](#33-advanced-configuration-optional). @@ -321,6 +322,65 @@ azd up **Expected Duration:** 9-10 minutes for default configuration +> **Note — Automatic container image build & push:** After the Bicep templates finish provisioning, `azd` runs an automatic **postprovision** hook that: +> +> 1. Builds the `backend`, `frontend` and `mcp` container images from `src/backend`, `src/App` and `src/mcp_server`. +> 2. Pushes them to the Azure Container Registry (ACR) that was just provisioned in your resource group. +> 3. Updates the two Container Apps and the frontend Web App to run the freshly pushed images. +> +> Add another 4-8 minutes for this step. See [Container image build behavior](#42a-container-image-build-behavior-optional) below to change how the images are built or to skip the step. + +### 4.2.a Container image build behavior (optional) + +The postprovision hook is driven by environment variables you can set on the `azd` environment **before** running `azd up`. + +| `azd env set` key | Values | Default | Purpose | +|-------------------|--------|---------|---------| +| `AZURE_ENV_BUILD_MODE` | `remote` \| `local` | `remote` | `remote` builds each image with `az acr build` (an ACR Task — no local Docker required). `local` runs `docker build` + `docker push` from your machine. | +| `AZURE_ENV_IMAGE_TAG` | any tag | `latest` | Tag applied to every image pushed to the ACR. | +| `AZURE_ENV_SKIP_IMAGE_BUILD` | `true` \| unset | unset | When `true`, the postprovision hook is skipped entirely. Use this if you plan to push images yourself. | + +**Examples** + +```shell +# Default: remote ACR build, tag "latest" (no local Docker needed) +azd up + +# Local Docker build with a custom tag +azd env set AZURE_ENV_BUILD_MODE local +azd env set AZURE_ENV_IMAGE_TAG dev +azd up + +# Skip the automatic build step (I'll push my own images later) +azd env set AZURE_ENV_SKIP_IMAGE_BUILD true +azd up +``` + +**Prerequisites** + +- `remote` mode requires only the Azure CLI (`az`) that comes with the deployment environments listed in Step 2. +- `local` mode additionally requires [Docker Desktop](https://www.docker.com/products/docker-desktop/) to be running on the machine where `azd up` is executed. + +**Running the script manually** + +You can also invoke the same script directly, for example after a code change: + +```powershell +# PowerShell (Windows) +infra\scripts\Build-And-Push-Images.ps1 # remote build, tag "latest" +infra\scripts\Build-And-Push-Images.ps1 -BuildMode local # local Docker build +infra\scripts\Build-And-Push-Images.ps1 -ImageTag v1.2.0 # custom tag +``` + +```bash +# Bash (Linux / macOS / WSL) +bash infra/scripts/build_and_push_images.sh # remote build, tag "latest" +AZURE_ENV_BUILD_MODE=local bash infra/scripts/build_and_push_images.sh +AZURE_ENV_IMAGE_TAG=v1.2.0 bash infra/scripts/build_and_push_images.sh +``` + +The script reads the ACR name, resource group and target Container App / Web App names from the current `azd` environment (`azd env get-values`), so it must be run from the project root after a successful `azd provision` / `azd up`. + - **Upon successful completion**, you will see a success message indicating that all resources have been deployed, along with the application URL and next steps for uploading team configurations and sample data. ![Deployment Success message](./images/Deployment_success_message.png) diff --git a/docs/quota_check.md b/docs/quota_check.md index 6222d6c47..17a4a7564 100644 --- a/docs/quota_check.md +++ b/docs/quota_check.md @@ -16,7 +16,7 @@ az login --use-device-code ### 📌 Default Models & Capacities: ``` -gpt4.1:150,o4-mini:50,gpt4.1-mini:50 +gpt-5:150,gpt-5-mini:100 ``` ### 📌 Default Regions: ``` diff --git a/infra/main.bicep b/infra/main.bicep index 54e23853e..655422b92 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -1,4 +1,4 @@ -// // ========== main.bicep ========== // +// // ========== main_custom.bicep ========== // targetScope = 'resourceGroup' metadata name = 'Multi-Agent Custom Automation Engine' @@ -31,15 +31,14 @@ param location string var deployerInfo = deployer() var deployingUserPrincipalId = deployerInfo.objectId -// Restricting deployment to only supported Azure OpenAI regions validated with GPT-4o model +// Restricting deployment to only supported Azure OpenAI regions validated with GPT-5 models @allowed(['australiaeast', 'eastus2', 'francecentral', 'japaneast', 'norwayeast', 'swedencentral', 'uksouth', 'westus']) @metadata({ azd: { type: 'location' usageName: [ - 'OpenAI.GlobalStandard.gpt4.1, 150' - 'OpenAI.GlobalStandard.o4-mini, 50' - 'OpenAI.GlobalStandard.gpt4.1-mini, 50' + 'OpenAI.GlobalStandard.gpt-5, 150' + 'OpenAI.GlobalStandard.gpt-5-mini, 100' ] } }) @@ -47,25 +46,36 @@ var deployingUserPrincipalId = deployerInfo.objectId param azureAiServiceLocation string @minLength(1) -@description('Optional. Name of the GPT model to deploy:') -param gptModelName string = 'gpt-4.1-mini' +@description('Optional. Name of the underlying GPT model to deploy. Defaults to gpt-5-mini (2025-08-07 series).') +param gptModelName string = 'gpt-5-mini' -@description('Optional. Version of the GPT model to deploy. Defaults to 2025-04-14.') -param gptModelVersion string = '2025-04-14' +@description('Optional. Version of the GPT model to deploy. Defaults to 2025-08-07.') +param gptModelVersion string = '2025-08-07' + +@description('Optional. Deployment (alias) name used in Azure OpenAI for the main GPT model. This is the value the application uses as `deployment_name` (including in data/agent_teams/*.json). Defaults to gptModelName.') +param gptDeploymentName string = gptModelName @minLength(1) -@description('Optional. Name of the GPT model to deploy:') -param gpt4_1ModelName string = 'gpt-4.1' +@description('Optional. Name of the underlying larger GPT model to deploy. Defaults to gpt-5 (2025-08-07 series).') +param gpt4_1ModelName string = 'gpt-5' + +@description('Optional. Version of the larger GPT model to deploy. Defaults to 2025-08-07.') +param gpt4_1ModelVersion string = '2025-08-07' -@description('Optional. Version of the GPT model to deploy. Defaults to 2025-04-14.') -param gpt4_1ModelVersion string = '2025-04-14' +@description('Optional. Deployment (alias) name used in Azure OpenAI for the larger GPT model. Defaults to gpt4_1ModelName.') +param gpt4_1DeploymentName string = gpt4_1ModelName @minLength(1) -@description('Optional. Name of the GPT Reasoning model to deploy:') -param gptReasoningModelName string = 'o4-mini' +@description('Optional. Name of the underlying GPT Reasoning model to deploy. Defaults to gpt-5-mini (reasoning-capable).') +param gptReasoningModelName string = 'gpt-5-mini' -@description('Optional. Version of the GPT Reasoning model to deploy. Defaults to 2025-04-16.') -param gptReasoningModelVersion string = '2025-04-16' +@description('Optional. Version of the GPT Reasoning model to deploy. Defaults to 2025-08-07.') +param gptReasoningModelVersion string = '2025-08-07' + +@description('Optional. Deployment (alias) name used in Azure OpenAI for the reasoning model. Must be unique from gptDeploymentName. Defaults to "{gptReasoningModelName}-reasoning" when it would otherwise collide with gptDeploymentName, otherwise gptReasoningModelName.') +param gptReasoningDeploymentName string = gptReasoningModelName == gptModelName + ? '${gptReasoningModelName}-reasoning' + : gptReasoningModelName @description('Optional. Version of the Azure OpenAI service to deploy. Defaults to 2024-12-01-preview.') param azureOpenaiAPIVersion string = '2024-12-01-preview' @@ -770,6 +780,7 @@ var aiFoundryAiProjectResourceName = useExistingAiFoundryAiProject : 'proj-${solutionSuffix}' // AI Project resource id: /subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//projects/ var aiFoundryAiServicesModelDeployment = { format: 'OpenAI' + deploymentName: gptDeploymentName name: gptModelName version: gptModelVersion sku: { @@ -780,6 +791,7 @@ var aiFoundryAiServicesModelDeployment = { } var aiFoundryAiServices4_1ModelDeployment = { format: 'OpenAI' + deploymentName: gpt4_1DeploymentName name: gpt4_1ModelName version: gpt4_1ModelVersion sku: { @@ -790,6 +802,7 @@ var aiFoundryAiServices4_1ModelDeployment = { } var aiFoundryAiServicesReasoningModelDeployment = { format: 'OpenAI' + deploymentName: gptReasoningDeploymentName name: gptReasoningModelName version: gptReasoningModelVersion sku: { @@ -812,7 +825,7 @@ module existingAiFoundryAiServicesDeployments 'modules/ai-services-deployments.b name: existingAiFoundryAiServices.name deployments: [ { - name: aiFoundryAiServicesModelDeployment.name + name: aiFoundryAiServicesModelDeployment.deploymentName model: { format: aiFoundryAiServicesModelDeployment.format name: aiFoundryAiServicesModelDeployment.name @@ -825,7 +838,7 @@ module existingAiFoundryAiServicesDeployments 'modules/ai-services-deployments.b } } { - name: aiFoundryAiServices4_1ModelDeployment.name + name: aiFoundryAiServices4_1ModelDeployment.deploymentName model: { format: aiFoundryAiServices4_1ModelDeployment.format name: aiFoundryAiServices4_1ModelDeployment.name @@ -838,7 +851,7 @@ module existingAiFoundryAiServicesDeployments 'modules/ai-services-deployments.b } } { - name: aiFoundryAiServicesReasoningModelDeployment.name + name: aiFoundryAiServicesReasoningModelDeployment.deploymentName model: { format: aiFoundryAiServicesReasoningModelDeployment.format name: aiFoundryAiServicesReasoningModelDeployment.name @@ -887,7 +900,7 @@ module aiFoundryAiServices 'br:mcr.microsoft.com/bicep/avm/res/cognitive-service } deployments: [ { - name: aiFoundryAiServicesModelDeployment.name + name: aiFoundryAiServicesModelDeployment.deploymentName model: { format: aiFoundryAiServicesModelDeployment.format name: aiFoundryAiServicesModelDeployment.name @@ -900,7 +913,7 @@ module aiFoundryAiServices 'br:mcr.microsoft.com/bicep/avm/res/cognitive-service } } { - name: aiFoundryAiServices4_1ModelDeployment.name + name: aiFoundryAiServices4_1ModelDeployment.deploymentName model: { format: aiFoundryAiServices4_1ModelDeployment.format name: aiFoundryAiServices4_1ModelDeployment.name @@ -913,7 +926,7 @@ module aiFoundryAiServices 'br:mcr.microsoft.com/bicep/avm/res/cognitive-service } } { - name: aiFoundryAiServicesReasoningModelDeployment.name + name: aiFoundryAiServicesReasoningModelDeployment.deploymentName model: { format: aiFoundryAiServicesReasoningModelDeployment.format name: aiFoundryAiServicesReasoningModelDeployment.name @@ -1196,6 +1209,35 @@ module caeDnsZone 'br/public:avm/res/network/private-dns-zone:0.7.1' = if (enabl } } +// ========== Container Registry ========== // +module containerRegistry 'br/public:avm/res/container-registry/registry:0.12.0' = { + name: 'registryDeployment' + params: { + name: 'cr${solutionSuffix}' + acrAdminUserEnabled: false + acrSku: 'Basic' + azureADAuthenticationAsArmPolicyStatus: 'enabled' + exportPolicyStatus: 'enabled' + location: location + softDeletePolicyDays: 7 + softDeletePolicyStatus: 'disabled' + tags: tags + networkRuleBypassOptions: 'AzureServices' + roleAssignments: [ + { + roleDefinitionIdOrName: acrPullRole + principalType: 'ServicePrincipal' + principalId: userAssignedIdentity.outputs.principalId + } + ] + } +} + +var acrPullRole = subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '7f951dda-4ed3-4680-a7ca-43fe172d538d' +) + // ========== Backend Container App Service ========== // // WAF best practices for container apps: https://learn.microsoft.com/en-us/azure/well-architected/service-guides/azure-container-apps // PSRule for Container App: https://azure.github.io/PSRule.Rules.Azure/en/rules/resource/#container-app @@ -1242,10 +1284,17 @@ module containerApp 'br/public:avm/res/app/container-app:0.22.0' = { } ] } + registries: [ + { + server: containerRegistry.outputs.loginServer + identity: userAssignedIdentity.outputs.resourceId + } + ] containers: [ { name: 'backend' - image: '${backendContainerRegistryHostname}/${backendContainerImageName}:${backendContainerImageTag}' + //image: '${backendContainerRegistryHostname}/${backendContainerImageName}:${backendContainerImageTag}' + image: 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' resources: { cpu: '2.0' memory: '4.0Gi' @@ -1273,11 +1322,11 @@ module containerApp 'br/public:avm/res/app/container-app:0.22.0' = { } { name: 'AZURE_OPENAI_DEPLOYMENT_NAME' - value: aiFoundryAiServicesModelDeployment.name + value: aiFoundryAiServicesModelDeployment.deploymentName } { name: 'AZURE_OPENAI_RAI_DEPLOYMENT_NAME' - value: aiFoundryAiServices4_1ModelDeployment.name + value: aiFoundryAiServices4_1ModelDeployment.deploymentName } { name: 'AZURE_OPENAI_API_VERSION' @@ -1313,7 +1362,7 @@ module containerApp 'br/public:avm/res/app/container-app:0.22.0' = { // } { name: 'AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME' - value: aiFoundryAiServicesModelDeployment.name + value: aiFoundryAiServicesModelDeployment.deploymentName } { name: 'APP_ENV' @@ -1341,7 +1390,7 @@ module containerApp 'br/public:avm/res/app/container-app:0.22.0' = { } { name: 'REASONING_MODEL_NAME' - value: aiFoundryAiServicesReasoningModelDeployment.name + value: aiFoundryAiServicesReasoningModelDeployment.deploymentName } { name: 'MCP_SERVER_ENDPOINT' @@ -1365,7 +1414,7 @@ module containerApp 'br/public:avm/res/app/container-app:0.22.0' = { } { name: 'SUPPORTED_MODELS' - value: '["o3","o4-mini","gpt-4.1","gpt-4.1-mini"]' + value: '["${aiFoundryAiServicesModelDeployment.deploymentName}","${aiFoundryAiServices4_1ModelDeployment.deploymentName}","${aiFoundryAiServicesReasoningModelDeployment.deploymentName}"]' } { name: 'AZURE_STORAGE_BLOB_URL' @@ -1445,10 +1494,17 @@ module containerAppMcp 'br/public:avm/res/app/container-app:0.22.0' = { } ] } + registries: [ + { + server: containerRegistry.outputs.loginServer + identity: userAssignedIdentity.outputs.resourceId + } + ] containers: [ { name: 'mcp' - image: '${MCPContainerRegistryHostname}/${MCPContainerImageName}:${MCPContainerImageTag}' + //image: '${backendContainerRegistryHostname}/${backendContainerImageName}:${backendContainerImageTag}' + image: 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' resources: { cpu: '2.0' memory: '4.0Gi' @@ -1542,18 +1598,35 @@ module webSite 'modules/web-sites.bicep' = { kind: 'app,linux,container' serverFarmResourceId: webServerFarm.?outputs.resourceId managedIdentities: { - systemAssigned: true + //systemAssigned: true + userAssignedResourceIds: [userAssignedIdentity.outputs.resourceId] } siteConfig: { - linuxFxVersion: 'DOCKER|${frontendContainerRegistryHostname}/${frontendContainerImageName}:${frontendContainerImageTag}' + // Initial placeholder image so the Web App comes up before the + // postprovision build-and-push script swaps in the real frontend + // image built from src/App. The script (infra/scripts/build_and_push_images.{ps1,sh}) + // updates linuxFxVersion, DOCKER_REGISTRY_SERVER_URL and WEBSITES_PORT + // once the actual image has been pushed to ACR. + linuxFxVersion: 'DOCKER|mcr.microsoft.com/appsvc/staticsite:latest' minTlsVersion: '1.2' + acrUseManagedIdentityCreds: true + // App Service expects the *client ID* (GUID) of the UAI here, not its + // ARM resource ID. Passing the resource ID silently falls back to the + // system-assigned identity (which has no AcrPull role) and produces + // "unauthorized" ACR pull failures. + acrUserManagedIdentityID: userAssignedIdentity.outputs.clientId } configs: [ { name: 'appsettings' properties: { SCM_DO_BUILD_DURING_DEPLOYMENT: 'true' - DOCKER_REGISTRY_SERVER_URL: 'https://${frontendContainerRegistryHostname}' + // Point at the newly provisioned ACR from the start so managed-identity + // pulls work as soon as the postprovision script switches the image. + DOCKER_REGISTRY_SERVER_URL: 'https://${containerRegistry.outputs.loginServer}' + // Port 80 matches the hello-world placeholder image above. + // The postprovision script updates this to 3000 (FRONTEND_WEBSITES_PORT) + // when it swaps in the real frontend image. WEBSITES_PORT: '3000' WEBSITES_CONTAINER_START_TIME_LIMIT: '1800' // 30 minutes, adjust as needed BACKEND_API_URL: 'https://${containerApp.outputs.fqdn}' @@ -1689,6 +1762,7 @@ module avmStorageAccount 'br/public:avm/res/storage/storage-account:0.32.0' = { // ========== Search Service ========== // var searchServiceName = 'srch-${solutionSuffix}' +var aiSearchIndexName = 'sample-dataset-index' var aiSearchIndexNameForContractSummary = 'contract-summary-doc-index' var aiSearchIndexNameForContractRisk = 'contract-risk-doc-index' var aiSearchIndexNameForContractCompliance = 'contract-compliance-doc-index' @@ -1778,7 +1852,7 @@ module searchServiceUpdate 'br/public:avm/res/search/search-service:0.12.0' = { ] } -// ========== Search Service - AI Project Connection ========== // +// ========== Search Service - AI Project Connection ==========// var aiSearchConnectionName = 'aifp-srch-connection-${solutionSuffix}' module aiSearchFoundryConnection 'modules/aifp-connections.bicep' = { @@ -1817,36 +1891,38 @@ output COSMOSDB_DATABASE string = cosmosDbDatabaseName output COSMOSDB_CONTAINER string = cosmosDbDatabaseMemoryContainerName output AZURE_OPENAI_ENDPOINT string = 'https://${aiFoundryAiServicesResourceName}.openai.azure.com/' output AZURE_OPENAI_MODEL_NAME string = aiFoundryAiServicesModelDeployment.name -output AZURE_OPENAI_DEPLOYMENT_NAME string = aiFoundryAiServicesModelDeployment.name -output AZURE_OPENAI_RAI_DEPLOYMENT_NAME string = aiFoundryAiServices4_1ModelDeployment.name +output AZURE_OPENAI_DEPLOYMENT_NAME string = aiFoundryAiServicesModelDeployment.deploymentName +output AZURE_OPENAI_RAI_DEPLOYMENT_NAME string = aiFoundryAiServices4_1ModelDeployment.deploymentName output AZURE_OPENAI_API_VERSION string = azureOpenaiAPIVersion // output APPLICATIONINSIGHTS_INSTRUMENTATION_KEY string = applicationInsights.outputs.instrumentationKey // output AZURE_AI_PROJECT_ENDPOINT string = aiFoundryAiServices.outputs.aiProjectInfo.apiEndpoint output AZURE_AI_SUBSCRIPTION_ID string = subscription().subscriptionId output AZURE_AI_RESOURCE_GROUP string = resourceGroup().name output AZURE_AI_PROJECT_NAME string = aiFoundryAiProjectName +output AZURE_AI_MODEL_DEPLOYMENT_NAME string = aiFoundryAiServicesModelDeployment.deploymentName // output APPLICATIONINSIGHTS_CONNECTION_STRING string = applicationInsights.outputs.connectionString -output AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME string = aiFoundryAiServicesModelDeployment.name +output AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME string = aiFoundryAiServicesModelDeployment.deploymentName // output AZURE_AI_AGENT_ENDPOINT string = aiFoundryAiProjectEndpoint output APP_ENV string = 'Prod' output AI_FOUNDRY_RESOURCE_ID string = !useExistingAiFoundryAiProject ? aiFoundryAiServices.outputs.resourceId : existingFoundryProjectResourceId output COSMOSDB_ACCOUNT_NAME string = cosmosDbResourceName -output AZURE_SEARCH_ENDPOINT string = searchServiceUpdate.outputs.endpoint +output AZURE_SEARCH_ENDPOINT string = searchServiceUpdate.outputs.endpoint output AZURE_CLIENT_ID string = userAssignedIdentity!.outputs.clientId output AZURE_TENANT_ID string = tenant().tenantId output AZURE_AI_SEARCH_CONNECTION_NAME string = aiSearchConnectionName output AZURE_COGNITIVE_SERVICES string = 'https://cognitiveservices.azure.com/.default' -output REASONING_MODEL_NAME string = aiFoundryAiServicesReasoningModelDeployment.name +output REASONING_MODEL_NAME string = aiFoundryAiServicesReasoningModelDeployment.deploymentName output MCP_SERVER_NAME string = 'MacaeMcpServer' output MCP_SERVER_DESCRIPTION string = 'MCP server with greeting, HR, and planning tools' -output SUPPORTED_MODELS string = '["o3","o4-mini","gpt-4.1","gpt-4.1-mini"]' +output SUPPORTED_MODELS string = '["${aiFoundryAiServicesModelDeployment.deploymentName}","${aiFoundryAiServices4_1ModelDeployment.deploymentName}","${aiFoundryAiServicesReasoningModelDeployment.deploymentName}"]' output BACKEND_URL string = 'https://${containerApp.outputs.fqdn}' output AZURE_AI_PROJECT_ENDPOINT string = aiFoundryAiProjectEndpoint output AZURE_AI_AGENT_ENDPOINT string = aiFoundryAiProjectEndpoint output AZURE_AI_AGENT_API_VERSION string = azureAiAgentAPIVersion output AZURE_AI_AGENT_PROJECT_CONNECTION_STRING string = '${aiFoundryAiServicesResourceName}.services.ai.azure.com;${aiFoundryAiServicesSubscriptionId};${aiFoundryAiServicesResourceGroupName};${aiFoundryAiProjectResourceName}' +output AZURE_DEV_COLLECT_TELEMETRY string = 'no' output AZURE_STORAGE_CONTAINER_NAME_RETAIL_CUSTOMER string = storageContainerNameRetailCustomer @@ -1866,3 +1942,17 @@ output AZURE_AI_SEARCH_INDEX_NAME_CONTRACT_SUMMARY string = aiSearchIndexNameFor output AZURE_AI_SEARCH_INDEX_NAME_CONTRACT_RISK string = aiSearchIndexNameForContractRisk output AZURE_AI_SEARCH_INDEX_NAME_CONTRACT_COMPLIANCE string = aiSearchIndexNameForContractCompliance +// Container Registry Outputs +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer +output AZURE_CONTAINER_REGISTRY_NAME string = containerRegistry.outputs.name + +// Outputs consumed by the post-provision image build & push script +// (infra/scripts/build_and_push_images.{ps1,sh}) +output AZURE_RESOURCE_GROUP string = resourceGroup().name +output BACKEND_CONTAINER_APP_NAME string = containerApp.outputs.name +output MCP_CONTAINER_APP_NAME string = containerAppMcp.outputs.name +output FRONTEND_WEB_APP_NAME string = webSite.outputs.name +output BACKEND_IMAGE_NAME string = 'macaebackend' +output FRONTEND_IMAGE_NAME string = 'macaefrontend' +output MCP_IMAGE_NAME string = 'macaemcp' +output FRONTEND_WEBSITES_PORT string = '3000' diff --git a/infra/main.json b/infra/main.json index b65ae11b3..011af8066 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,8 +5,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "6587818059632090787" + "version": "0.44.1.10279", + "templateHash": "13565483192814579761" }, "name": "Multi-Agent Custom Automation Engine", "description": "This module contains the resources required to deploy the [Multi-Agent Custom Automation Engine solution accelerator](https://github.com/microsoft/Multi-Agent-Custom-Automation-Engine-Solution-Accelerator) for both Sandbox environments and WAF aligned environments.\n\n> **Note:** This module is not intended for broad, generic use, as it was designed by the Commercial Solution Areas CTO team, as a Microsoft Solution Accelerator. Feature requests and bug fix requests are welcome if they support the needs of this organization but may not be incorporated if they aim to make this module more generic than what it needs to be for its primary use case. This module will likely be updated to leverage AVM resource modules in the future. This may result in breaking changes in upcoming versions when these features are implemented.\n" @@ -64,9 +64,8 @@ "azd": { "type": "location", "usageName": [ - "OpenAI.GlobalStandard.gpt4.1, 150", - "OpenAI.GlobalStandard.o4-mini, 50", - "OpenAI.GlobalStandard.gpt4.1-mini, 50" + "OpenAI.GlobalStandard.gpt-5, 150", + "OpenAI.GlobalStandard.gpt-5-mini, 100" ] }, "description": "Required. Location for all AI service resources. This should be one of the supported Azure AI Service locations." @@ -74,47 +73,68 @@ }, "gptModelName": { "type": "string", - "defaultValue": "gpt-4.1-mini", + "defaultValue": "gpt-5-mini", "minLength": 1, "metadata": { - "description": "Optional. Name of the GPT model to deploy:" + "description": "Optional. Name of the underlying GPT model to deploy. Defaults to gpt-5-mini (2025-08-07 series)." } }, "gptModelVersion": { "type": "string", - "defaultValue": "2025-04-14", + "defaultValue": "2025-08-07", "metadata": { - "description": "Optional. Version of the GPT model to deploy. Defaults to 2025-04-14." + "description": "Optional. Version of the GPT model to deploy. Defaults to 2025-08-07." + } + }, + "gptDeploymentName": { + "type": "string", + "defaultValue": "[parameters('gptModelName')]", + "metadata": { + "description": "Optional. Deployment (alias) name used in Azure OpenAI for the main GPT model. This is the value the application uses as `deployment_name` (including in data/agent_teams/*.json). Defaults to gptModelName." } }, "gpt4_1ModelName": { "type": "string", - "defaultValue": "gpt-4.1", + "defaultValue": "gpt-5", "minLength": 1, "metadata": { - "description": "Optional. Name of the GPT model to deploy:" + "description": "Optional. Name of the underlying larger GPT model to deploy. Defaults to gpt-5 (2025-08-07 series)." } }, "gpt4_1ModelVersion": { "type": "string", - "defaultValue": "2025-04-14", + "defaultValue": "2025-08-07", + "metadata": { + "description": "Optional. Version of the larger GPT model to deploy. Defaults to 2025-08-07." + } + }, + "gpt4_1DeploymentName": { + "type": "string", + "defaultValue": "[parameters('gpt4_1ModelName')]", "metadata": { - "description": "Optional. Version of the GPT model to deploy. Defaults to 2025-04-14." + "description": "Optional. Deployment (alias) name used in Azure OpenAI for the larger GPT model. Defaults to gpt4_1ModelName." } }, "gptReasoningModelName": { "type": "string", - "defaultValue": "o4-mini", + "defaultValue": "gpt-5-mini", "minLength": 1, "metadata": { - "description": "Optional. Name of the GPT Reasoning model to deploy:" + "description": "Optional. Name of the underlying GPT Reasoning model to deploy. Defaults to gpt-5-mini (reasoning-capable)." } }, "gptReasoningModelVersion": { "type": "string", - "defaultValue": "2025-04-16", + "defaultValue": "2025-08-07", "metadata": { - "description": "Optional. Version of the GPT Reasoning model to deploy. Defaults to 2025-04-16." + "description": "Optional. Version of the GPT Reasoning model to deploy. Defaults to 2025-08-07." + } + }, + "gptReasoningDeploymentName": { + "type": "string", + "defaultValue": "[if(equals(parameters('gptReasoningModelName'), parameters('gptModelName')), format('{0}-reasoning', parameters('gptReasoningModelName')), parameters('gptReasoningModelName'))]", + "metadata": { + "description": "Optional. Deployment (alias) name used in Azure OpenAI for the reasoning model. Must be unique from gptDeploymentName. Defaults to \"{gptReasoningModelName}-reasoning\" when it would otherwise collide with gptDeploymentName, otherwise gptReasoningModelName." } }, "azureOpenaiAPIVersion": { @@ -447,6 +467,7 @@ "aiFoundryAiProjectResourceName": "[if(variables('useExistingAiFoundryAiProject'), split(parameters('existingFoundryProjectResourceId'), '/')[10], format('proj-{0}', variables('solutionSuffix')))]", "aiFoundryAiServicesModelDeployment": { "format": "OpenAI", + "deploymentName": "[parameters('gptDeploymentName')]", "name": "[parameters('gptModelName')]", "version": "[parameters('gptModelVersion')]", "sku": { @@ -457,6 +478,7 @@ }, "aiFoundryAiServices4_1ModelDeployment": { "format": "OpenAI", + "deploymentName": "[parameters('gpt4_1DeploymentName')]", "name": "[parameters('gpt4_1ModelName')]", "version": "[parameters('gpt4_1ModelVersion')]", "sku": { @@ -467,6 +489,7 @@ }, "aiFoundryAiServicesReasoningModelDeployment": { "format": "OpenAI", + "deploymentName": "[parameters('gptReasoningDeploymentName')]", "name": "[parameters('gptReasoningModelName')]", "version": "[parameters('gptReasoningModelVersion')]", "sku": { @@ -480,12 +503,14 @@ "cosmosDbDatabaseName": "macae", "cosmosDbDatabaseMemoryContainerName": "memory", "containerAppEnvironmentResourceName": "[format('cae-{0}', variables('solutionSuffix'))]", + "acrPullRole": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')]", "containerAppResourceName": "[format('ca-{0}', variables('solutionSuffix'))]", "containerAppMcpResourceName": "[format('ca-mcp-{0}', variables('solutionSuffix'))]", "webServerFarmResourceName": "[format('asp-{0}', variables('solutionSuffix'))]", "webSiteResourceName": "[format('app-{0}', variables('solutionSuffix'))]", "storageAccountName": "[replace(format('st{0}', variables('solutionSuffix')), '-', '')]", "searchServiceName": "[format('srch-{0}', variables('solutionSuffix'))]", + "aiSearchIndexName": "sample-dataset-index", "aiSearchIndexNameForContractSummary": "contract-summary-doc-index", "aiSearchIndexNameForContractRisk": "contract-risk-doc-index", "aiSearchIndexNameForContractCompliance": "contract-compliance-doc-index", @@ -4992,8 +5017,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "9540091515555271756" + "version": "0.44.1.10279", + "templateHash": "16578961703143216826" } }, "definitions": { @@ -24243,7 +24268,7 @@ "deployments": { "value": [ { - "name": "[variables('aiFoundryAiServicesModelDeployment').name]", + "name": "[variables('aiFoundryAiServicesModelDeployment').deploymentName]", "model": { "format": "[variables('aiFoundryAiServicesModelDeployment').format]", "name": "[variables('aiFoundryAiServicesModelDeployment').name]", @@ -24256,7 +24281,7 @@ } }, { - "name": "[variables('aiFoundryAiServices4_1ModelDeployment').name]", + "name": "[variables('aiFoundryAiServices4_1ModelDeployment').deploymentName]", "model": { "format": "[variables('aiFoundryAiServices4_1ModelDeployment').format]", "name": "[variables('aiFoundryAiServices4_1ModelDeployment').name]", @@ -24269,7 +24294,7 @@ } }, { - "name": "[variables('aiFoundryAiServicesReasoningModelDeployment').name]", + "name": "[variables('aiFoundryAiServicesReasoningModelDeployment').deploymentName]", "model": { "format": "[variables('aiFoundryAiServicesReasoningModelDeployment').format]", "name": "[variables('aiFoundryAiServicesReasoningModelDeployment').name]", @@ -24310,8 +24335,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "7866379492866507946" + "version": "0.44.1.10279", + "templateHash": "9937220877261080488" } }, "definitions": { @@ -24678,7 +24703,7 @@ "deployments": { "value": [ { - "name": "[variables('aiFoundryAiServicesModelDeployment').name]", + "name": "[variables('aiFoundryAiServicesModelDeployment').deploymentName]", "model": { "format": "[variables('aiFoundryAiServicesModelDeployment').format]", "name": "[variables('aiFoundryAiServicesModelDeployment').name]", @@ -24691,7 +24716,7 @@ } }, { - "name": "[variables('aiFoundryAiServices4_1ModelDeployment').name]", + "name": "[variables('aiFoundryAiServices4_1ModelDeployment').deploymentName]", "model": { "format": "[variables('aiFoundryAiServices4_1ModelDeployment').format]", "name": "[variables('aiFoundryAiServices4_1ModelDeployment').name]", @@ -24704,7 +24729,7 @@ } }, { - "name": "[variables('aiFoundryAiServicesReasoningModelDeployment').name]", + "name": "[variables('aiFoundryAiServicesReasoningModelDeployment').deploymentName]", "model": { "format": "[variables('aiFoundryAiServicesReasoningModelDeployment').format]", "name": "[variables('aiFoundryAiServicesReasoningModelDeployment').name]", @@ -27976,8 +28001,8 @@ "dependsOn": [ "aiFoundryAiServices", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').aiServices)]", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", "virtualNetwork" ] }, @@ -28014,8 +28039,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "2868048678223903575" + "version": "0.44.1.10279", + "templateHash": "8001462637037527844" } }, "parameters": { @@ -38406,10 +38431,10 @@ "virtualNetwork" ] }, - "containerApp": { + "containerRegistry": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[take(format('avm.res.app.container-app.{0}', variables('containerAppResourceName')), 64)]", + "name": "registryDeployment", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -38417,230 +38442,43 @@ "mode": "Incremental", "parameters": { "name": { - "value": "[variables('containerAppResourceName')]" - }, - "tags": { - "value": "[parameters('tags')]" + "value": "[format('cr{0}', variables('solutionSuffix'))]" }, - "location": { - "value": "[parameters('location')]" - }, - "enableTelemetry": { - "value": "[parameters('enableTelemetry')]" + "acrAdminUserEnabled": { + "value": false }, - "environmentResourceId": { - "value": "[reference('containerAppEnvironment').outputs.resourceId.value]" + "acrSku": { + "value": "Basic" }, - "managedIdentities": { - "value": { - "userAssignedResourceIds": [ - "[reference('userAssignedIdentity').outputs.resourceId.value]" - ] - } + "azureADAuthenticationAsArmPolicyStatus": { + "value": "enabled" }, - "ingressTargetPort": { - "value": 8000 + "exportPolicyStatus": { + "value": "enabled" }, - "ingressExternal": { - "value": true + "location": { + "value": "[parameters('location')]" }, - "activeRevisionsMode": { - "value": "Single" + "softDeletePolicyDays": { + "value": 7 }, - "ingressAllowInsecure": { - "value": false + "softDeletePolicyStatus": { + "value": "disabled" }, - "corsPolicy": { - "value": { - "allowedOrigins": [ - "[format('https://{0}.azurewebsites.net', variables('webSiteResourceName'))]", - "[format('http://{0}.azurewebsites.net', variables('webSiteResourceName'))]" - ], - "allowedMethods": [ - "GET", - "POST", - "PUT", - "DELETE", - "OPTIONS" - ] - } + "tags": { + "value": "[parameters('tags')]" }, - "scaleSettings": { - "value": { - "maxReplicas": "[if(parameters('enableScalability'), 3, 1)]", - "minReplicas": "[if(parameters('enableScalability'), 1, 1)]", - "rules": [ - { - "name": "http-scaler", - "http": { - "metadata": { - "concurrentRequests": "100" - } - } - } - ] - } + "networkRuleBypassOptions": { + "value": "AzureServices" }, - "containers": { + "roleAssignments": { "value": [ { - "name": "backend", - "image": "[format('{0}/{1}:{2}', parameters('backendContainerRegistryHostname'), parameters('backendContainerImageName'), parameters('backendContainerImageTag'))]", - "resources": { - "cpu": "2.0", - "memory": "4.0Gi" - }, - "env": [ - { - "name": "COSMOSDB_ENDPOINT", - "value": "[format('https://{0}.documents.azure.com:443/', variables('cosmosDbResourceName'))]" - }, - { - "name": "COSMOSDB_DATABASE", - "value": "[variables('cosmosDbDatabaseName')]" - }, - { - "name": "COSMOSDB_CONTAINER", - "value": "[variables('cosmosDbDatabaseMemoryContainerName')]" - }, - { - "name": "AZURE_OPENAI_ENDPOINT", - "value": "[format('https://{0}.openai.azure.com/', variables('aiFoundryAiServicesResourceName'))]" - }, - { - "name": "AZURE_OPENAI_MODEL_NAME", - "value": "[variables('aiFoundryAiServicesModelDeployment').name]" - }, - { - "name": "AZURE_OPENAI_DEPLOYMENT_NAME", - "value": "[variables('aiFoundryAiServicesModelDeployment').name]" - }, - { - "name": "AZURE_OPENAI_RAI_DEPLOYMENT_NAME", - "value": "[variables('aiFoundryAiServices4_1ModelDeployment').name]" - }, - { - "name": "AZURE_OPENAI_API_VERSION", - "value": "[parameters('azureOpenaiAPIVersion')]" - }, - { - "name": "APPLICATIONINSIGHTS_INSTRUMENTATION_KEY", - "value": "[if(parameters('enableMonitoring'), reference('applicationInsights').outputs.instrumentationKey.value, '')]" - }, - { - "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", - "value": "[if(parameters('enableMonitoring'), reference('applicationInsights').outputs.connectionString.value, '')]" - }, - { - "name": "AZURE_AI_SUBSCRIPTION_ID", - "value": "[variables('aiFoundryAiServicesSubscriptionId')]" - }, - { - "name": "AZURE_AI_RESOURCE_GROUP", - "value": "[variables('aiFoundryAiServicesResourceGroupName')]" - }, - { - "name": "AZURE_AI_PROJECT_NAME", - "value": "[if(variables('useExistingAiFoundryAiProject'), variables('aiFoundryAiProjectResourceName'), reference('aiFoundryAiServicesProject').outputs.name.value)]" - }, - { - "name": "FRONTEND_SITE_NAME", - "value": "[format('https://{0}.azurewebsites.net', variables('webSiteResourceName'))]" - }, - { - "name": "AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME", - "value": "[variables('aiFoundryAiServicesModelDeployment').name]" - }, - { - "name": "APP_ENV", - "value": "Prod" - }, - { - "name": "AZURE_AI_SEARCH_CONNECTION_NAME", - "value": "[variables('aiSearchConnectionName')]" - }, - { - "name": "AZURE_AI_SEARCH_ENDPOINT", - "value": "[reference('searchServiceUpdate').outputs.endpoint.value]" - }, - { - "name": "AZURE_COGNITIVE_SERVICES", - "value": "https://cognitiveservices.azure.com/.default" - }, - { - "name": "AZURE_BING_CONNECTION_NAME", - "value": "binggrnd" - }, - { - "name": "BING_CONNECTION_NAME", - "value": "binggrnd" - }, - { - "name": "REASONING_MODEL_NAME", - "value": "[variables('aiFoundryAiServicesReasoningModelDeployment').name]" - }, - { - "name": "MCP_SERVER_ENDPOINT", - "value": "[format('https://{0}/mcp', reference('containerAppMcp').outputs.fqdn.value)]" - }, - { - "name": "MCP_SERVER_NAME", - "value": "MacaeMcpServer" - }, - { - "name": "MCP_SERVER_DESCRIPTION", - "value": "MCP server with greeting, HR, and planning tools" - }, - { - "name": "AZURE_TENANT_ID", - "value": "[tenant().tenantId]" - }, - { - "name": "AZURE_CLIENT_ID", - "value": "[reference('userAssignedIdentity').outputs.clientId.value]" - }, - { - "name": "SUPPORTED_MODELS", - "value": "[[\"o3\",\"o4-mini\",\"gpt-4.1\",\"gpt-4.1-mini\"]" - }, - { - "name": "AZURE_STORAGE_BLOB_URL", - "value": "[reference('avmStorageAccount').outputs.serviceEndpoints.value.blob]" - }, - { - "name": "AZURE_AI_PROJECT_ENDPOINT", - "value": "[if(variables('useExistingAiFoundryAiProject'), reference('existingAiFoundryAiServicesProject').endpoints['AI Foundry API'], reference('aiFoundryAiServicesProject').outputs.apiEndpoint.value)]" - }, - { - "name": "AZURE_AI_AGENT_ENDPOINT", - "value": "[if(variables('useExistingAiFoundryAiProject'), reference('existingAiFoundryAiServicesProject').endpoints['AI Foundry API'], reference('aiFoundryAiServicesProject').outputs.apiEndpoint.value)]" - }, - { - "name": "AZURE_AI_AGENT_API_VERSION", - "value": "[parameters('azureAiAgentAPIVersion')]" - }, - { - "name": "AZURE_AI_AGENT_PROJECT_CONNECTION_STRING", - "value": "[format('{0}.services.ai.azure.com;{1};{2};{3}', variables('aiFoundryAiServicesResourceName'), variables('aiFoundryAiServicesSubscriptionId'), variables('aiFoundryAiServicesResourceGroupName'), variables('aiFoundryAiProjectResourceName'))]" - }, - { - "name": "AZURE_BASIC_LOGGING_LEVEL", - "value": "INFO" - }, - { - "name": "AZURE_PACKAGE_LOGGING_LEVEL", - "value": "WARNING" - }, - { - "name": "AZURE_LOGGING_PACKAGES", - "value": "" - } - ] + "roleDefinitionIdOrName": "[variables('acrPullRole')]", + "principalType": "ServicePrincipal", + "principalId": "[reference('userAssignedIdentity').outputs.principalId.value]" } ] - }, - "secrets": { - "value": [] } }, "template": { @@ -38650,267 +38488,4322 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.42.1.51946", - "templateHash": "14994565436716579607" + "version": "0.41.2.15936", + "templateHash": "6962321898079302529" }, - "name": "Container Apps", - "description": "This module deploys a Container App." + "name": "Azure Container Registries (ACR)", + "description": "This module deploys an Azure Container Registry (ACR)." }, "definitions": { - "ingressPortMappingType": { + "privateEndpointOutputType": { "type": "object", "properties": { - "exposedPort": { - "type": "int", + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + } + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + } + }, + "groupId": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Specifies the exposed port for the target port. If not specified, it defaults to target port." + "description": "The group Id for the private endpoint Group." } }, - "external": { - "type": "bool", + "customDnsConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "A list of private IP addresses of the private endpoint." + } + } + } + }, "metadata": { - "description": "Required. Specifies whether the app port is accessible outside of the environment." + "description": "The custom DNS configurations of the private endpoint." } }, - "targetPort": { - "type": "int", + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, "metadata": { - "description": "Required. Specifies the port the container listens on." + "description": "The IDs of the network interfaces associated with the private endpoint." } } }, "metadata": { - "__bicep_export!": true, - "description": "The type for an ingress port mapping." + "__bicep_export!": true } }, - "serviceBindingType": { + "credentialSetType": { "type": "object", "properties": { "name": { "type": "string", "metadata": { - "description": "Required. The name of the service." + "description": "Required. The name of the credential set." } }, - "serviceId": { + "managedIdentities": { + "$ref": "#/definitions/managedIdentityOnlySysAssignedType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource." + } + }, + "authCredentials": { + "type": "array", + "items": { + "$ref": "#/definitions/authCredentialsType" + }, + "metadata": { + "description": "Required. List of authentication credentials stored for an upstream. Usually consists of a primary and an optional secondary credential." + } + }, + "loginServer": { "type": "string", "metadata": { - "description": "Required. The service ID." + "description": "Required. The credentials are stored for this upstream or login server." } } }, "metadata": { - "description": "The type for a service binding." + "__bicep_export!": true, + "description": "The type for a credential set." } }, - "environmentVarType": { + "scopeMapsType": { "type": "object", "properties": { "name": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. Environment variable name." + "description": "Optional. The name of the scope map." } }, - "secretRef": { - "type": "string", - "nullable": true, + "actions": { + "type": "array", "metadata": { - "description": "Optional. Name of the Container App secret from which to pull the environment variable value." + "__bicep_resource_derived_type!": { + "source": "Microsoft.ContainerRegistry/registries/scopeMaps@2025-03-01-preview#properties/properties/properties/actions" + }, + "description": "Required. The list of scoped permissions for registry artifacts." } }, - "value": { + "description": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Non-secret environment variable value." + "description": "Optional. The user friendly description of the scope map." } } }, "metadata": { "__bicep_export!": true, - "description": "The type for an environment variable." + "description": "The type for a scope map." } }, - "containerAppProbeType": { + "cacheRuleType": { "type": "object", "properties": { - "failureThreshold": { - "type": "int", + "name": { + "type": "string", "nullable": true, - "minValue": 1, - "maxValue": 10, "metadata": { - "description": "Optional. Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3." + "description": "Optional. The name of the cache rule. Will be derived from the source repository name if not defined." } }, - "httpGet": { - "$ref": "#/definitions/containerAppProbeHttpGetType", - "nullable": true, + "sourceRepository": { + "type": "string", "metadata": { - "description": "Optional. HTTPGet specifies the http request to perform." + "description": "Required. Source repository pulled from upstream." } }, - "initialDelaySeconds": { - "type": "int", + "targetRepository": { + "type": "string", "nullable": true, - "minValue": 1, - "maxValue": 60, "metadata": { - "description": "Optional. Number of seconds after the container has started before liveness probes are initiated." + "description": "Optional. Target repository specified in docker pull command. E.g.: docker pull myregistry.azurecr.io/{targetRepository}:{tag}." } }, - "periodSeconds": { - "type": "int", + "credentialSetResourceId": { + "type": "string", "nullable": true, - "minValue": 1, - "maxValue": 240, "metadata": { - "description": "Optional. How often (in seconds) to perform the probe. Default to 10 seconds." + "description": "Optional. The resource ID of the credential store which is associated with the cache rule." } - }, - "successThreshold": { - "type": "int", - "nullable": true, - "minValue": 1, - "maxValue": 10, + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a cache rule." + } + }, + "replicationType": { + "type": "object", + "properties": { + "name": { + "type": "string", "metadata": { - "description": "Optional. Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup." + "description": "Required. The name of the replication." } }, - "tcpSocket": { - "$ref": "#/definitions/containerAppProbeTcpSocketType", + "location": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. The TCP socket specifies an action involving a TCP port. TCP hooks not yet supported." + "description": "Optional. Location for all resources." } }, - "terminationGracePeriodSeconds": { - "type": "int", - "nullable": true, + "tags": { + "type": "object", "metadata": { - "description": "Optional. Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is an alpha field and requires enabling ProbeTerminationGracePeriod feature gate. Maximum value is 3600 seconds (1 hour)." - } + "__bicep_resource_derived_type!": { + "source": "Microsoft.ContainerRegistry/registries/replications@2025-03-01-preview#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true }, - "timeoutSeconds": { - "type": "int", + "regionEndpointEnabled": { + "type": "bool", "nullable": true, - "minValue": 1, - "maxValue": 240, "metadata": { - "description": "Optional. Number of seconds after which the probe times out. Defaults to 1 second." + "description": "Optional. Specifies whether the replication regional endpoint is enabled. Requests will not be routed to a replication whose regional endpoint is disabled, however its data will continue to be synced with other replications." } }, - "type": { + "zoneRedundancy": { "type": "string", - "allowedValues": [ - "Liveness", - "Readiness", - "Startup" - ], - "nullable": true, "metadata": { - "description": "Optional. The type of probe." - } + "__bicep_resource_derived_type!": { + "source": "Microsoft.ContainerRegistry/registries@2025-03-01-preview#properties/properties/properties/zoneRedundancy" + }, + "description": "Optional. Whether or not zone redundancy is enabled for this container registry." + }, + "nullable": true } }, "metadata": { - "description": "The type for a container app probe." + "__bicep_export!": true, + "description": "The type for a replication." } }, - "corsPolicyType": { + "taskType": { "type": "object", "properties": { - "allowCredentials": { - "type": "bool", - "nullable": true, + "name": { + "type": "string", "metadata": { - "description": "Optional. Switch to determine whether the resource allows credentials." + "description": "Required. The name of the task." } }, - "allowedHeaders": { - "type": "array", - "items": { - "type": "string" - }, + "location": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Specifies the content for the access-control-allow-headers header." + "description": "Optional. Location for all resources." } }, - "allowedMethods": { - "type": "array", - "items": { - "type": "string" + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.ContainerRegistry/registries/tasks@2025-03-01-preview#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "platform": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.ContainerRegistry/registries/tasks@2025-03-01-preview#properties/properties/properties/platform" + }, + "description": "Optional. The platform properties for the task." + }, + "nullable": true + }, + "step": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.ContainerRegistry/registries/tasks@2025-03-01-preview#properties/properties/properties/step" + }, + "description": "Optional. The step properties for the task." + }, + "nullable": true + }, + "trigger": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.ContainerRegistry/registries/tasks@2025-03-01-preview#properties/properties/properties/trigger" + }, + "description": "Optional. The trigger properties for the task." + }, + "nullable": true + }, + "status": { + "type": "string", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.ContainerRegistry/registries/tasks@2025-03-01-preview#properties/properties/properties/status" + }, + "description": "Optional. The status of the task at the time the operation was called." }, + "nullable": true + }, + "timeout": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. Specifies the content for the access-control-allow-methods header." + "description": "Optional. The timeout in seconds for the task to run before it is automatically disabled." } }, - "allowedOrigins": { - "type": "array", - "items": { - "type": "string" + "agentConfiguration": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.ContainerRegistry/registries/tasks@2025-03-01-preview#properties/properties/properties/agentConfiguration" + }, + "description": "Optional. The agent configuration for the task." }, + "nullable": true + }, + "agentPoolName": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Specifies the content for the access-control-allow-origins header." + "description": "Optional. The name of the agent pool to run the task on. If not specified, the task will run on Microsoft-hosted agents." } }, - "exposeHeaders": { - "type": "array", - "items": { - "type": "string" - }, + "isSystemTask": { + "type": "bool", "nullable": true, "metadata": { - "description": "Optional. Specifies the content for the access-control-expose-headers header." + "description": "Optional. Whether this is a system task or not. System tasks have some additional restrictions and are used for internal purposes by Microsoft services, such as Azure DevOps pipelines integration." } }, - "maxAge": { - "type": "int", + "logTemplate": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Specifies the content for the access-control-max-age header." + "description": "Optional. The log template for the task to use when creating logs in Log Analytics." } } }, "metadata": { "__bicep_export!": true, - "description": "The type for a CORS policy." + "description": "The type for a task." } }, - "containerAppProbeHttpGetType": { + "tokenType": { "type": "object", "properties": { - "host": { + "name": { "type": "string", - "nullable": true, "metadata": { - "description": "Optional. Host name to connect to. Defaults to the pod IP." + "description": "Required. The name of the token." } }, - "httpHeaders": { - "type": "array", - "items": { - "$ref": "#/definitions/containerAppProbeHttpGetHeadersItemType" - }, - "nullable": true, + "scopeMapResourceId": { + "type": "string", "metadata": { - "description": "Optional. HTTP headers to set in the request." + "description": "Required. The resource ID of the scope map which defines the permissions for this token." } }, - "path": { + "status": { "type": "string", "metadata": { - "description": "Required. Path to access on the HTTP server." - } + "__bicep_resource_derived_type!": { + "source": "Microsoft.ContainerRegistry/registries/tokens@2025-11-01#properties/properties/properties/status" + }, + "description": "Optional. The status of the token at the time the operation was called." + }, + "nullable": true }, - "port": { - "type": "int", + "credentials": { + "type": "array", + "items": { + "$ref": "#/definitions/authCredentialsType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The list of credentials associated with the token. Usually consists of a primary and an optional secondary credential." + } + } + } + }, + "webhookType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "minLength": 5, + "maxLength": 50, + "metadata": { + "description": "Optional. The name of the registry webhook." + } + }, + "serviceUri": { + "type": "string", + "metadata": { + "description": "Required. The service URI for the webhook to post notifications." + } + }, + "status": { + "type": "string", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.ContainerRegistry/registries/webhooks@2025-03-01-preview#properties/properties/properties/status" + }, + "description": "Optional. The status of the webhook at the time the operation was called." + }, + "nullable": true + }, + "action": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The list of actions that trigger the webhook to post notifications." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.ContainerRegistry/registries/webhooks@2025-03-01-preview#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "customHeaders": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.ContainerRegistry/registries/webhooks@2025-03-01-preview#properties/properties/properties/customHeaders" + }, + "description": "Optional. Custom headers that will be added to the webhook notifications." + }, + "nullable": true + }, + "scope": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The scope of repositories where the event can be triggered. For example, 'foo:*' means events for all tags under repository 'foo'. 'foo:bar' means events for 'foo:bar' only. 'foo' is equivalent to 'foo:latest'. Empty means all events." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a webhook." + } + }, + "_1.privateEndpointCustomDnsConfigType": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.7.0" + } + } + }, + "_1.privateEndpointIpConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource that is unique within a resource group." + } + }, + "properties": { + "type": "object", + "properties": { + "groupId": { + "type": "string", + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to." + } + }, + "memberName": { + "type": "string", + "metadata": { + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to." + } + }, + "privateIPAddress": { + "type": "string", + "metadata": { + "description": "Required. A private IP address obtained from the private endpoint's subnet." + } + } + }, + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.7.0" + } + } + }, + "_1.privateEndpointPrivateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS Zone Group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + } + }, + "metadata": { + "description": "Required. The private DNS Zone Groups to associate the Private Endpoint. A DNS Zone Group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.7.0" + } + } + }, + "authCredentialsType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the credential." + } + }, + "usernameSecretIdentifier": { + "type": "string", + "metadata": { + "description": "Required. KeyVault Secret URI for accessing the username." + } + }, + "passwordSecretIdentifier": { + "type": "string", + "metadata": { + "description": "Required. KeyVault Secret URI for accessing the password." + } + } + }, + "metadata": { + "description": "The type for auth credentials.", + "__bicep_imported_from!": { + "sourceTemplate": "credential-set/main.bicep" + } + } + }, + "customerManagedKeyWithAutoRotateType": { + "type": "object", + "properties": { + "keyVaultResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of a key vault to reference a customer managed key for encryption from." + } + }, + "keyName": { + "type": "string", + "metadata": { + "description": "Required. The name of the customer managed key to use for encryption." + } + }, + "keyVersion": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The version of the customer managed key to reference for encryption. If not provided, using version as per 'autoRotationEnabled' setting." + } + }, + "autoRotationEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable auto-rotating to the latest key version. Default is `true`. If set to `false`, the latest key version at the time of the deployment is used." + } + }, + "userAssignedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. User assigned identity to use when fetching the customer managed key. Required if no system assigned identity is available for use." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a customer-managed key. To be used if the resource type supports auto-rotation of the customer-managed key.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.7.0" + } + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.7.0" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.7.0" + } + } + }, + "managedIdentityAllType": { + "type": "object", + "properties": { + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } + }, + "userAssignedResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.7.0" + } + } + }, + "managedIdentityOnlySysAssignedType": { + "type": "object", + "properties": { + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if only system-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.7.0" + } + } + }, + "privateEndpointSingleServiceType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private Endpoint." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The location to deploy the Private Endpoint to." + } + }, + "privateLinkServiceConnectionName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private link connection to create." + } + }, + "service": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The subresource to deploy the Private Endpoint for. For example \"vault\" for a Key Vault Private Endpoint." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "resourceGroupResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the Resource Group the Private Endpoint will be created in. If not specified, the Resource Group of the provided Virtual Network Subnet is used." + } + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/_1.privateEndpointPrivateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS Zone Group to configure for the Private Endpoint." + } + }, + "isManualConnection": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. If Manual Private Link Connection is required." + } + }, + "manualConnectionRequestMessage": { + "type": "string", + "nullable": true, + "maxLength": 140, + "metadata": { + "description": "Optional. A message passed to the owner of the remote resource with the manual connection request." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.privateEndpointCustomDnsConfigType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Custom DNS configurations." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.privateEndpointIpConfigurationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of IP configurations of the Private Endpoint. This will be used to map to the first-party Service endpoints." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the Private Endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The custom name of the network interface attached to the Private Endpoint." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-07-01#properties/tags" + }, + "description": "Optional. Tags to be applied on all resources/Resource Groups in this deployment." + } + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a private endpoint. To be used if the private endpoint's default service / groupId can be assumed (i.e., for services that only have one Private Endpoint type like 'vault' for key vault).", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.7.0" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.7.0" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "minLength": 5, + "maxLength": 50, + "metadata": { + "description": "Required. Name of your Azure Container Registry." + } + }, + "acrAdminUserEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Enable admin user that have push / pull permission to the registry." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "autoGeneratedDomainNameLabelScope": { + "type": "string", + "nullable": true, + "allowedValues": [ + "NoReuse", + "ResourceGroupReuse", + "SubscriptionReuse", + "TenantReuse", + "Unsecure" + ], + "metadata": { + "description": "Optional. The domain name label reuse scope." + } + }, + "roleAssignmentMode": { + "type": "string", + "nullable": true, + "allowedValues": [ + "AbacRepositoryPermissions", + "LegacyRegistryPermissions" + ], + "metadata": { + "description": "Optional. The registry permissions role assignment mode." + } + }, + "acrSku": { + "type": "string", + "defaultValue": "Premium", + "allowedValues": [ + "Basic", + "Premium", + "Standard" + ], + "metadata": { + "description": "Optional. Tier of your Azure container registry." + } + }, + "exportPolicyStatus": { + "type": "string", + "defaultValue": "disabled", + "allowedValues": [ + "disabled", + "enabled" + ], + "metadata": { + "description": "Optional. The value that indicates whether the export policy is enabled or not." + } + }, + "quarantinePolicyStatus": { + "type": "string", + "defaultValue": "disabled", + "allowedValues": [ + "disabled", + "enabled" + ], + "metadata": { + "description": "Optional. The value that indicates whether the quarantine policy is enabled or not. Note, requires the 'acrSku' to be 'Premium'." + } + }, + "trustPolicyStatus": { + "type": "string", + "defaultValue": "disabled", + "allowedValues": [ + "disabled", + "enabled" + ], + "metadata": { + "description": "Optional. The value that indicates whether the trust policy is enabled or not. Note, requires the 'acrSku' to be 'Premium'." + } + }, + "retentionPolicyStatus": { + "type": "string", + "defaultValue": "enabled", + "allowedValues": [ + "disabled", + "enabled" + ], + "metadata": { + "description": "Optional. The value that indicates whether the retention policy is enabled or not." + } + }, + "retentionPolicyDays": { + "type": "int", + "defaultValue": 15, + "metadata": { + "description": "Optional. The number of days to retain an untagged manifest after which it gets purged." + } + }, + "azureADAuthenticationAsArmPolicyStatus": { + "type": "string", + "defaultValue": "disabled", + "allowedValues": [ + "disabled", + "enabled" + ], + "metadata": { + "description": "Optional. The value that indicates whether the policy for using ARM audience token for a container registry is enabled or not. Default is disabled." + } + }, + "softDeletePolicyStatus": { + "type": "string", + "defaultValue": "disabled", + "allowedValues": [ + "disabled", + "enabled" + ], + "metadata": { + "description": "Optional. Soft Delete policy status. Default is disabled." + } + }, + "softDeletePolicyDays": { + "type": "int", + "defaultValue": 7, + "metadata": { + "description": "Optional. The number of days after which a soft-deleted item is permanently deleted." + } + }, + "dataEndpointEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Enable a single data endpoint per region for serving data. Not relevant in case of disabled public access. Note, requires the 'acrSku' to be 'Premium'." + } + }, + "publicNetworkAccess": { + "type": "string", + "nullable": true, + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Optional. Whether or not public network access is allowed for this resource. For security reasons it should be disabled. If not specified, it will be disabled by default if private endpoints are set and networkRuleSetIpRules are not set. Note, requires the 'acrSku' to be 'Premium'." + } + }, + "networkRuleBypassOptions": { + "type": "string", + "defaultValue": "AzureServices", + "allowedValues": [ + "AzureServices", + "None" + ], + "metadata": { + "description": "Optional. Whether to allow trusted Azure services to access a network restricted registry." + } + }, + "networkRuleSetDefaultAction": { + "type": "string", + "defaultValue": "Deny", + "allowedValues": [ + "Allow", + "Deny" + ], + "metadata": { + "description": "Optional. The default action of allow or deny when no other rules match." + } + }, + "networkRuleSetIpRules": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. The IP ACL rules. Note, requires the 'acrSku' to be 'Premium'. Set to an empty array to explicitly configure no allowed IPs." + } + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointSingleServiceType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible. Note, requires the 'acrSku' to be 'Premium'." + } + }, + "zoneRedundancy": { + "type": "string", + "defaultValue": "Enabled", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "metadata": { + "description": "Optional. Whether or not zone redundancy is enabled for this container registry." + } + }, + "replications": { + "type": "array", + "items": { + "$ref": "#/definitions/replicationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. All replications to create." + } + }, + "webhooks": { + "type": "array", + "items": { + "$ref": "#/definitions/webhookType" + }, + "nullable": true, + "metadata": { + "description": "Optional. All webhooks to create." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityAllType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.ContainerRegistry/registries@2025-04-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service. If neither metrics nor logs are specified, all metrics & logs are configured by default. If either one is specified, the other is ignored." + } + }, + "anonymousPullEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Enables registry-wide pull from unauthenticated clients. It's in preview and available in the Standard and Premium service tiers." + } + }, + "customerManagedKey": { + "$ref": "#/definitions/customerManagedKeyWithAutoRotateType", + "nullable": true, + "metadata": { + "description": "Optional. The customer managed key definition." + } + }, + "cacheRules": { + "type": "array", + "items": { + "$ref": "#/definitions/cacheRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of Cache Rules." + } + }, + "credentialSets": { + "type": "array", + "items": { + "$ref": "#/definitions/credentialSetType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of Credential Sets." + } + }, + "scopeMaps": { + "type": "array", + "items": { + "$ref": "#/definitions/scopeMapsType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Scope maps setting." + } + }, + "tokens": { + "type": "array", + "items": { + "$ref": "#/definitions/tokenType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Tokens to create for the container registry." + } + }, + "tasks": { + "type": "array", + "items": { + "$ref": "#/definitions/taskType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of ACR Tasks to create." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "enableReferencedModulesTelemetry": false, + "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned, UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', 'None')), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", + "builtInRoleNames": { + "AcrDelete": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c2f4ef07-c644-48eb-af81-4b1b4947fb11')]", + "AcrImageSigner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '6cef56e8-d556-48e5-a04f-b8e64114680f')]", + "AcrPull": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')]", + "AcrPush": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8311e382-0749-4cb8-b61a-304f252e45ec')]", + "AcrQuarantineReader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'cdda3590-29a3-44f6-95f2-9f980659eb04')]", + "AcrQuarantineWriter": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c8d4ff99-41c3-41a8-9f60-21dfdad59608')]", + "Container Registry Repository Catalog Lister": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'bfdb9389-c9a5-478a-bb2f-ba9ca092c3c7')]", + "Container Registry Repository Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '2efddaa5-3f1f-4df3-97df-af3f13818f4c')]", + "Container Registry Repository Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b93aa761-3e63-49ed-ac28-beffa264f7ac')]", + "Container Registry Repository Writer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '2a1e307c-b015-4ebd-883e-5b7698a07328')]", + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + }, + "publicNetworkAccessMode": "[if(not(empty(parameters('publicNetworkAccess'))), parameters('publicNetworkAccess'), if(and(not(empty(parameters('privateEndpoints'))), empty(parameters('networkRuleSetIpRules'))), 'Disabled', null()))]", + "shouldConfigureNetworkRuleSet": "[or(not(equals(parameters('networkRuleSetIpRules'), null())), and(equals(variables('publicNetworkAccessMode'), 'Enabled'), equals(parameters('networkRuleSetDefaultAction'), 'Deny')))]" + }, + "resources": { + "cMKKeyVault::cMKKey": { + "condition": "[and(not(empty(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'))), and(not(empty(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'))), not(empty(tryGet(parameters('customerManagedKey'), 'keyName')))))]", + "existing": true, + "type": "Microsoft.KeyVault/vaults/keys", + "apiVersion": "2024-11-01", + "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[4]]", + "name": "[format('{0}/{1}', last(split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')), tryGet(parameters('customerManagedKey'), 'keyName'))]" + }, + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('46d3xbcp.res.containerregistry-registry.{0}.{1}', replace('0.12.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "cMKKeyVault": { + "condition": "[not(empty(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId')))]", + "existing": true, + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2024-11-01", + "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[4]]", + "name": "[last(split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/'))]" + }, + "cMKUserAssignedIdentity": { + "condition": "[not(empty(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId')))]", + "existing": true, + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2024-11-30", + "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[4]]", + "name": "[last(split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/'))]" + }, + "registry": { + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2025-06-01-preview", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "identity": "[variables('identity')]", + "tags": "[parameters('tags')]", + "sku": { + "name": "[parameters('acrSku')]" + }, + "properties": { + "anonymousPullEnabled": "[parameters('anonymousPullEnabled')]", + "adminUserEnabled": "[parameters('acrAdminUserEnabled')]", + "autoGeneratedDomainNameLabelScope": "[parameters('autoGeneratedDomainNameLabelScope')]", + "roleAssignmentMode": "[parameters('roleAssignmentMode')]", + "encryption": "[if(not(empty(parameters('customerManagedKey'))), createObject('status', 'enabled', 'keyVaultProperties', createObject('identity', if(not(empty(coalesce(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), ''))), reference('cMKUserAssignedIdentity').clientId, null()), 'keyIdentifier', if(not(empty(tryGet(parameters('customerManagedKey'), 'keyVersion'))), format('{0}/{1}', reference('cMKKeyVault::cMKKey').keyUri, tryGet(parameters('customerManagedKey'), 'keyVersion')), if(coalesce(tryGet(parameters('customerManagedKey'), 'autoRotationEnabled'), true()), reference('cMKKeyVault::cMKKey').keyUri, reference('cMKKeyVault::cMKKey').keyUriWithVersion)))), null())]", + "policies": { + "azureADAuthenticationAsArmPolicy": { + "status": "[parameters('azureADAuthenticationAsArmPolicyStatus')]" + }, + "exportPolicy": "[if(equals(parameters('acrSku'), 'Premium'), createObject('status', parameters('exportPolicyStatus')), null())]", + "quarantinePolicy": "[if(equals(parameters('acrSku'), 'Premium'), createObject('status', parameters('quarantinePolicyStatus')), null())]", + "trustPolicy": "[if(equals(parameters('acrSku'), 'Premium'), createObject('type', 'Notary', 'status', parameters('trustPolicyStatus')), null())]", + "retentionPolicy": "[if(equals(parameters('acrSku'), 'Premium'), createObject('days', parameters('retentionPolicyDays'), 'status', parameters('retentionPolicyStatus')), null())]", + "softDeletePolicy": { + "retentionDays": "[parameters('softDeletePolicyDays')]", + "status": "[parameters('softDeletePolicyStatus')]" + } + }, + "dataEndpointEnabled": "[parameters('dataEndpointEnabled')]", + "publicNetworkAccess": "[variables('publicNetworkAccessMode')]", + "networkRuleBypassOptions": "[parameters('networkRuleBypassOptions')]", + "networkRuleSet": "[if(variables('shouldConfigureNetworkRuleSet'), createObject('defaultAction', parameters('networkRuleSetDefaultAction'), 'ipRules', coalesce(parameters('networkRuleSetIpRules'), createArray())), null())]", + "zoneRedundancy": "[if(equals(parameters('acrSku'), 'Premium'), parameters('zoneRedundancy'), null())]" + }, + "dependsOn": [ + "cMKKeyVault::cMKKey", + "cMKUserAssignedIdentity" + ] + }, + "registry_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "registry" + ] + }, + "registry_diagnosticSettings": { + "copy": { + "name": "registry_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), if(empty(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups')), createArray(createObject('category', 'AllMetrics')), createArray())))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), if(empty(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups')), createArray(createObject('category', 'AllMetrics')), createArray()))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), if(empty(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups')), createArray(createObject('category', 'AllMetrics')), createArray()))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), if(empty(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories')), createArray(createObject('categoryGroup', 'allLogs')), createArray())))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), if(empty(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories')), createArray(createObject('categoryGroup', 'allLogs')), createArray()))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), if(empty(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories')), createArray(createObject('categoryGroup', 'allLogs')), createArray()))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), if(empty(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories')), createArray(createObject('categoryGroup', 'allLogs')), createArray()))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "registry" + ] + }, + "registry_roleAssignments": { + "copy": { + "name": "registry_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.ContainerRegistry/registries', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "registry" + ] + }, + "registry_scopeMaps": { + "copy": { + "name": "registry_scopeMaps", + "count": "[length(coalesce(parameters('scopeMaps'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-Registry-Scope-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[tryGet(coalesce(parameters('scopeMaps'), createArray())[copyIndex()], 'name')]" + }, + "actions": { + "value": "[coalesce(parameters('scopeMaps'), createArray())[copyIndex()].actions]" + }, + "description": { + "value": "[tryGet(coalesce(parameters('scopeMaps'), createArray())[copyIndex()], 'description')]" + }, + "registryName": { + "value": "[parameters('name')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "6960042545085324765" + }, + "name": "Container Registries scope maps", + "description": "This module deploys an Azure Container Registry (ACR) scope map." + }, + "parameters": { + "registryName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent registry. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "defaultValue": "[format('{0}-scopemaps', parameters('registryName'))]", + "metadata": { + "description": "Optional. The name of the scope map." + } + }, + "actions": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. The list of scoped permissions for registry artifacts." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The user friendly description of the scope map." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('46d3xbcp.res.containerregistry-registry-scopemap.{0}.{1}', replace('0.1.0', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "registry": { + "existing": true, + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2025-11-01", + "name": "[parameters('registryName')]" + }, + "scopeMap": { + "type": "Microsoft.ContainerRegistry/registries/scopeMaps", + "apiVersion": "2025-11-01", + "name": "[format('{0}/{1}', parameters('registryName'), parameters('name'))]", + "properties": { + "actions": "[parameters('actions')]", + "description": "[parameters('description')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the scope map." + }, + "value": "[parameters('name')]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the scope map was created in." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the scope map." + }, + "value": "[resourceId('Microsoft.ContainerRegistry/registries/scopeMaps', parameters('registryName'), parameters('name'))]" + } + } + } + }, + "dependsOn": [ + "registry" + ] + }, + "registry_replications": { + "copy": { + "name": "registry_replications", + "count": "[length(coalesce(parameters('replications'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-Registry-Replication-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(parameters('replications'), createArray())[copyIndex()].name]" + }, + "registryName": { + "value": "[parameters('name')]" + }, + "location": { + "value": "[coalesce(parameters('replications'), createArray())[copyIndex()].location]" + }, + "regionEndpointEnabled": { + "value": "[tryGet(coalesce(parameters('replications'), createArray())[copyIndex()], 'regionEndpointEnabled')]" + }, + "zoneRedundancy": { + "value": "[tryGet(coalesce(parameters('replications'), createArray())[copyIndex()], 'zoneRedundancy')]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('replications'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "11610607853469346303" + }, + "name": "Azure Container Registry (ACR) Replications", + "description": "This module deploys an Azure Container Registry (ACR) Replication." + }, + "parameters": { + "registryName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent registry. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the replication." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.ContainerRegistry/registries/replications@2025-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "regionEndpointEnabled": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Specifies whether the replication regional endpoint is enabled. Requests will not be routed to a replication whose regional endpoint is disabled, however its data will continue to be synced with other replications." + } + }, + "zoneRedundancy": { + "type": "string", + "defaultValue": "Disabled", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "metadata": { + "description": "Optional. Whether or not zone redundancy is enabled for this container registry." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('46d3xbcp.res.containerregistry-registry-replication.{0}.{1}', replace('0.1.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "registry": { + "existing": true, + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2025-11-01", + "name": "[parameters('registryName')]" + }, + "replication": { + "type": "Microsoft.ContainerRegistry/registries/replications", + "apiVersion": "2025-11-01", + "name": "[format('{0}/{1}', parameters('registryName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "regionEndpointEnabled": "[parameters('regionEndpointEnabled')]", + "zoneRedundancy": "[parameters('zoneRedundancy')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the replication." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the replication." + }, + "value": "[resourceId('Microsoft.ContainerRegistry/registries/replications', parameters('registryName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the replication was created in." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('replication', '2025-11-01', 'full').location]" + } + } + } + }, + "dependsOn": [ + "registry" + ] + }, + "registry_credentialSets": { + "copy": { + "name": "registry_credentialSets", + "count": "[length(coalesce(parameters('credentialSets'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-Registry-CredentialSet-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(parameters('credentialSets'), createArray())[copyIndex()].name]" + }, + "registryName": { + "value": "[parameters('name')]" + }, + "managedIdentities": { + "value": "[coalesce(parameters('credentialSets'), createArray())[copyIndex()].managedIdentities]" + }, + "authCredentials": { + "value": "[coalesce(parameters('credentialSets'), createArray())[copyIndex()].authCredentials]" + }, + "loginServer": { + "value": "[coalesce(parameters('credentialSets'), createArray())[copyIndex()].loginServer]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "11999349049369037949" + }, + "name": "Container Registries Credential Sets", + "description": "This module deploys an ACR Credential Set." + }, + "definitions": { + "authCredentialsType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the credential." + } + }, + "usernameSecretIdentifier": { + "type": "string", + "metadata": { + "description": "Required. KeyVault Secret URI for accessing the username." + } + }, + "passwordSecretIdentifier": { + "type": "string", + "metadata": { + "description": "Required. KeyVault Secret URI for accessing the password." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for auth credentials." + } + }, + "managedIdentityOnlySysAssignedType": { + "type": "object", + "properties": { + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if only system-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.7.0" + } + } + } + }, + "parameters": { + "registryName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent registry. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the credential set." + } + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityOnlySysAssignedType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource." + } + }, + "authCredentials": { + "type": "array", + "items": { + "$ref": "#/definitions/authCredentialsType" + }, + "metadata": { + "description": "Required. List of authentication credentials stored for an upstream. Usually consists of a primary and an optional secondary credential." + } + }, + "loginServer": { + "type": "string", + "metadata": { + "description": "Required. The credentials are stored for this upstream or login server." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('46d3xbcp.res.containerregistry-registry-credset.{0}.{1}', replace('0.1.0', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "registry": { + "existing": true, + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2025-11-01", + "name": "[parameters('registryName')]" + }, + "credentialSet": { + "type": "Microsoft.ContainerRegistry/registries/credentialSets", + "apiVersion": "2025-11-01", + "name": "[format('{0}/{1}', parameters('registryName'), parameters('name'))]", + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), 'SystemAssigned', null())), null())]", + "properties": { + "authCredentials": "[parameters('authCredentials')]", + "loginServer": "[parameters('loginServer')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The Name of the Credential Set." + }, + "value": "[parameters('name')]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Credential Set." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the Credential Set." + }, + "value": "[resourceId('Microsoft.ContainerRegistry/registries/credentialSets', parameters('registryName'), parameters('name'))]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[tryGet(tryGet(reference('credentialSet', '2025-11-01', 'full'), 'identity'), 'principalId')]" + } + } + } + }, + "dependsOn": [ + "registry" + ] + }, + "registry_cacheRules": { + "copy": { + "name": "registry_cacheRules", + "count": "[length(coalesce(parameters('cacheRules'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-Registry-Cache-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "registryName": { + "value": "[parameters('name')]" + }, + "sourceRepository": { + "value": "[coalesce(parameters('cacheRules'), createArray())[copyIndex()].sourceRepository]" + }, + "name": { + "value": "[tryGet(coalesce(parameters('cacheRules'), createArray())[copyIndex()], 'name')]" + }, + "targetRepository": { + "value": "[coalesce(tryGet(coalesce(parameters('cacheRules'), createArray())[copyIndex()], 'targetRepository'), coalesce(parameters('cacheRules'), createArray())[copyIndex()].sourceRepository)]" + }, + "credentialSetResourceId": { + "value": "[tryGet(coalesce(parameters('cacheRules'), createArray())[copyIndex()], 'credentialSetResourceId')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "16057130579934063333" + }, + "name": "Container Registry Cache", + "description": "The cache for Azure Container Registry (Preview) feature allows users to cache container images in a private container registry. Cache for ACR, is a preview feature available in Basic, Standard, and Premium service tiers ([ref](https://learn.microsoft.com/en-us/azure/container-registry/tutorial-registry-cache))." + }, + "parameters": { + "registryName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent registry. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "defaultValue": "[replace(replace(replace(parameters('sourceRepository'), '/', '-'), '.', '-'), '*', '')]", + "metadata": { + "description": "Optional. The name of the cache rule. Will be derived from the source repository name if not defined." + } + }, + "sourceRepository": { + "type": "string", + "metadata": { + "description": "Required. Source repository pulled from upstream." + } + }, + "targetRepository": { + "type": "string", + "defaultValue": "[parameters('sourceRepository')]", + "metadata": { + "description": "Optional. Target repository specified in docker pull command. E.g.: docker pull myregistry.azurecr.io/{targetRepository}:{tag}." + } + }, + "credentialSetResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the credential store which is associated with the cache rule. Required only when pulling from authenticated upstream registries (e.g., Docker Hub). Omit for anonymous public registries such as MCR (mcr.microsoft.com)." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('46d3xbcp.res.containerregistry-registry-cacherule.{0}.{1}', replace('0.1.0', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "registry": { + "existing": true, + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2025-11-01", + "name": "[parameters('registryName')]" + }, + "cacheRule": { + "type": "Microsoft.ContainerRegistry/registries/cacheRules", + "apiVersion": "2025-11-01", + "name": "[format('{0}/{1}', parameters('registryName'), parameters('name'))]", + "properties": { + "sourceRepository": "[parameters('sourceRepository')]", + "targetRepository": "[parameters('targetRepository')]", + "credentialSetResourceId": "[parameters('credentialSetResourceId')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The Name of the Cache Rule." + }, + "value": "[parameters('name')]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Cache Rule." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the Cache Rule." + }, + "value": "[resourceId('Microsoft.ContainerRegistry/registries/cacheRules', parameters('registryName'), parameters('name'))]" + } + } + } + }, + "dependsOn": [ + "registry", + "registry_credentialSets" + ] + }, + "registry_tokens": { + "copy": { + "name": "registry_tokens", + "count": "[length(coalesce(parameters('tokens'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-Registry-Token-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(parameters('tokens'), createArray())[copyIndex()].name]" + }, + "registryName": { + "value": "[parameters('name')]" + }, + "scopeMapResourceId": { + "value": "[coalesce(parameters('tokens'), createArray())[copyIndex()].scopeMapResourceId]" + }, + "status": { + "value": "[tryGet(coalesce(parameters('tokens'), createArray())[copyIndex()], 'status')]" + }, + "credentials": { + "value": "[tryGet(coalesce(parameters('tokens'), createArray())[copyIndex()], 'credentials')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "2549463827710309021" + }, + "name": "Container Registries Tokens", + "description": "Deploys an Azure Container Registry (ACR) Token." + }, + "parameters": { + "registryName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent registry. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "minLength": 5, + "maxLength": 50, + "metadata": { + "description": "Required. The name of the token." + } + }, + "scopeMapResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the scope map to which the token will be associated with." + } + }, + "status": { + "type": "string", + "defaultValue": "enabled", + "allowedValues": [ + "disabled", + "enabled" + ], + "metadata": { + "description": "Optional. The status of the token. Default is enabled." + } + }, + "credentials": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.ContainerRegistry/registries/tokens@2025-11-01#properties/properties/properties/credentials" + }, + "description": "Optional. The credentials associated with the token for authentication." + }, + "nullable": true + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('46d3xbcp.res.containerregistry-registry-token.{0}.{1}', replace('0.1.0', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "registry": { + "existing": true, + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2025-11-01", + "name": "[parameters('registryName')]" + }, + "token": { + "type": "Microsoft.ContainerRegistry/registries/tokens", + "apiVersion": "2025-11-01", + "name": "[format('{0}/{1}', parameters('registryName'), parameters('name'))]", + "properties": { + "scopeMapId": "[parameters('scopeMapResourceId')]", + "status": "[parameters('status')]", + "credentials": "[if(not(empty(coalesce(parameters('credentials'), createArray()))), createObject('certificates', tryGet(parameters('credentials'), 'certificates'), 'passwords', tryGet(parameters('credentials'), 'passwords')), null())]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the token." + }, + "value": "[parameters('name')]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the token was created in." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the token." + }, + "value": "[resourceId('Microsoft.ContainerRegistry/registries/tokens', parameters('registryName'), parameters('name'))]" + } + } + } + }, + "dependsOn": [ + "registry", + "registry_scopeMaps" + ] + }, + "registry_tasks": { + "copy": { + "name": "registry_tasks", + "count": "[length(coalesce(parameters('tasks'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-Registry-Task-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "registryName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('tasks'), createArray())[copyIndex()].name]" + }, + "location": { + "value": "[coalesce(tryGet(coalesce(parameters('tasks'), createArray())[copyIndex()], 'location'), parameters('location'))]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('tasks'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "platform": { + "value": "[tryGet(coalesce(parameters('tasks'), createArray())[copyIndex()], 'platform')]" + }, + "step": { + "value": "[tryGet(coalesce(parameters('tasks'), createArray())[copyIndex()], 'step')]" + }, + "trigger": { + "value": "[tryGet(coalesce(parameters('tasks'), createArray())[copyIndex()], 'trigger')]" + }, + "status": { + "value": "[tryGet(coalesce(parameters('tasks'), createArray())[copyIndex()], 'status')]" + }, + "timeout": { + "value": "[tryGet(coalesce(parameters('tasks'), createArray())[copyIndex()], 'timeout')]" + }, + "agentConfiguration": { + "value": "[tryGet(coalesce(parameters('tasks'), createArray())[copyIndex()], 'agentConfiguration')]" + }, + "agentPoolName": { + "value": "[tryGet(coalesce(parameters('tasks'), createArray())[copyIndex()], 'agentPoolName')]" + }, + "credentials": { + "value": "[tryGet(coalesce(parameters('tasks'), createArray())[copyIndex()], 'credentials')]" + }, + "isSystemTask": { + "value": "[tryGet(coalesce(parameters('tasks'), createArray())[copyIndex()], 'isSystemTask')]" + }, + "logTemplate": { + "value": "[tryGet(coalesce(parameters('tasks'), createArray())[copyIndex()], 'logTemplate')]" + }, + "managedIdentities": { + "value": "[tryGet(coalesce(parameters('tasks'), createArray())[copyIndex()], 'managedIdentities')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "11575933255801787770" + }, + "name": "Container Registries Tasks", + "description": "Deploys an Azure Container Registry (ACR) Task that can be used to automate container image builds and other workflows ([ref](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-tasks-overview))." + }, + "definitions": { + "managedIdentityAllType": { + "type": "object", + "properties": { + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } + }, + "userAssignedResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.7.0" + } + } + } + }, + "parameters": { + "registryName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent registry. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "minLength": 5, + "maxLength": 50, + "metadata": { + "description": "Required. The name of the task." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.ContainerRegistry/registries/tasks@2025-03-01-preview#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "platform": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.ContainerRegistry/registries/tasks@2025-03-01-preview#properties/properties/properties/platform" + }, + "description": "Optional. The platform properties against which the task has to run." + }, + "nullable": true + }, + "step": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.ContainerRegistry/registries/tasks@2025-03-01-preview#properties/properties/properties/step" + }, + "description": "Optional. The task step properties. Exactly one of dockerBuildStep, encodedTaskStep, or fileTaskStep must be provided." + }, + "nullable": true + }, + "trigger": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.ContainerRegistry/registries/tasks@2025-03-01-preview#properties/properties/properties/trigger" + }, + "description": "Optional. The properties that describe all triggers for the task." + }, + "nullable": true + }, + "status": { + "type": "string", + "defaultValue": "Enabled", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "metadata": { + "description": "Optional. The current status of task." + } + }, + "timeout": { + "type": "int", + "defaultValue": 3600, + "minValue": 300, + "maxValue": 28800, + "metadata": { + "description": "Optional. Run timeout in seconds." + } + }, + "agentConfiguration": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.ContainerRegistry/registries/tasks@2025-03-01-preview#properties/properties/properties/agentConfiguration" + }, + "description": "Optional. The machine configuration of the run agent." + }, + "nullable": true + }, + "agentPoolName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The dedicated agent pool for the task." + } + }, + "credentials": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.ContainerRegistry/registries/tasks@2025-03-01-preview#properties/properties/properties/credentials" + }, + "description": "Optional. The properties that describe the credentials that will be used when the task is invoked." + }, + "nullable": true + }, + "isSystemTask": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. The value of this property indicates whether the task resource is system task or not." + } + }, + "logTemplate": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The template that describes the repository and tag information for run log artifact." + } + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityAllType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned, UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', 'None')), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]" + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('46d3xbcp.res.containerregistry-registry-task.{0}.{1}', replace('0.1.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "registry": { + "existing": true, + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2025-11-01", + "name": "[parameters('registryName')]" + }, + "task": { + "type": "Microsoft.ContainerRegistry/registries/tasks", + "apiVersion": "2025-03-01-preview", + "name": "[format('{0}/{1}', parameters('registryName'), parameters('name'))]", + "location": "[parameters('location')]", + "identity": "[variables('identity')]", + "tags": "[parameters('tags')]", + "properties": { + "agentConfiguration": "[parameters('agentConfiguration')]", + "agentPoolName": "[parameters('agentPoolName')]", + "credentials": "[parameters('credentials')]", + "isSystemTask": "[parameters('isSystemTask')]", + "logTemplate": "[parameters('logTemplate')]", + "platform": "[parameters('platform')]", + "status": "[parameters('status')]", + "step": "[parameters('step')]", + "timeout": "[parameters('timeout')]", + "trigger": "[parameters('trigger')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the task." + }, + "value": "[parameters('name')]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the task was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the task." + }, + "value": "[resourceId('Microsoft.ContainerRegistry/registries/tasks', parameters('registryName'), parameters('name'))]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('task', '2025-03-01-preview', 'full').location]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[tryGet(tryGet(reference('task', '2025-03-01-preview', 'full'), 'identity'), 'principalId')]" + } + } + } + }, + "dependsOn": [ + "registry" + ] + }, + "registry_webhooks": { + "copy": { + "name": "registry_webhooks", + "count": "[length(coalesce(parameters('webhooks'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-Registry-Webhook-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(parameters('webhooks'), createArray())[copyIndex()].name]" + }, + "registryName": { + "value": "[parameters('name')]" + }, + "location": { + "value": "[coalesce(tryGet(coalesce(parameters('webhooks'), createArray())[copyIndex()], 'location'), parameters('location'))]" + }, + "action": { + "value": "[tryGet(coalesce(parameters('webhooks'), createArray())[copyIndex()], 'action')]" + }, + "customHeaders": { + "value": "[tryGet(coalesce(parameters('webhooks'), createArray())[copyIndex()], 'customHeaders')]" + }, + "scope": { + "value": "[tryGet(coalesce(parameters('webhooks'), createArray())[copyIndex()], 'scope')]" + }, + "status": { + "value": "[tryGet(coalesce(parameters('webhooks'), createArray())[copyIndex()], 'status')]" + }, + "serviceUri": { + "value": "[coalesce(parameters('webhooks'), createArray())[copyIndex()].serviceUri]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('webhooks'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "7866842825441732262" + }, + "name": "Azure Container Registry (ACR) Webhooks", + "description": "This module deploys an Azure Container Registry (ACR) Webhook." + }, + "parameters": { + "registryName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent registry. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "defaultValue": "[format('{0}webhook', parameters('registryName'))]", + "minLength": 5, + "maxLength": 50, + "metadata": { + "description": "Optional. The name of the registry webhook." + } + }, + "serviceUri": { + "type": "securestring", + "metadata": { + "description": "Required. The service URI for the webhook to post notifications." + } + }, + "status": { + "type": "string", + "defaultValue": "enabled", + "allowedValues": [ + "disabled", + "enabled" + ], + "metadata": { + "description": "Optional. The status of the webhook at the time the operation was called." + } + }, + "action": { + "type": "array", + "items": { + "type": "string" + }, + "defaultValue": [ + "chart_delete", + "chart_push", + "delete", + "push", + "quarantine" + ], + "metadata": { + "description": "Optional. The list of actions that trigger the webhook to post notifications." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.ContainerRegistry/registries/webhooks@2025-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "customHeaders": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Custom headers that will be added to the webhook notifications." + } + }, + "scope": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The scope of repositories where the event can be triggered. For example, 'foo:*' means events for all tags under repository 'foo'. 'foo:bar' means events for 'foo:bar' only. 'foo' is equivalent to 'foo:latest'. Empty means all events." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('46d3xbcp.res.containerregistry-registry-webhook.{0}.{1}', replace('0.1.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "registry": { + "existing": true, + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2025-11-01", + "name": "[parameters('registryName')]" + }, + "webhook": { + "type": "Microsoft.ContainerRegistry/registries/webhooks", + "apiVersion": "2025-11-01", + "name": "[format('{0}/{1}', parameters('registryName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "actions": "[parameters('action')]", + "customHeaders": "[parameters('customHeaders')]", + "scope": "[parameters('scope')]", + "serviceUri": "[parameters('serviceUri')]", + "status": "[parameters('status')]" + } + } + }, + "outputs": { + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the webhook." + }, + "value": "[resourceId('Microsoft.ContainerRegistry/registries/webhooks', parameters('registryName'), parameters('name'))]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the webhook." + }, + "value": "[parameters('name')]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Azure container registry." + }, + "value": "[resourceGroup().name]" + }, + "actions": { + "type": "array", + "metadata": { + "description": "The actions of the webhook." + }, + "value": "[reference('webhook').actions]" + }, + "status": { + "type": "string", + "metadata": { + "description": "The status of the webhook." + }, + "value": "[reference('webhook').status]" + }, + "provistioningState": { + "type": "string", + "metadata": { + "description": "The provisioning state of the webhook." + }, + "value": "[reference('webhook').provisioningState]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('webhook', '2025-11-01', 'full').location]" + } + } + } + }, + "dependsOn": [ + "registry" + ] + }, + "registry_privateEndpoints": { + "copy": { + "name": "registry_privateEndpoints", + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-registry-PrivateEndpoint-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "subscriptionId": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[2]]", + "resourceGroup": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[4]]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'name'), format('pep-{0}-{1}-{2}', last(split(resourceId('Microsoft.ContainerRegistry/registries', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'registry'), copyIndex()))]" + }, + "privateLinkServiceConnections": "[if(not(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true())), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.ContainerRegistry/registries', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'registry'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.ContainerRegistry/registries', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'registry')))))), createObject('value', null()))]", + "manualPrivateLinkServiceConnections": "[if(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true()), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.ContainerRegistry/registries', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'registry'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.ContainerRegistry/registries', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'registry')), 'requestMessage', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'manualConnectionRequestMessage'), 'Manual approval required.'))))), createObject('value', null()))]", + "subnetResourceId": { + "value": "[coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + }, + "location": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'location'), reference(split(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location)]" + }, + "lock": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'lock'), parameters('lock'))]" + }, + "privateDnsZoneGroup": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateDnsZoneGroup')]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "customDnsConfigs": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customDnsConfigs')]" + }, + "ipConfigurations": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'ipConfigurations')]" + }, + "applicationSecurityGroupResourceIds": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'applicationSecurityGroupResourceIds')]" + }, + "customNetworkInterfaceName": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customNetworkInterfaceName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "18436885663402767850" + }, + "name": "Private Endpoints", + "description": "This module deploys a Private Endpoint." + }, + "definitions": { + "privateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "metadata": { + "description": "Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a private dns zone group." + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.7.0" + } + } + }, + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "description": "The type of a private DNS zone group configuration.", + "__bicep_imported_from!": { + "sourceTemplate": "private-dns-zone-group/main.bicep" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.7.0" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the private endpoint resource to create." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the private endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The custom name of the network interface attached to the private endpoint." + } + }, + "ipConfigurations": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2025-05-01#properties/properties/properties/ipConfigurations" + }, + "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." + }, + "nullable": true + }, + "ipVersionType": { + "type": "string", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2025-05-01#properties/properties/properties/ipVersionType" + }, + "description": "Optional. Specifies the IP version type for the private IPs of the private endpoint. If not defined, this defaults to IPv4." + }, + "defaultValue": "IPv4" + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/privateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS zone group to configure for the private endpoint." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2025-05-01#properties/tags" + }, + "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." + }, + "nullable": true + }, + "customDnsConfigs": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2025-05-01#properties/properties/properties/customDnsConfigs" + }, + "description": "Optional. Custom DNS configurations." + }, + "nullable": true + }, + "manualPrivateLinkServiceConnections": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2025-05-01#properties/properties/properties/manualPrivateLinkServiceConnections" + }, + "description": "Conditional. A grouping of information about the connection to the remote resource. Used when the network admin does not have access to approve connections to the remote resource. Required if `privateLinkServiceConnections` is empty." + }, + "nullable": true + }, + "privateLinkServiceConnections": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2025-05-01#properties/properties/properties/privateLinkServiceConnections" + }, + "description": "Conditional. A grouping of information about the connection to the remote resource. Required if `manualPrivateLinkServiceConnections` is empty." + }, + "nullable": true + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", + "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", + "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", + "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('46d3xbcp.res.network-privateendpoint.{0}.{1}', replace('0.12.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "privateEndpoint": { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2025-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "copy": [ + { + "name": "applicationSecurityGroups", + "count": "[length(coalesce(parameters('applicationSecurityGroupResourceIds'), createArray()))]", + "input": { + "id": "[coalesce(parameters('applicationSecurityGroupResourceIds'), createArray())[copyIndex('applicationSecurityGroups')]]" + } + } + ], + "customDnsConfigs": "[coalesce(parameters('customDnsConfigs'), createArray())]", + "customNetworkInterfaceName": "[coalesce(parameters('customNetworkInterfaceName'), '')]", + "ipConfigurations": "[coalesce(parameters('ipConfigurations'), createArray())]", + "manualPrivateLinkServiceConnections": "[coalesce(parameters('manualPrivateLinkServiceConnections'), createArray())]", + "privateLinkServiceConnections": "[coalesce(parameters('privateLinkServiceConnections'), createArray())]", + "subnet": { + "id": "[parameters('subnetResourceId')]" + }, + "ipVersionType": "[parameters('ipVersionType')]" + } + }, + "privateEndpoint_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_roleAssignments": { + "copy": { + "name": "privateEndpoint_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateEndpoints', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_privateDnsZoneGroup": { + "condition": "[not(empty(parameters('privateDnsZoneGroup')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[tryGet(parameters('privateDnsZoneGroup'), 'name')]" + }, + "privateEndpointName": { + "value": "[parameters('name')]" + }, + "privateDnsZoneConfigs": { + "value": "[parameters('privateDnsZoneGroup').privateDnsZoneGroupConfigs]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.41.2.15936", + "templateHash": "9935179114830442414" + }, + "name": "Private Endpoint Private DNS Zone Groups", + "description": "This module deploys a Private Endpoint Private DNS Zone Group." + }, + "definitions": { + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a private DNS zone group configuration." + } + } + }, + "parameters": { + "privateEndpointName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent private endpoint. Required if the template is used in a standalone deployment." + } + }, + "privateDnsZoneConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "minLength": 1, + "maxLength": 5, + "metadata": { + "description": "Required. Array of private DNS zone configurations of the private DNS zone group. A DNS zone group can support up to 5 DNS zones." + } + }, + "name": { + "type": "string", + "defaultValue": "default", + "metadata": { + "description": "Optional. The name of the private DNS zone group." + } + } + }, + "resources": { + "privateEndpoint": { + "existing": true, + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2025-05-01", + "name": "[parameters('privateEndpointName')]" + }, + "privateDnsZoneGroup": { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2025-05-01", + "name": "[format('{0}/{1}', parameters('privateEndpointName'), parameters('name'))]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneConfigs'))]", + "input": { + "name": "[coalesce(tryGet(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigs')], 'name'), last(split(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigs')].privateDnsZoneResourceId, '/')))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigs')].privateDnsZoneResourceId]" + } + } + } + ] + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint DNS zone group." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint DNS zone group." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints/privateDnsZoneGroups', parameters('privateEndpointName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint DNS zone group was deployed into." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "privateEndpoint" + ] + } + }, + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + }, + "value": "[parameters('name')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('privateEndpoint', '2025-05-01', 'full').location]" + }, + "customDnsConfigs": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2025-05-01#properties/properties/properties/customDnsConfigs", + "output": true + }, + "description": "The custom DNS configurations of the private endpoint." + }, + "value": "[reference('privateEndpoint').customDnsConfigs]" + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The resource IDs of the network interfaces associated with the private endpoint." + }, + "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group Id for the private endpoint Group." + }, + "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" + } + } + } + }, + "dependsOn": [ + "registry", + "registry_replications" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The Name of the Azure container registry." + }, + "value": "[parameters('name')]" + }, + "loginServer": { + "type": "string", + "metadata": { + "description": "The reference to the Azure container registry." + }, + "value": "[reference('registry').loginServer]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Azure container registry." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the Azure container registry." + }, + "value": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('name'))]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[tryGet(tryGet(reference('registry', '2025-06-01-preview', 'full'), 'identity'), 'principalId')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('registry', '2025-06-01-preview', 'full').location]" + }, + "credentialSetsSystemAssignedMIPrincipalIds": { + "type": "array", + "metadata": { + "description": "The Principal IDs of the ACR Credential Sets system-assigned identities." + }, + "copy": { + "count": "[length(range(0, length(coalesce(parameters('credentialSets'), createArray()))))]", + "input": "[tryGet(tryGet(reference(format('registry_credentialSets[{0}]', range(0, length(coalesce(parameters('credentialSets'), createArray())))[copyIndex()])).outputs, 'systemAssignedMIPrincipalId'), 'value')]" + } + }, + "credentialSetsResourceIds": { + "type": "array", + "metadata": { + "description": "The Resource IDs of the ACR Credential Sets." + }, + "copy": { + "count": "[length(range(0, length(coalesce(parameters('credentialSets'), createArray()))))]", + "input": "[reference(format('registry_credentialSets[{0}]', range(0, length(coalesce(parameters('credentialSets'), createArray())))[copyIndex()])).outputs.resourceId.value]" + } + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointOutputType" + }, + "metadata": { + "description": "The private endpoints of the Azure container registry." + }, + "copy": { + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]", + "input": { + "name": "[reference(format('registry_privateEndpoints[{0}]', copyIndex())).outputs.name.value]", + "resourceId": "[reference(format('registry_privateEndpoints[{0}]', copyIndex())).outputs.resourceId.value]", + "groupId": "[tryGet(tryGet(reference(format('registry_privateEndpoints[{0}]', copyIndex())).outputs, 'groupId'), 'value')]", + "customDnsConfigs": "[reference(format('registry_privateEndpoints[{0}]', copyIndex())).outputs.customDnsConfigs.value]", + "networkInterfaceResourceIds": "[reference(format('registry_privateEndpoints[{0}]', copyIndex())).outputs.networkInterfaceResourceIds.value]" + } + } + } + } + } + }, + "dependsOn": [ + "userAssignedIdentity" + ] + }, + "containerApp": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('avm.res.app.container-app.{0}', variables('containerAppResourceName')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('containerAppResourceName')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" + }, + "environmentResourceId": { + "value": "[reference('containerAppEnvironment').outputs.resourceId.value]" + }, + "managedIdentities": { + "value": { + "userAssignedResourceIds": [ + "[reference('userAssignedIdentity').outputs.resourceId.value]" + ] + } + }, + "ingressTargetPort": { + "value": 8000 + }, + "ingressExternal": { + "value": true + }, + "activeRevisionsMode": { + "value": "Single" + }, + "ingressAllowInsecure": { + "value": false + }, + "corsPolicy": { + "value": { + "allowedOrigins": [ + "[format('https://{0}.azurewebsites.net', variables('webSiteResourceName'))]", + "[format('http://{0}.azurewebsites.net', variables('webSiteResourceName'))]" + ], + "allowedMethods": [ + "GET", + "POST", + "PUT", + "DELETE", + "OPTIONS" + ] + } + }, + "scaleSettings": { + "value": { + "maxReplicas": "[if(parameters('enableScalability'), 3, 1)]", + "minReplicas": "[if(parameters('enableScalability'), 1, 1)]", + "rules": [ + { + "name": "http-scaler", + "http": { + "metadata": { + "concurrentRequests": "100" + } + } + } + ] + } + }, + "registries": { + "value": [ + { + "server": "[reference('containerRegistry').outputs.loginServer.value]", + "identity": "[reference('userAssignedIdentity').outputs.resourceId.value]" + } + ] + }, + "containers": { + "value": [ + { + "name": "backend", + "image": "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest", + "resources": { + "cpu": "2.0", + "memory": "4.0Gi" + }, + "env": [ + { + "name": "COSMOSDB_ENDPOINT", + "value": "[format('https://{0}.documents.azure.com:443/', variables('cosmosDbResourceName'))]" + }, + { + "name": "COSMOSDB_DATABASE", + "value": "[variables('cosmosDbDatabaseName')]" + }, + { + "name": "COSMOSDB_CONTAINER", + "value": "[variables('cosmosDbDatabaseMemoryContainerName')]" + }, + { + "name": "AZURE_OPENAI_ENDPOINT", + "value": "[format('https://{0}.openai.azure.com/', variables('aiFoundryAiServicesResourceName'))]" + }, + { + "name": "AZURE_OPENAI_MODEL_NAME", + "value": "[variables('aiFoundryAiServicesModelDeployment').name]" + }, + { + "name": "AZURE_OPENAI_DEPLOYMENT_NAME", + "value": "[variables('aiFoundryAiServicesModelDeployment').deploymentName]" + }, + { + "name": "AZURE_OPENAI_RAI_DEPLOYMENT_NAME", + "value": "[variables('aiFoundryAiServices4_1ModelDeployment').deploymentName]" + }, + { + "name": "AZURE_OPENAI_API_VERSION", + "value": "[parameters('azureOpenaiAPIVersion')]" + }, + { + "name": "APPLICATIONINSIGHTS_INSTRUMENTATION_KEY", + "value": "[if(parameters('enableMonitoring'), reference('applicationInsights').outputs.instrumentationKey.value, '')]" + }, + { + "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", + "value": "[if(parameters('enableMonitoring'), reference('applicationInsights').outputs.connectionString.value, '')]" + }, + { + "name": "AZURE_AI_SUBSCRIPTION_ID", + "value": "[variables('aiFoundryAiServicesSubscriptionId')]" + }, + { + "name": "AZURE_AI_RESOURCE_GROUP", + "value": "[variables('aiFoundryAiServicesResourceGroupName')]" + }, + { + "name": "AZURE_AI_PROJECT_NAME", + "value": "[if(variables('useExistingAiFoundryAiProject'), variables('aiFoundryAiProjectResourceName'), reference('aiFoundryAiServicesProject').outputs.name.value)]" + }, + { + "name": "FRONTEND_SITE_NAME", + "value": "[format('https://{0}.azurewebsites.net', variables('webSiteResourceName'))]" + }, + { + "name": "AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME", + "value": "[variables('aiFoundryAiServicesModelDeployment').deploymentName]" + }, + { + "name": "APP_ENV", + "value": "Prod" + }, + { + "name": "AZURE_AI_SEARCH_CONNECTION_NAME", + "value": "[variables('aiSearchConnectionName')]" + }, + { + "name": "AZURE_AI_SEARCH_ENDPOINT", + "value": "[reference('searchServiceUpdate').outputs.endpoint.value]" + }, + { + "name": "AZURE_COGNITIVE_SERVICES", + "value": "https://cognitiveservices.azure.com/.default" + }, + { + "name": "AZURE_BING_CONNECTION_NAME", + "value": "binggrnd" + }, + { + "name": "BING_CONNECTION_NAME", + "value": "binggrnd" + }, + { + "name": "REASONING_MODEL_NAME", + "value": "[variables('aiFoundryAiServicesReasoningModelDeployment').deploymentName]" + }, + { + "name": "MCP_SERVER_ENDPOINT", + "value": "[format('https://{0}/mcp', reference('containerAppMcp').outputs.fqdn.value)]" + }, + { + "name": "MCP_SERVER_NAME", + "value": "MacaeMcpServer" + }, + { + "name": "MCP_SERVER_DESCRIPTION", + "value": "MCP server with greeting, HR, and planning tools" + }, + { + "name": "AZURE_TENANT_ID", + "value": "[tenant().tenantId]" + }, + { + "name": "AZURE_CLIENT_ID", + "value": "[reference('userAssignedIdentity').outputs.clientId.value]" + }, + { + "name": "SUPPORTED_MODELS", + "value": "[format('[\"{0}\",\"{1}\",\"{2}\"]', variables('aiFoundryAiServicesModelDeployment').deploymentName, variables('aiFoundryAiServices4_1ModelDeployment').deploymentName, variables('aiFoundryAiServicesReasoningModelDeployment').deploymentName)]" + }, + { + "name": "AZURE_STORAGE_BLOB_URL", + "value": "[reference('avmStorageAccount').outputs.serviceEndpoints.value.blob]" + }, + { + "name": "AZURE_AI_PROJECT_ENDPOINT", + "value": "[if(variables('useExistingAiFoundryAiProject'), reference('existingAiFoundryAiServicesProject').endpoints['AI Foundry API'], reference('aiFoundryAiServicesProject').outputs.apiEndpoint.value)]" + }, + { + "name": "AZURE_AI_AGENT_ENDPOINT", + "value": "[if(variables('useExistingAiFoundryAiProject'), reference('existingAiFoundryAiServicesProject').endpoints['AI Foundry API'], reference('aiFoundryAiServicesProject').outputs.apiEndpoint.value)]" + }, + { + "name": "AZURE_AI_AGENT_API_VERSION", + "value": "[parameters('azureAiAgentAPIVersion')]" + }, + { + "name": "AZURE_AI_AGENT_PROJECT_CONNECTION_STRING", + "value": "[format('{0}.services.ai.azure.com;{1};{2};{3}', variables('aiFoundryAiServicesResourceName'), variables('aiFoundryAiServicesSubscriptionId'), variables('aiFoundryAiServicesResourceGroupName'), variables('aiFoundryAiProjectResourceName'))]" + }, + { + "name": "AZURE_BASIC_LOGGING_LEVEL", + "value": "INFO" + }, + { + "name": "AZURE_PACKAGE_LOGGING_LEVEL", + "value": "WARNING" + }, + { + "name": "AZURE_LOGGING_PACKAGES", + "value": "" + } + ] + } + ] + }, + "secrets": { + "value": [] + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.42.1.51946", + "templateHash": "14994565436716579607" + }, + "name": "Container Apps", + "description": "This module deploys a Container App." + }, + "definitions": { + "ingressPortMappingType": { + "type": "object", + "properties": { + "exposedPort": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the exposed port for the target port. If not specified, it defaults to target port." + } + }, + "external": { + "type": "bool", + "metadata": { + "description": "Required. Specifies whether the app port is accessible outside of the environment." + } + }, + "targetPort": { + "type": "int", + "metadata": { + "description": "Required. Specifies the port the container listens on." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for an ingress port mapping." + } + }, + "serviceBindingType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the service." + } + }, + "serviceId": { + "type": "string", + "metadata": { + "description": "Required. The service ID." + } + } + }, + "metadata": { + "description": "The type for a service binding." + } + }, + "environmentVarType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Environment variable name." + } + }, + "secretRef": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the Container App secret from which to pull the environment variable value." + } + }, + "value": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Non-secret environment variable value." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for an environment variable." + } + }, + "containerAppProbeType": { + "type": "object", + "properties": { + "failureThreshold": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 10, + "metadata": { + "description": "Optional. Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3." + } + }, + "httpGet": { + "$ref": "#/definitions/containerAppProbeHttpGetType", + "nullable": true, + "metadata": { + "description": "Optional. HTTPGet specifies the http request to perform." + } + }, + "initialDelaySeconds": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 60, + "metadata": { + "description": "Optional. Number of seconds after the container has started before liveness probes are initiated." + } + }, + "periodSeconds": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 240, + "metadata": { + "description": "Optional. How often (in seconds) to perform the probe. Default to 10 seconds." + } + }, + "successThreshold": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 10, + "metadata": { + "description": "Optional. Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup." + } + }, + "tcpSocket": { + "$ref": "#/definitions/containerAppProbeTcpSocketType", + "nullable": true, + "metadata": { + "description": "Optional. The TCP socket specifies an action involving a TCP port. TCP hooks not yet supported." + } + }, + "terminationGracePeriodSeconds": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is an alpha field and requires enabling ProbeTerminationGracePeriod feature gate. Maximum value is 3600 seconds (1 hour)." + } + }, + "timeoutSeconds": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 240, + "metadata": { + "description": "Optional. Number of seconds after which the probe times out. Defaults to 1 second." + } + }, + "type": { + "type": "string", + "allowedValues": [ + "Liveness", + "Readiness", + "Startup" + ], + "nullable": true, + "metadata": { + "description": "Optional. The type of probe." + } + } + }, + "metadata": { + "description": "The type for a container app probe." + } + }, + "corsPolicyType": { + "type": "object", + "properties": { + "allowCredentials": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Switch to determine whether the resource allows credentials." + } + }, + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the content for the access-control-allow-headers header." + } + }, + "allowedMethods": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the content for the access-control-allow-methods header." + } + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the content for the access-control-allow-origins header." + } + }, + "exposeHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the content for the access-control-expose-headers header." + } + }, + "maxAge": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the content for the access-control-max-age header." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a CORS policy." + } + }, + "containerAppProbeHttpGetType": { + "type": "object", + "properties": { + "host": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Host name to connect to. Defaults to the pod IP." + } + }, + "httpHeaders": { + "type": "array", + "items": { + "$ref": "#/definitions/containerAppProbeHttpGetHeadersItemType" + }, + "nullable": true, + "metadata": { + "description": "Optional. HTTP headers to set in the request." + } + }, + "path": { + "type": "string", + "metadata": { + "description": "Required. Path to access on the HTTP server." + } + }, + "port": { + "type": "int", "metadata": { "description": "Required. Name or number of the port to access on the container." } @@ -40146,6 +44039,7 @@ "avmStorageAccount", "containerAppEnvironment", "containerAppMcp", + "containerRegistry", "existingAiFoundryAiServicesProject", "searchServiceUpdate", "userAssignedIdentity" @@ -40219,11 +44113,19 @@ ] } }, + "registries": { + "value": [ + { + "server": "[reference('containerRegistry').outputs.loginServer.value]", + "identity": "[reference('userAssignedIdentity').outputs.resourceId.value]" + } + ] + }, "containers": { "value": [ { "name": "mcp", - "image": "[format('{0}/{1}:{2}', parameters('MCPContainerRegistryHostname'), parameters('MCPContainerImageName'), parameters('MCPContainerImageTag'))]", + "image": "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest", "resources": { "cpu": "2.0", "memory": "4.0Gi" @@ -41777,6 +45679,7 @@ }, "dependsOn": [ "containerAppEnvironment", + "containerRegistry", "userAssignedIdentity" ] }, @@ -42526,13 +46429,17 @@ }, "managedIdentities": { "value": { - "systemAssigned": true + "userAssignedResourceIds": [ + "[reference('userAssignedIdentity').outputs.resourceId.value]" + ] } }, "siteConfig": { "value": { - "linuxFxVersion": "[format('DOCKER|{0}/{1}:{2}', parameters('frontendContainerRegistryHostname'), parameters('frontendContainerImageName'), parameters('frontendContainerImageTag'))]", - "minTlsVersion": "1.2" + "linuxFxVersion": "DOCKER|mcr.microsoft.com/appsvc/staticsite:latest", + "minTlsVersion": "1.2", + "acrUseManagedIdentityCreds": true, + "acrUserManagedIdentityID": "[reference('userAssignedIdentity').outputs.clientId.value]" } }, "configs": { @@ -42541,7 +46448,7 @@ "name": "appsettings", "properties": { "SCM_DO_BUILD_DURING_DEPLOYMENT": "true", - "DOCKER_REGISTRY_SERVER_URL": "[format('https://{0}', parameters('frontendContainerRegistryHostname'))]", + "DOCKER_REGISTRY_SERVER_URL": "[format('https://{0}', reference('containerRegistry').outputs.loginServer.value)]", "WEBSITES_PORT": "3000", "WEBSITES_CONTAINER_START_TIME_LIMIT": "1800", "BACKEND_API_URL": "[format('https://{0}', reference('containerApp').outputs.fqdn.value)]", @@ -42569,8 +46476,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "18345308984648474640" + "version": "0.44.1.10279", + "templateHash": "7693002176758630395" } }, "definitions": { @@ -43601,8 +47508,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "1009721598684973971" + "version": "0.44.1.10279", + "templateHash": "16045336481224683590" }, "name": "Site App Settings", "description": "This module deploys a Site App Setting." @@ -44477,7 +48384,9 @@ "dependsOn": [ "applicationInsights", "containerApp", + "containerRegistry", "logAnalyticsWorkspace", + "userAssignedIdentity", "virtualNetwork", "webServerFarm" ] @@ -54851,8 +58760,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.43.8.12551", - "templateHash": "9739523049889844356" + "version": "0.44.1.10279", + "templateHash": "1221855634359106261" } }, "parameters": { @@ -54973,11 +58882,11 @@ }, "AZURE_OPENAI_DEPLOYMENT_NAME": { "type": "string", - "value": "[variables('aiFoundryAiServicesModelDeployment').name]" + "value": "[variables('aiFoundryAiServicesModelDeployment').deploymentName]" }, "AZURE_OPENAI_RAI_DEPLOYMENT_NAME": { "type": "string", - "value": "[variables('aiFoundryAiServices4_1ModelDeployment').name]" + "value": "[variables('aiFoundryAiServices4_1ModelDeployment').deploymentName]" }, "AZURE_OPENAI_API_VERSION": { "type": "string", @@ -54995,9 +58904,13 @@ "type": "string", "value": "[if(variables('useExistingAiFoundryAiProject'), variables('aiFoundryAiProjectResourceName'), reference('aiFoundryAiServicesProject').outputs.name.value)]" }, + "AZURE_AI_MODEL_DEPLOYMENT_NAME": { + "type": "string", + "value": "[variables('aiFoundryAiServicesModelDeployment').deploymentName]" + }, "AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME": { "type": "string", - "value": "[variables('aiFoundryAiServicesModelDeployment').name]" + "value": "[variables('aiFoundryAiServicesModelDeployment').deploymentName]" }, "APP_ENV": { "type": "string", @@ -55033,7 +58946,7 @@ }, "REASONING_MODEL_NAME": { "type": "string", - "value": "[variables('aiFoundryAiServicesReasoningModelDeployment').name]" + "value": "[variables('aiFoundryAiServicesReasoningModelDeployment').deploymentName]" }, "MCP_SERVER_NAME": { "type": "string", @@ -55045,7 +58958,7 @@ }, "SUPPORTED_MODELS": { "type": "string", - "value": "[[\"o3\",\"o4-mini\",\"gpt-4.1\",\"gpt-4.1-mini\"]" + "value": "[format('[\"{0}\",\"{1}\",\"{2}\"]', variables('aiFoundryAiServicesModelDeployment').deploymentName, variables('aiFoundryAiServices4_1ModelDeployment').deploymentName, variables('aiFoundryAiServicesReasoningModelDeployment').deploymentName)]" }, "BACKEND_URL": { "type": "string", @@ -55067,6 +58980,10 @@ "type": "string", "value": "[format('{0}.services.ai.azure.com;{1};{2};{3}', variables('aiFoundryAiServicesResourceName'), variables('aiFoundryAiServicesSubscriptionId'), variables('aiFoundryAiServicesResourceGroupName'), variables('aiFoundryAiProjectResourceName'))]" }, + "AZURE_DEV_COLLECT_TELEMETRY": { + "type": "string", + "value": "no" + }, "AZURE_STORAGE_CONTAINER_NAME_RETAIL_CUSTOMER": { "type": "string", "value": "[parameters('storageContainerNameRetailCustomer')]" @@ -55130,6 +59047,46 @@ "AZURE_AI_SEARCH_INDEX_NAME_CONTRACT_COMPLIANCE": { "type": "string", "value": "[variables('aiSearchIndexNameForContractCompliance')]" + }, + "AZURE_CONTAINER_REGISTRY_ENDPOINT": { + "type": "string", + "value": "[reference('containerRegistry').outputs.loginServer.value]" + }, + "AZURE_CONTAINER_REGISTRY_NAME": { + "type": "string", + "value": "[reference('containerRegistry').outputs.name.value]" + }, + "AZURE_RESOURCE_GROUP": { + "type": "string", + "value": "[resourceGroup().name]" + }, + "BACKEND_CONTAINER_APP_NAME": { + "type": "string", + "value": "[reference('containerApp').outputs.name.value]" + }, + "MCP_CONTAINER_APP_NAME": { + "type": "string", + "value": "[reference('containerAppMcp').outputs.name.value]" + }, + "FRONTEND_WEB_APP_NAME": { + "type": "string", + "value": "[reference('webSite').outputs.name.value]" + }, + "BACKEND_IMAGE_NAME": { + "type": "string", + "value": "macaebackend" + }, + "FRONTEND_IMAGE_NAME": { + "type": "string", + "value": "macaefrontend" + }, + "MCP_IMAGE_NAME": { + "type": "string", + "value": "macaemcp" + }, + "FRONTEND_WEBSITES_PORT": { + "type": "string", + "value": "3000" } } } \ No newline at end of file diff --git a/infra/main.parameters.json b/infra/main.parameters.json index 7aa975400..53c5f72b3 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -20,6 +20,9 @@ "gptModelVersion": { "value": "${AZURE_ENV_GPT_MODEL_VERSION}" }, + "gptDeploymentName": { + "value": "${AZURE_ENV_GPT_DEPLOYMENT_NAME}" + }, "gptDeploymentCapacity": { "value": "${AZURE_ENV_GPT_MODEL_CAPACITY}" }, @@ -32,6 +35,9 @@ "gpt4_1ModelVersion": { "value": "${AZURE_ENV_MODEL_4_1_VERSION}" }, + "gpt4_1DeploymentName": { + "value": "${AZURE_ENV_MODEL_4_1_DEPLOYMENT_NAME}" + }, "gpt4_1ModelCapacity": { "value": "${AZURE_ENV_MODEL_4_1_CAPACITY}" }, @@ -44,6 +50,9 @@ "gptReasoningModelVersion": { "value": "${AZURE_ENV_REASONING_MODEL_VERSION}" }, + "gptReasoningDeploymentName": { + "value": "${AZURE_ENV_REASONING_DEPLOYMENT_NAME}" + }, "gptReasoningModelCapacity": { "value": "${AZURE_ENV_REASONING_MODEL_CAPACITY}" }, diff --git a/infra/main.waf.parameters.json b/infra/main.waf.parameters.json index bb551e202..04d377647 100644 --- a/infra/main.waf.parameters.json +++ b/infra/main.waf.parameters.json @@ -20,6 +20,9 @@ "gptModelVersion": { "value": "${AZURE_ENV_GPT_MODEL_VERSION}" }, + "gptDeploymentName": { + "value": "${AZURE_ENV_GPT_DEPLOYMENT_NAME}" + }, "gptDeploymentCapacity": { "value": "${AZURE_ENV_GPT_MODEL_CAPACITY}" }, @@ -32,6 +35,9 @@ "gpt4_1ModelVersion": { "value": "${AZURE_ENV_MODEL_4_1_VERSION}" }, + "gpt4_1DeploymentName": { + "value": "${AZURE_ENV_MODEL_4_1_DEPLOYMENT_NAME}" + }, "gpt4_1ModelCapacity": { "value": "${AZURE_ENV_MODEL_4_1_CAPACITY}" }, @@ -44,6 +50,9 @@ "gptReasoningModelVersion": { "value": "${AZURE_ENV_REASONING_MODEL_VERSION}" }, + "gptReasoningDeploymentName": { + "value": "${AZURE_ENV_REASONING_DEPLOYMENT_NAME}" + }, "gptReasoningModelCapacity": { "value": "${AZURE_ENV_REASONING_MODEL_CAPACITY}" }, diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index f48b78ad4..186a28e75 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -31,15 +31,14 @@ param location string var deployerInfo = deployer() var deployingUserPrincipalId = deployerInfo.objectId -// Restricting deployment to only supported Azure OpenAI regions validated with GPT-4o model +// Restricting deployment to only supported Azure OpenAI regions validated with GPT-5 models @allowed(['australiaeast', 'eastus2', 'francecentral', 'japaneast', 'norwayeast', 'swedencentral', 'uksouth', 'westus']) @metadata({ azd: { type: 'location' usageName: [ - 'OpenAI.GlobalStandard.gpt4.1, 150' - 'OpenAI.GlobalStandard.o4-mini, 50' - 'OpenAI.GlobalStandard.gpt4.1-mini, 50' + 'OpenAI.GlobalStandard.gpt-5, 150' + 'OpenAI.GlobalStandard.gpt-5-mini, 100' ] } }) @@ -47,25 +46,36 @@ var deployingUserPrincipalId = deployerInfo.objectId param azureAiServiceLocation string @minLength(1) -@description('Optional. Name of the GPT model to deploy:') -param gptModelName string = 'gpt-4.1-mini' +@description('Optional. Name of the underlying GPT model to deploy. Defaults to gpt-5-mini (2025-08-07 series).') +param gptModelName string = 'gpt-5-mini' -@description('Optional. Version of the GPT model to deploy. Defaults to 2025-04-14.') -param gptModelVersion string = '2025-04-14' +@description('Optional. Version of the GPT model to deploy. Defaults to 2025-08-07.') +param gptModelVersion string = '2025-08-07' + +@description('Optional. Deployment (alias) name used in Azure OpenAI for the main GPT model. This is the value the application uses as `deployment_name` (including in data/agent_teams/*.json). Defaults to gptModelName.') +param gptDeploymentName string = gptModelName @minLength(1) -@description('Optional. Name of the GPT model to deploy:') -param gpt4_1ModelName string = 'gpt-4.1' +@description('Optional. Name of the underlying larger GPT model to deploy. Defaults to gpt-5 (2025-08-07 series).') +param gpt4_1ModelName string = 'gpt-5' + +@description('Optional. Version of the larger GPT model to deploy. Defaults to 2025-08-07.') +param gpt4_1ModelVersion string = '2025-08-07' -@description('Optional. Version of the GPT model to deploy. Defaults to 2025-04-14.') -param gpt4_1ModelVersion string = '2025-04-14' +@description('Optional. Deployment (alias) name used in Azure OpenAI for the larger GPT model. Defaults to gpt4_1ModelName.') +param gpt4_1DeploymentName string = gpt4_1ModelName @minLength(1) -@description('Optional. Name of the GPT Reasoning model to deploy:') -param gptReasoningModelName string = 'o4-mini' +@description('Optional. Name of the underlying GPT Reasoning model to deploy. Defaults to gpt-5-mini (reasoning-capable).') +param gptReasoningModelName string = 'gpt-5-mini' + +@description('Optional. Version of the GPT Reasoning model to deploy. Defaults to 2025-08-07.') +param gptReasoningModelVersion string = '2025-08-07' -@description('Optional. Version of the GPT Reasoning model to deploy. Defaults to 2025-04-16.') -param gptReasoningModelVersion string = '2025-04-16' +@description('Optional. Deployment (alias) name used in Azure OpenAI for the reasoning model. Must be unique from gptDeploymentName. Defaults to "{gptReasoningModelName}-reasoning" when it would otherwise collide with gptDeploymentName, otherwise gptReasoningModelName.') +param gptReasoningDeploymentName string = gptReasoningModelName == gptModelName + ? '${gptReasoningModelName}-reasoning' + : gptReasoningModelName @description('Optional. Version of the Azure OpenAI service to deploy. Defaults to 2024-12-01-preview.') param azureOpenaiAPIVersion string = '2024-12-01-preview' @@ -769,6 +779,7 @@ var aiFoundryAiProjectResourceName = useExistingAiFoundryAiProject : 'proj-${solutionSuffix}' // AI Project resource id: /subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//projects/ var aiFoundryAiServicesModelDeployment = { format: 'OpenAI' + deploymentName: gptDeploymentName name: gptModelName version: gptModelVersion sku: { @@ -779,6 +790,7 @@ var aiFoundryAiServicesModelDeployment = { } var aiFoundryAiServices4_1ModelDeployment = { format: 'OpenAI' + deploymentName: gpt4_1DeploymentName name: gpt4_1ModelName version: gpt4_1ModelVersion sku: { @@ -789,6 +801,7 @@ var aiFoundryAiServices4_1ModelDeployment = { } var aiFoundryAiServicesReasoningModelDeployment = { format: 'OpenAI' + deploymentName: gptReasoningDeploymentName name: gptReasoningModelName version: gptReasoningModelVersion sku: { @@ -811,7 +824,7 @@ module existingAiFoundryAiServicesDeployments 'modules/ai-services-deployments.b name: existingAiFoundryAiServices.name deployments: [ { - name: aiFoundryAiServicesModelDeployment.name + name: aiFoundryAiServicesModelDeployment.deploymentName model: { format: aiFoundryAiServicesModelDeployment.format name: aiFoundryAiServicesModelDeployment.name @@ -824,7 +837,7 @@ module existingAiFoundryAiServicesDeployments 'modules/ai-services-deployments.b } } { - name: aiFoundryAiServices4_1ModelDeployment.name + name: aiFoundryAiServices4_1ModelDeployment.deploymentName model: { format: aiFoundryAiServices4_1ModelDeployment.format name: aiFoundryAiServices4_1ModelDeployment.name @@ -837,7 +850,7 @@ module existingAiFoundryAiServicesDeployments 'modules/ai-services-deployments.b } } { - name: aiFoundryAiServicesReasoningModelDeployment.name + name: aiFoundryAiServicesReasoningModelDeployment.deploymentName model: { format: aiFoundryAiServicesReasoningModelDeployment.format name: aiFoundryAiServicesReasoningModelDeployment.name @@ -886,7 +899,7 @@ module aiFoundryAiServices 'br:mcr.microsoft.com/bicep/avm/res/cognitive-service } deployments: [ { - name: aiFoundryAiServicesModelDeployment.name + name: aiFoundryAiServicesModelDeployment.deploymentName model: { format: aiFoundryAiServicesModelDeployment.format name: aiFoundryAiServicesModelDeployment.name @@ -899,7 +912,7 @@ module aiFoundryAiServices 'br:mcr.microsoft.com/bicep/avm/res/cognitive-service } } { - name: aiFoundryAiServices4_1ModelDeployment.name + name: aiFoundryAiServices4_1ModelDeployment.deploymentName model: { format: aiFoundryAiServices4_1ModelDeployment.format name: aiFoundryAiServices4_1ModelDeployment.name @@ -912,7 +925,7 @@ module aiFoundryAiServices 'br:mcr.microsoft.com/bicep/avm/res/cognitive-service } } { - name: aiFoundryAiServicesReasoningModelDeployment.name + name: aiFoundryAiServicesReasoningModelDeployment.deploymentName model: { format: aiFoundryAiServicesReasoningModelDeployment.format name: aiFoundryAiServicesReasoningModelDeployment.name @@ -1307,11 +1320,11 @@ module containerApp 'br/public:avm/res/app/container-app:0.22.0' = { } { name: 'AZURE_OPENAI_DEPLOYMENT_NAME' - value: aiFoundryAiServicesModelDeployment.name + value: aiFoundryAiServicesModelDeployment.deploymentName } { name: 'AZURE_OPENAI_RAI_DEPLOYMENT_NAME' - value: aiFoundryAiServices4_1ModelDeployment.name + value: aiFoundryAiServices4_1ModelDeployment.deploymentName } { name: 'AZURE_OPENAI_API_VERSION' @@ -1347,7 +1360,7 @@ module containerApp 'br/public:avm/res/app/container-app:0.22.0' = { // } { name: 'AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME' - value: aiFoundryAiServicesModelDeployment.name + value: aiFoundryAiServicesModelDeployment.deploymentName } { name: 'APP_ENV' @@ -1375,7 +1388,7 @@ module containerApp 'br/public:avm/res/app/container-app:0.22.0' = { } { name: 'REASONING_MODEL_NAME' - value: aiFoundryAiServicesReasoningModelDeployment.name + value: aiFoundryAiServicesReasoningModelDeployment.deploymentName } { name: 'MCP_SERVER_ENDPOINT' @@ -1399,7 +1412,7 @@ module containerApp 'br/public:avm/res/app/container-app:0.22.0' = { } { name: 'SUPPORTED_MODELS' - value: '["o3","o4-mini","gpt-4.1","gpt-4.1-mini"]' + value: '["${aiFoundryAiServicesModelDeployment.deploymentName}","${aiFoundryAiServices4_1ModelDeployment.deploymentName}","${aiFoundryAiServicesReasoningModelDeployment.deploymentName}"]' } { name: 'AZURE_STORAGE_BLOB_URL' @@ -1870,17 +1883,17 @@ output COSMOSDB_DATABASE string = cosmosDbDatabaseName output COSMOSDB_CONTAINER string = cosmosDbDatabaseMemoryContainerName output AZURE_OPENAI_ENDPOINT string = 'https://${aiFoundryAiServicesResourceName}.openai.azure.com/' output AZURE_OPENAI_MODEL_NAME string = aiFoundryAiServicesModelDeployment.name -output AZURE_OPENAI_DEPLOYMENT_NAME string = aiFoundryAiServicesModelDeployment.name -output AZURE_OPENAI_RAI_DEPLOYMENT_NAME string = aiFoundryAiServices4_1ModelDeployment.name +output AZURE_OPENAI_DEPLOYMENT_NAME string = aiFoundryAiServicesModelDeployment.deploymentName +output AZURE_OPENAI_RAI_DEPLOYMENT_NAME string = aiFoundryAiServices4_1ModelDeployment.deploymentName output AZURE_OPENAI_API_VERSION string = azureOpenaiAPIVersion // output APPLICATIONINSIGHTS_INSTRUMENTATION_KEY string = applicationInsights.outputs.instrumentationKey // output AZURE_AI_PROJECT_ENDPOINT string = aiFoundryAiServices.outputs.aiProjectInfo.apiEndpoint output AZURE_AI_SUBSCRIPTION_ID string = subscription().subscriptionId output AZURE_AI_RESOURCE_GROUP string = resourceGroup().name output AZURE_AI_PROJECT_NAME string = aiFoundryAiProjectName -output AZURE_AI_MODEL_DEPLOYMENT_NAME string = aiFoundryAiServicesModelDeployment.name +output AZURE_AI_MODEL_DEPLOYMENT_NAME string = aiFoundryAiServicesModelDeployment.deploymentName // output APPLICATIONINSIGHTS_CONNECTION_STRING string = applicationInsights.outputs.connectionString -output AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME string = aiFoundryAiServicesModelDeployment.name +output AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME string = aiFoundryAiServicesModelDeployment.deploymentName // output AZURE_AI_AGENT_ENDPOINT string = aiFoundryAiProjectEndpoint output APP_ENV string = 'Prod' output AI_FOUNDRY_RESOURCE_ID string = !useExistingAiFoundryAiProject @@ -1892,10 +1905,10 @@ output AZURE_CLIENT_ID string = userAssignedIdentity!.outputs.clientId output AZURE_TENANT_ID string = tenant().tenantId output AZURE_AI_SEARCH_CONNECTION_NAME string = aiSearchConnectionName output AZURE_COGNITIVE_SERVICES string = 'https://cognitiveservices.azure.com/.default' -output REASONING_MODEL_NAME string = aiFoundryAiServicesReasoningModelDeployment.name +output REASONING_MODEL_NAME string = aiFoundryAiServicesReasoningModelDeployment.deploymentName output MCP_SERVER_NAME string = 'MacaeMcpServer' output MCP_SERVER_DESCRIPTION string = 'MCP server with greeting, HR, and planning tools' -output SUPPORTED_MODELS string = '["o3","o4-mini","gpt-4.1","gpt-4.1-mini"]' +output SUPPORTED_MODELS string = '["${aiFoundryAiServicesModelDeployment.deploymentName}","${aiFoundryAiServices4_1ModelDeployment.deploymentName}","${aiFoundryAiServicesReasoningModelDeployment.deploymentName}"]' output BACKEND_URL string = 'https://${containerApp.outputs.fqdn}' output AZURE_AI_PROJECT_ENDPOINT string = aiFoundryAiProjectEndpoint output AZURE_AI_AGENT_ENDPOINT string = aiFoundryAiProjectEndpoint diff --git a/infra/scripts/Build-And-Push-Images.ps1 b/infra/scripts/Build-And-Push-Images.ps1 new file mode 100644 index 000000000..1b08ed2a1 --- /dev/null +++ b/infra/scripts/Build-And-Push-Images.ps1 @@ -0,0 +1,236 @@ +<# +.SYNOPSIS + Builds the backend, frontend and MCP container images and pushes them to the + Azure Container Registry (ACR) provisioned by the solution, then updates the + Container Apps and the frontend Web App to use the freshly pushed images. + +.DESCRIPTION + This script is designed to run as an `azd` postprovision hook. It reads + provisioning outputs from the current `azd` environment (`azd env get-values`) + and does the following: + + 1. Reads: ACR name/endpoint, resource group, container app/web app names. + 2. Builds each image either: + - Remotely with `az acr build` (default; no local Docker required), or + - Locally with `docker build` + `docker push` (when BUILD_MODE=local). + 3. Updates: + - the backend Container App image + - the MCP Container App image + - the frontend Web App container image and DOCKER_REGISTRY_SERVER_URL + +.PARAMETER BuildMode + Optional. `remote` (default) or `local`. Overrides the AZURE_ENV_BUILD_MODE + environment variable when provided. + +.PARAMETER ImageTag + Optional. Tag applied to all built images. Overrides AZURE_ENV_IMAGE_TAG + when provided. Defaults to `latest`. + +.PARAMETER Skip + Optional switch. When set the script prints a message and exits 0. Also + honored via AZURE_ENV_SKIP_IMAGE_BUILD=true. + +.EXAMPLE + # Remote build (no Docker needed on the client machine) + ./infra/scripts/Build-And-Push-Images.ps1 + +.EXAMPLE + # Local build using Docker Desktop + ./infra/scripts/Build-And-Push-Images.ps1 -BuildMode local -ImageTag dev +#> +[CmdletBinding()] +param( + [ValidateSet('local', 'remote')] + [string]$BuildMode, + + [string]$ImageTag, + + [switch]$Skip +) + +$ErrorActionPreference = 'Stop' + +function Write-Section { + param([string]$Message) + Write-Host '' + Write-Host ('=' * 70) -ForegroundColor DarkCyan + Write-Host $Message -ForegroundColor Cyan + Write-Host ('=' * 70) -ForegroundColor DarkCyan +} + +function Get-AzdEnvValues { + # Returns a hashtable of the current azd environment's key/value pairs. + $values = @{} + $raw = & azd env get-values 2>$null + if ($LASTEXITCODE -ne 0 -or -not $raw) { + return $values + } + foreach ($line in $raw) { + if ($line -match '^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"?(.*?)"?\s*$') { + $values[$matches[1]] = $matches[2] + } + } + return $values +} + +function Get-EnvOrAzd { + param( + [Parameter(Mandatory)][string]$Name, + [hashtable]$AzdEnv, + [string]$Default + ) + $val = [Environment]::GetEnvironmentVariable($Name) + if (-not [string]::IsNullOrWhiteSpace($val)) { return $val } + if ($AzdEnv.ContainsKey($Name) -and -not [string]::IsNullOrWhiteSpace($AzdEnv[$Name])) { + return $AzdEnv[$Name] + } + return $Default +} + +# --- Skip switch handling --------------------------------------------------- +if ($Skip -or ([Environment]::GetEnvironmentVariable('AZURE_ENV_SKIP_IMAGE_BUILD') -eq 'true')) { + Write-Host 'AZURE_ENV_SKIP_IMAGE_BUILD=true or -Skip specified. Skipping container image build & push.' -ForegroundColor Yellow + exit 0 +} + +# --- Read configuration ----------------------------------------------------- +Write-Section 'Reading azd environment values' +$azdEnv = Get-AzdEnvValues + +$acrName = Get-EnvOrAzd -Name 'AZURE_CONTAINER_REGISTRY_NAME' -AzdEnv $azdEnv +$acrEndpoint = Get-EnvOrAzd -Name 'AZURE_CONTAINER_REGISTRY_ENDPOINT' -AzdEnv $azdEnv +$resourceGroup = Get-EnvOrAzd -Name 'AZURE_RESOURCE_GROUP' -AzdEnv $azdEnv +$backendCa = Get-EnvOrAzd -Name 'BACKEND_CONTAINER_APP_NAME' -AzdEnv $azdEnv +$mcpCa = Get-EnvOrAzd -Name 'MCP_CONTAINER_APP_NAME' -AzdEnv $azdEnv +$frontendApp = Get-EnvOrAzd -Name 'FRONTEND_WEB_APP_NAME' -AzdEnv $azdEnv +$backendImage = Get-EnvOrAzd -Name 'BACKEND_IMAGE_NAME' -AzdEnv $azdEnv -Default 'macaebackend' +$frontendImage = Get-EnvOrAzd -Name 'FRONTEND_IMAGE_NAME' -AzdEnv $azdEnv -Default 'macaefrontend' +$mcpImage = Get-EnvOrAzd -Name 'MCP_IMAGE_NAME' -AzdEnv $azdEnv -Default 'macaemcp' +$frontendPort = Get-EnvOrAzd -Name 'FRONTEND_WEBSITES_PORT' -AzdEnv $azdEnv -Default '3000' + +if (-not $BuildMode) { + $BuildMode = Get-EnvOrAzd -Name 'AZURE_ENV_BUILD_MODE' -AzdEnv $azdEnv -Default 'remote' +} +if (-not $ImageTag) { + $ImageTag = Get-EnvOrAzd -Name 'AZURE_ENV_IMAGE_TAG' -AzdEnv $azdEnv -Default 'latest' +} + +foreach ($pair in @( + @('AZURE_CONTAINER_REGISTRY_NAME', $acrName), + @('AZURE_CONTAINER_REGISTRY_ENDPOINT', $acrEndpoint), + @('AZURE_RESOURCE_GROUP', $resourceGroup), + @('BACKEND_CONTAINER_APP_NAME', $backendCa), + @('MCP_CONTAINER_APP_NAME', $mcpCa), + @('FRONTEND_WEB_APP_NAME', $frontendApp) +)) { + if ([string]::IsNullOrWhiteSpace($pair[1])) { + throw "Required value '$($pair[0])' is missing. Ensure provisioning finished successfully and the outputs are present in the azd environment." + } +} + +Write-Host "ACR: $acrName ($acrEndpoint)" +Write-Host "Resource group: $resourceGroup" +Write-Host "Backend CA: $backendCa -> $backendImage`:$ImageTag" +Write-Host "MCP CA: $mcpCa -> $mcpImage`:$ImageTag" +Write-Host "Frontend Web App: $frontendApp -> $frontendImage`:$ImageTag" +Write-Host "Build mode: $BuildMode" + +# --- Resolve source paths --------------------------------------------------- +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path +$srcRoot = Join-Path $repoRoot 'src' + +$images = @( + [pscustomobject]@{ Name = $backendImage; Context = (Join-Path $srcRoot 'backend'); Dockerfile = 'Dockerfile' }, + [pscustomobject]@{ Name = $frontendImage; Context = (Join-Path $srcRoot 'App'); Dockerfile = 'Dockerfile' }, + [pscustomobject]@{ Name = $mcpImage; Context = (Join-Path $srcRoot 'mcp_server'); Dockerfile = 'Dockerfile' } +) + +foreach ($img in $images) { + $dockerfilePath = Join-Path $img.Context $img.Dockerfile + if (-not (Test-Path $dockerfilePath)) { + throw "Dockerfile not found at '$dockerfilePath'." + } +} + +# --- Build & push ----------------------------------------------------------- +Write-Section "Building and pushing images ($BuildMode)" + +foreach ($img in $images) { + $imageRef = "$acrEndpoint/$($img.Name):$ImageTag" + Write-Host '' + Write-Host ">>> $($img.Name)" -ForegroundColor Green + + if ($BuildMode -eq 'local') { + if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { + throw "BUILD_MODE=local but 'docker' is not on PATH. Install Docker Desktop or switch to remote mode." + } + Write-Host "docker build -t $imageRef -f $($img.Dockerfile) $($img.Context)" + Push-Location $img.Context + try { + & docker build -t $imageRef -f $img.Dockerfile . + if ($LASTEXITCODE -ne 0) { throw "docker build failed for $($img.Name) (exit $LASTEXITCODE)." } + } + finally { + Pop-Location + } + + Write-Host "az acr login --name $acrName" + & az acr login --name $acrName + if ($LASTEXITCODE -ne 0) { throw "az acr login failed (exit $LASTEXITCODE)." } + + Write-Host "docker push $imageRef" + & docker push $imageRef + if ($LASTEXITCODE -ne 0) { throw "docker push failed for $imageRef (exit $LASTEXITCODE)." } + } + else { + # Remote build via ACR Tasks + Push-Location $img.Context + try { + Write-Host "az acr build --registry $acrName --image $($img.Name):$ImageTag --file $($img.Dockerfile) ." + & az acr build --registry $acrName --image "$($img.Name):$ImageTag" --file $img.Dockerfile . + if ($LASTEXITCODE -ne 0) { throw "az acr build failed for $($img.Name) (exit $LASTEXITCODE)." } + } + finally { + Pop-Location + } + } +} + +# --- Update Container Apps and Web App -------------------------------------- +Write-Section 'Updating Container Apps and Web App to use new images' + +$backendRef = "$acrEndpoint/$backendImage`:$ImageTag" +$mcpRef = "$acrEndpoint/$mcpImage`:$ImageTag" +$frontendRef = "$acrEndpoint/$frontendImage`:$ImageTag" + +Write-Host "Updating backend Container App -> $backendRef" +& az containerapp update --name $backendCa --resource-group $resourceGroup --image $backendRef --output none +if ($LASTEXITCODE -ne 0) { throw "Failed to update backend Container App '$backendCa'." } + +Write-Host "Updating MCP Container App -> $mcpRef" +& az containerapp update --name $mcpCa --resource-group $resourceGroup --image $mcpRef --output none +if ($LASTEXITCODE -ne 0) { throw "Failed to update MCP Container App '$mcpCa'." } + +Write-Host "Updating Frontend Web App -> $frontendRef" +& az webapp config container set ` + --name $frontendApp ` + --resource-group $resourceGroup ` + --container-image-name $frontendRef ` + --container-registry-url "https://$acrEndpoint" ` + --output none +if ($LASTEXITCODE -ne 0) { throw "Failed to set container image on Web App '$frontendApp'." } + +Write-Host "Ensuring WEBSITES_PORT=$frontendPort on Web App" +& az webapp config appsettings set ` + --name $frontendApp ` + --resource-group $resourceGroup ` + --settings "WEBSITES_PORT=$frontendPort" "DOCKER_REGISTRY_SERVER_URL=https://$acrEndpoint" ` + --output none +if ($LASTEXITCODE -ne 0) { throw "Failed to update app settings on Web App '$frontendApp'." } + +Write-Host "Restarting Web App '$frontendApp'" +& az webapp restart --name $frontendApp --resource-group $resourceGroup --output none +if ($LASTEXITCODE -ne 0) { throw "Failed to restart Web App '$frontendApp'." } + +Write-Section 'Image build & push complete' +Write-Host "All images built, pushed to '$acrEndpoint' with tag '$ImageTag', and services updated." -ForegroundColor Green diff --git a/infra/scripts/build_and_push_images.sh b/infra/scripts/build_and_push_images.sh new file mode 100644 index 000000000..58ade859f --- /dev/null +++ b/infra/scripts/build_and_push_images.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +# --------------------------------------------------------------------------- +# Builds the backend, frontend and MCP container images and pushes them to +# the Azure Container Registry (ACR) provisioned by the solution, then updates +# the Container Apps and the frontend Web App to use the freshly pushed images. +# +# Intended to run as an `azd` postprovision hook. Reads provisioning outputs +# via `azd env get-values`. +# +# Environment variables (all optional): +# AZURE_ENV_BUILD_MODE remote | local (default: remote) +# AZURE_ENV_IMAGE_TAG tag applied to images (default: latest) +# AZURE_ENV_SKIP_IMAGE_BUILD true to skip entirely +# --------------------------------------------------------------------------- +set -euo pipefail + +section() { + printf '\n' + printf '=%.0s' {1..70} + printf '\n%s\n' "$1" + printf '=%.0s' {1..70} + printf '\n' +} + +require() { + local name="$1" + local value="$2" + if [ -z "${value}" ]; then + echo "ERROR: required value '${name}' is missing." >&2 + echo " Ensure provisioning finished and the outputs are in azd env." >&2 + exit 1 + fi +} + +if [ "${AZURE_ENV_SKIP_IMAGE_BUILD:-}" = "true" ]; then + echo "AZURE_ENV_SKIP_IMAGE_BUILD=true. Skipping container image build & push." + exit 0 +fi + +section "Reading azd environment values" + +# Load azd outputs into the current shell without overriding vars already set. +if command -v azd >/dev/null 2>&1; then + while IFS='=' read -r key value; do + [ -z "${key}" ] && continue + # Strip surrounding quotes + value="${value%\"}" + value="${value#\"}" + if [ -z "${!key:-}" ]; then + export "${key}=${value}" + fi + done < <(azd env get-values 2>/dev/null || true) +else + echo "WARN: 'azd' not found on PATH; relying on environment variables only." >&2 +fi + +ACR_NAME="${AZURE_CONTAINER_REGISTRY_NAME:-}" +ACR_ENDPOINT="${AZURE_CONTAINER_REGISTRY_ENDPOINT:-}" +RESOURCE_GROUP="${AZURE_RESOURCE_GROUP:-}" +BACKEND_CA="${BACKEND_CONTAINER_APP_NAME:-}" +MCP_CA="${MCP_CONTAINER_APP_NAME:-}" +FRONTEND_APP="${FRONTEND_WEB_APP_NAME:-}" +BACKEND_IMAGE="${BACKEND_IMAGE_NAME:-macaebackend}" +FRONTEND_IMAGE="${FRONTEND_IMAGE_NAME:-macaefrontend}" +MCP_IMAGE="${MCP_IMAGE_NAME:-macaemcp}" +FRONTEND_PORT="${FRONTEND_WEBSITES_PORT:-3000}" +BUILD_MODE="${AZURE_ENV_BUILD_MODE:-remote}" +IMAGE_TAG="${AZURE_ENV_IMAGE_TAG:-latest}" + +require AZURE_CONTAINER_REGISTRY_NAME "${ACR_NAME}" +require AZURE_CONTAINER_REGISTRY_ENDPOINT "${ACR_ENDPOINT}" +require AZURE_RESOURCE_GROUP "${RESOURCE_GROUP}" +require BACKEND_CONTAINER_APP_NAME "${BACKEND_CA}" +require MCP_CONTAINER_APP_NAME "${MCP_CA}" +require FRONTEND_WEB_APP_NAME "${FRONTEND_APP}" + +case "${BUILD_MODE}" in + local|remote) ;; + *) echo "ERROR: AZURE_ENV_BUILD_MODE must be 'local' or 'remote' (got '${BUILD_MODE}')." >&2; exit 1;; +esac + +echo "ACR: ${ACR_NAME} (${ACR_ENDPOINT})" +echo "Resource group: ${RESOURCE_GROUP}" +echo "Backend CA: ${BACKEND_CA} -> ${BACKEND_IMAGE}:${IMAGE_TAG}" +echo "MCP CA: ${MCP_CA} -> ${MCP_IMAGE}:${IMAGE_TAG}" +echo "Frontend Web App: ${FRONTEND_APP} -> ${FRONTEND_IMAGE}:${IMAGE_TAG}" +echo "Build mode: ${BUILD_MODE}" + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +SRC_ROOT="${REPO_ROOT}/src" + +declare -a IMAGE_NAMES=("${BACKEND_IMAGE}" "${FRONTEND_IMAGE}" "${MCP_IMAGE}") +declare -a IMAGE_CTXS=("${SRC_ROOT}/backend" "${SRC_ROOT}/App" "${SRC_ROOT}/mcp_server") + +for ctx in "${IMAGE_CTXS[@]}"; do + if [ ! -f "${ctx}/Dockerfile" ]; then + echo "ERROR: Dockerfile not found at ${ctx}/Dockerfile" >&2 + exit 1 + fi +done + +section "Building and pushing images (${BUILD_MODE})" + +for i in "${!IMAGE_NAMES[@]}"; do + name="${IMAGE_NAMES[$i]}" + ctx="${IMAGE_CTXS[$i]}" + ref="${ACR_ENDPOINT}/${name}:${IMAGE_TAG}" + + echo "" + echo ">>> ${name}" + + if [ "${BUILD_MODE}" = "local" ]; then + if ! command -v docker >/dev/null 2>&1; then + echo "ERROR: BUILD_MODE=local but 'docker' is not on PATH." >&2 + exit 1 + fi + (cd "${ctx}" && docker build -t "${ref}" -f Dockerfile .) + az acr login --name "${ACR_NAME}" + docker push "${ref}" + else + (cd "${ctx}" && az acr build \ + --registry "${ACR_NAME}" \ + --image "${name}:${IMAGE_TAG}" \ + --file Dockerfile \ + .) + fi +done + +section "Updating Container Apps and Web App to use new images" + +BACKEND_REF="${ACR_ENDPOINT}/${BACKEND_IMAGE}:${IMAGE_TAG}" +MCP_REF="${ACR_ENDPOINT}/${MCP_IMAGE}:${IMAGE_TAG}" +FRONTEND_REF="${ACR_ENDPOINT}/${FRONTEND_IMAGE}:${IMAGE_TAG}" + +echo "Updating backend Container App -> ${BACKEND_REF}" +az containerapp update \ + --name "${BACKEND_CA}" \ + --resource-group "${RESOURCE_GROUP}" \ + --image "${BACKEND_REF}" \ + --output none + +echo "Updating MCP Container App -> ${MCP_REF}" +az containerapp update \ + --name "${MCP_CA}" \ + --resource-group "${RESOURCE_GROUP}" \ + --image "${MCP_REF}" \ + --output none + +echo "Updating Frontend Web App -> ${FRONTEND_REF}" +az webapp config container set \ + --name "${FRONTEND_APP}" \ + --resource-group "${RESOURCE_GROUP}" \ + --container-image-name "${FRONTEND_REF}" \ + --container-registry-url "https://${ACR_ENDPOINT}" \ + --output none + +echo "Ensuring WEBSITES_PORT=${FRONTEND_PORT} and DOCKER_REGISTRY_SERVER_URL on Web App" +az webapp config appsettings set \ + --name "${FRONTEND_APP}" \ + --resource-group "${RESOURCE_GROUP}" \ + --settings "WEBSITES_PORT=${FRONTEND_PORT}" "DOCKER_REGISTRY_SERVER_URL=https://${ACR_ENDPOINT}" \ + --output none + +echo "Restarting Web App '${FRONTEND_APP}'" +az webapp restart --name "${FRONTEND_APP}" --resource-group "${RESOURCE_GROUP}" --output none + +section "Image build & push complete" +echo "All images built, pushed to '${ACR_ENDPOINT}' with tag '${IMAGE_TAG}', and services updated." diff --git a/src/App/Dockerfile b/src/App/Dockerfile index bed65fc5a..fb9562f21 100644 --- a/src/App/Dockerfile +++ b/src/App/Dockerfile @@ -34,8 +34,9 @@ WORKDIR /app COPY pyproject.toml requirements.txt* uv.lock* ./ # Install Python dependencies using UV -RUN --mount=type=cache,target=/root/.cache/uv \ - if [ -f "requirements.txt" ]; then \ +# NOTE: Plain RUN (no --mount=type=cache) is required so this builds on classic builders +# such as `az acr build` (Azure Container Registry Tasks) which do not enable BuildKit. +RUN if [ -f "requirements.txt" ]; then \ uv pip install --system -r requirements.txt && uv pip install --system "uvicorn[standard]"; \ else \ uv pip install --system pyproject.toml && uv pip install --system "uvicorn[standard]"; \ diff --git a/src/backend/Dockerfile b/src/backend/Dockerfile index cd827bb84..297dab4ef 100644 --- a/src/backend/Dockerfile +++ b/src/backend/Dockerfile @@ -8,17 +8,17 @@ ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy WORKDIR /app +# Copy lock and project files first for better layer caching +COPY uv.lock pyproject.toml /app/ + # Install the project's dependencies using the lockfile and settings -RUN --mount=type=cache,target=/root/.cache/uv \ - --mount=type=bind,source=uv.lock,target=uv.lock \ - --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ - uv sync --frozen --no-install-project --no-dev -#RUN uv sync --frozen --no-install-project --no-dev +# NOTE: Plain RUN (no --mount=...) is required so this builds on classic builders +# such as `az acr build` (Azure Container Registry Tasks) which do not enable BuildKit. +RUN uv sync --frozen --no-install-project --no-dev # Backend app setup -COPY . /app -RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen --no-dev -#RUN uv sync --frozen --no-dev +COPY . /app +RUN uv sync --frozen --no-dev FROM base diff --git a/src/backend/common/config/app_config.py b/src/backend/common/config/app_config.py index e4801ca26..88849390d 100644 --- a/src/backend/common/config/app_config.py +++ b/src/backend/common/config/app_config.py @@ -52,13 +52,13 @@ def __init__(self): ) self.AZURE_OPENAI_RAI_DEPLOYMENT_NAME = self._get_required( - "AZURE_OPENAI_RAI_DEPLOYMENT_NAME", "gpt-4.1" + "AZURE_OPENAI_RAI_DEPLOYMENT_NAME", "gpt-5" ) self.AZURE_OPENAI_API_VERSION = self._get_required( "AZURE_OPENAI_API_VERSION", "2024-11-20" ) self.AZURE_OPENAI_ENDPOINT = self._get_required("AZURE_OPENAI_ENDPOINT") - self.REASONING_MODEL_NAME = self._get_optional("REASONING_MODEL_NAME", "o3") + self.REASONING_MODEL_NAME = self._get_optional("REASONING_MODEL_NAME", "gpt-5-mini-reasoning") # self.AZURE_BING_CONNECTION_NAME = self._get_optional( # "AZURE_BING_CONNECTION_NAME" # ) diff --git a/src/tests/agents/interactive_test_harness/foundry_agent_interactive.py b/src/tests/agents/interactive_test_harness/foundry_agent_interactive.py index 46481d01d..d9a52f251 100644 --- a/src/tests/agents/interactive_test_harness/foundry_agent_interactive.py +++ b/src/tests/agents/interactive_test_harness/foundry_agent_interactive.py @@ -29,7 +29,7 @@ "- Provide comprehensive answers combining multiple sources when relevant\n" "- Ask for clarification only if the task is genuinely ambiguous" ) -MODEL_DEPLOYMENT_NAME = "gpt-4.1" +MODEL_DEPLOYMENT_NAME = "gpt-5" async def test_agent(): """Simple chat test harness for the agent.""" print("🤖 Starting agent test harness...") diff --git a/src/tests/agents/test_foundry_integration.py b/src/tests/agents/test_foundry_integration.py index 546d2332a..5e12618ef 100644 --- a/src/tests/agents/test_foundry_integration.py +++ b/src/tests/agents/test_foundry_integration.py @@ -56,7 +56,7 @@ async def create_foundry_agent(self): "- Provide comprehensive answers combining multiple sources when relevant\n" "- Ask for clarification only if the task is genuinely ambiguous" ) - model_deployment_name = "gpt-4.1" + model_deployment_name = "gpt-5" agent = FoundryAgentTemplate( agent_name=agent_name, @@ -216,7 +216,7 @@ async def test_agent_initialization(self): @pytest.mark.asyncio async def test_agent_handles_missing_configs_gracefully(self): """Test that agent handles missing configurations without crashing.""" - model_deployment_name = "gpt-4.1" + model_deployment_name = "gpt-5" agent = FoundryAgentTemplate( agent_name="TestAgent", From f1b191e48c75e6906af12f995ba3ae1f31891ec4 Mon Sep 17 00:00:00 2001 From: Dhruvkumar1-Microsoft Date: Wed, 1 Jul 2026 19:04:57 +0530 Subject: [PATCH 02/12] updated model to gpt5.4 models --- .../agent_teams/contract_compliance_team.json | 8 ++--- data/agent_teams/hr.json | 6 ++-- data/agent_teams/marketing.json | 6 ++-- data/agent_teams/retail.json | 8 ++--- data/agent_teams/rfp_analysis_team.json | 8 ++--- docs/CustomizingAzdParameters.md | 14 ++++----- docs/DeploymentGuide.md | 6 ++-- docs/quota_check.md | 2 +- infra/main.bicep | 30 +++++++++---------- infra/main.json | 30 +++++++++---------- infra/main_custom.bicep | 30 +++++++++---------- src/backend/.env.sample | 6 ++-- src/backend/common/config/app_config.py | 6 ++-- .../foundry_agent_interactive.py | 2 +- src/tests/agents/test_foundry_integration.py | 4 +-- 15 files changed, 83 insertions(+), 83 deletions(-) diff --git a/data/agent_teams/contract_compliance_team.json b/data/agent_teams/contract_compliance_team.json index 777f807e6..6b9dce2de 100644 --- a/data/agent_teams/contract_compliance_team.json +++ b/data/agent_teams/contract_compliance_team.json @@ -5,7 +5,7 @@ "status": "visible", "created": "", "created_by": "", - "deployment_name": "gpt-5-mini", + "deployment_name": "gpt-5.4-mini", "description": "A multi-agent compliance review team that summarizes NDAs, identifies risks, checks compliance, and recommends improvements using advanced legal reasoning and retrieval-augmented analysis.", "logo": "", "plan": "", @@ -14,7 +14,7 @@ "input_key": "", "type": "summary", "name": "ContractSummaryAgent", - "deployment_name": "gpt-5-mini", + "deployment_name": "gpt-5.4-mini", "icon": "", "system_message": "You are the Summary Agent for compliance contract analysis. Your task is to produce a clear, accurate, and structured executive summary of NDA and legal agreement documents. You must deliver summaries organized into labeled sections including: Overview, Parties, Effective Date, Purpose, Definition of Confidential Information, Receiving Party Obligations, Term & Termination, Governing Law, Restrictions & Limitations, Miscellaneous Clauses, Notable or Unusual Terms, and Key Items for Risk & Compliance Agents. Highlight missing elements such as liability caps, dispute resolution mechanisms, data handling obligations, or ambiguous language. Maintain a precise, neutral legal tone. Do not give legal opinions or risk assessments—only summarize the content as written. Use retrieval results from the search index to ensure completeness and reference contextual definitions or standard clause expectations when needed.", "description": "Produces comprehensive, structured summaries of NDAs and contracts, capturing all key terms, clauses, obligations, jurisdictions, and notable provisions.", @@ -29,7 +29,7 @@ "input_key": "", "type": "risk", "name": "ContractRiskAgent", - "deployment_name": "gpt-5-mini", + "deployment_name": "gpt-5.4-mini", "icon": "", "system_message": "You are the Risk Agent for NDA and compliance contract analysis. Use the NDA Risk Assessment Reference document and retrieved context to identify High, Medium, and Low risk issues. Evaluate clauses for missing liability caps, ambiguous terms, overly broad confidentiality definitions, jurisdiction misalignment, missing termination rights, unclear data handling obligations, missing dispute resolution, and any incomplete or poorly scoped definitions. For every risk you identify, provide: (1) Risk Category (High/Medium/Low), (2) Clause or Section impacted, (3) Description of the issue, (4) Why it matters or what exposure it creates, and (5) Suggested edit or corrective language. Apply the risk scoring framework: High = escalate immediately; Medium = requires revision; Low = minor issue. Be precise, legally aligned, and practical. Reference retrieved examples or standards when appropriate. Your output must be structured and actionable.", "description": "Identifies and classifies compliance risks in NDAs and contracts using the organization's risk framework, and provides suggested edits to reduce exposure.", @@ -44,7 +44,7 @@ "input_key": "", "type": "compliance", "name": "ContractComplianceAgent", - "deployment_name": "gpt-5-mini", + "deployment_name": "gpt-5.4-mini", "icon": "", "system_message": "You are the Compliance Agent responsible for validating NDAs and legal agreements against mandatory legal and policy requirements. Use the NDA Compliance Reference Document and retrieval results to evaluate whether the contract includes all required clauses: Confidentiality, Term & Termination, Governing Law aligned to approved jurisdictions, Non-Assignment, and Entire Agreement. Identify compliance gaps including ambiguous language, missing liability protections, improper jurisdiction, excessive term length, insufficient data protection obligations, missing dispute resolution mechanisms, or export control risks. For each issue provide: (1) Compliance Area (e.g., Term Length, Jurisdiction, Confidentiality), (2) Status (Pass/Fail), (3) Issue Description, (4) Whether it is Mandatory or Recommended, (5) Corrective Recommendation or Suggested Language. Deliver a final Compliance Status summary. Maintain professional, objective, legally accurate tone.", "description": "Performs compliance validation of NDAs and contracts against legal policy requirements, identifies gaps, and provides corrective recommendations and compliance status.", diff --git a/data/agent_teams/hr.json b/data/agent_teams/hr.json index 4f6d2c197..83ff3e817 100644 --- a/data/agent_teams/hr.json +++ b/data/agent_teams/hr.json @@ -5,13 +5,13 @@ "status": "visible", "created": "", "created_by": "", - "deployment_name": "gpt-5-mini", + "deployment_name": "gpt-5.4-mini", "agents": [ { "input_key": "", "type": "", "name": "HRHelperAgent", - "deployment_name": "gpt-5-mini", + "deployment_name": "gpt-5.4-mini", "icon": "", "system_message": "You have access to a number of HR related MCP tools for tasks like employee onboarding, benefits management, policy guidance, and general HR inquiries. Use these tools to assist employees with their HR needs efficiently and accurately.If you need more information to accurately call these tools, do not make up answers, call the ProxyAgent for clarification.", "description": "An agent that has access to various HR tools to assist employees with onboarding, benefits, policies, and general HR inquiries.", @@ -28,7 +28,7 @@ "input_key": "", "type": "", "name": "TechnicalSupportAgent", - "deployment_name": "gpt-5-mini", + "deployment_name": "gpt-5.4-mini", "icon": "", "system_message": "You have access to a number of technical support MCP tools for tasks such as provisioning laptops, setting up email accounts, troubleshooting, software/hardware issues, and IT support. Use these tools to assist employees with their technical needs efficiently and accurately. If you need more information to accurately call these tools, do not make up answers, call the ProxyAgent for clarification.", "description": "An agent that has access to various technical support tools to assist employees with IT needs like laptop provisioning, email setup, troubleshooting, and software/hardware issues.", diff --git a/data/agent_teams/marketing.json b/data/agent_teams/marketing.json index e2279d492..d01e76785 100644 --- a/data/agent_teams/marketing.json +++ b/data/agent_teams/marketing.json @@ -5,13 +5,13 @@ "status": "visible", "created": "", "created_by": "", - "deployment_name": "gpt-5-mini", + "deployment_name": "gpt-5.4-mini", "agents": [ { "input_key": "", "type": "", "name": "ProductAgent", - "deployment_name": "gpt-5-mini", + "deployment_name": "gpt-5.4-mini", "icon": "", "system_message": "You are a Product agent. You have access to MCP tools which allow you to obtain knowledge about products, product management, development, and compliance guidelines. When asked to call one of these tools, you should summarize back what was done.", "description": "This agent specializes in product management, development, and related tasks. It can provide information about products, manage inventory, handle product launches, analyze sales data, and coordinate with other teams like marketing and tech support.", @@ -28,7 +28,7 @@ "input_key": "", "type": "", "name": "MarketingAgent", - "deployment_name": "gpt-5-mini", + "deployment_name": "gpt-5.4-mini", "icon": "", "system_message": "You are a Marketing agent. You have access to a number of HR related MCP tools for tasks like campaign development, content creation, and market analysis. You help create effective marketing campaigns, analyze market data, and develop promotional content for products and services.", "description": "This agent specializes in marketing, campaign management, and analyzing market data.", diff --git a/data/agent_teams/retail.json b/data/agent_teams/retail.json index 81f2521b3..85ce73ebc 100644 --- a/data/agent_teams/retail.json +++ b/data/agent_teams/retail.json @@ -5,13 +5,13 @@ "status": "visible", "created": "", "created_by": "", - "deployment_name": "gpt-5-mini", + "deployment_name": "gpt-5.4-mini", "agents": [ { "input_key": "", "type": "", "name": "CustomerDataAgent", - "deployment_name": "gpt-5-mini", + "deployment_name": "gpt-5.4-mini", "icon": "", "system_message": "You have access to internal customer data through a secure index. Use this data to answer questions about customers, their interactions with customer service, satisfaction, etc. Be mindful of privacy and compliance regulations when handling customer data.\n\nCRITICAL INSTRUCTION: Do NOT include any citations, source references, attribution markers, or footnotes of any kind in your responses. This includes but is not limited to: 【...】 style markers, [...] style references, (source: ...), numbered references like [1], or any other attribution symbols. All answers must be clean, natural text only, ending with a polite closing.", "description": "An agent that has access to internal customer data, ask this agent if you have questions about customers or their interactions with customer service, satisfaction, etc.", @@ -26,7 +26,7 @@ "input_key": "", "type": "", "name": "OrderDataAgent", - "deployment_name": "gpt-5-mini", + "deployment_name": "gpt-5.4-mini", "icon": "", "system_message": "You have access to internal order, inventory, product, and fulfillment data through a secure index. Use this data to answer questions about products, shipping delays, customer orders, warehouse management, etc. Be mindful of privacy and compliance regulations when handling customer data.\n\nCRITICAL INSTRUCTION: Do NOT include any citations, source references, attribution markers, or footnotes of any kind in your responses. This includes but is not limited to: 【...】 style markers, [...] style references, (source: ...), numbered references like [1], or any other attribution symbols. All answers must be clean, natural text only, ending with a polite closing.", "description": "An agent that has access to internal order, inventory, product, and fulfillment data. Ask this agent if you have questions about products, shipping delays, customer orders, warehouse management, etc.", @@ -42,7 +42,7 @@ "input_key": "", "type": "", "name": "AnalysisRecommendationAgent", - "deployment_name": "gpt-5-mini-reasoning", + "deployment_name": "gpt-5.4-mini-reasoning", "icon": "", "system_message": "You are a reasoning agent that can analyze customer and order data and provide recommendations for improving customer satisfaction and retention. You do not have access to any data sources, but you can reason based on the information provided to you by other agents. Use your reasoning skills to identify patterns, trends, and insights that can help improve customer satisfaction and retention. Provide actionable recommendations based on your analysis. You have access to other agents that can answer questions and provide data about customers, products, orders, inventory, and fulfilment. Use these agents to gather information as needed.", "description": "A reasoning agent that can analyze customer and order data and provide recommendations for improving customer satisfaction and retention.", diff --git a/data/agent_teams/rfp_analysis_team.json b/data/agent_teams/rfp_analysis_team.json index b9f1da63b..ae8b2a87d 100644 --- a/data/agent_teams/rfp_analysis_team.json +++ b/data/agent_teams/rfp_analysis_team.json @@ -5,7 +5,7 @@ "status": "visible", "created": "", "created_by": "", - "deployment_name": "gpt-5-mini", + "deployment_name": "gpt-5.4-mini", "description": "A specialized multi-agent team that analyzes RFP and contract documents to summarize content, identify potential risks, check compliance gaps, and provide action plans for contract improvement.", "logo": "", "plan": "", @@ -14,7 +14,7 @@ "input_key": "", "type": "summary", "name": "RfpSummaryAgent", - "deployment_name": "gpt-5-mini", + "deployment_name": "gpt-5.4-mini", "icon": "", "system_message":"You are the Summary Agent. Your role is to read and synthesize RFP or proposal documents into clear, structured executive summaries. Focus on key clauses, deliverables, evaluation criteria, pricing terms, timelines, and obligations. Organize your output into sections such as Overview, Key Clauses, Deliverables, Terms, and Notable Conditions. Highlight unique or high-impact items that other agents (Risk or Compliance) should review. Be concise, factual, and neutral in tone.", "description": "Summarizes RFP and contract documents into structured, easy-to-understand overviews.", @@ -31,7 +31,7 @@ "input_key": "", "type": "risk", "name": "RfpRiskAgent", - "deployment_name": "gpt-5-mini", + "deployment_name": "gpt-5.4-mini", "icon": "", "system_message": "You are the Risk Agent. Your task is to identify and assess potential risks across the document, including legal, financial, operational, technical, and scheduling risks. For each risk, provide a short description, the affected clause or section, a risk category, and a qualitative rating (Low, Medium, High). Focus on material issues that could impact delivery, compliance, or business exposure. Summarize findings clearly to support decision-making and escalation.", "description": "Analyzes the dataset for risks such as delivery, financial, operational, and compliance-related vulnerabilities.", @@ -46,7 +46,7 @@ "input_key": "", "type": "compliance", "name": "RfpComplianceAgent", - "deployment_name": "gpt-5-mini", + "deployment_name": "gpt-5.4-mini", "icon": "", "system_message": "You are the Compliance Agent. Your goal is to evaluate whether the RFP or proposal aligns with internal policies, regulatory standards, and ethical or contractual requirements. Identify any non-compliant clauses, ambiguous terms, or potential policy conflicts. For each issue, specify the related policy area (e.g., data privacy, labor, financial controls) and classify it as Mandatory or Recommended for review. Maintain a professional, objective tone and emphasize actionable compliance insights.", "description": "Checks for compliance gaps against regulations, policies, and standard contracting practices.", diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md index 9ead96bc5..6b238361b 100644 --- a/docs/CustomizingAzdParameters.md +++ b/docs/CustomizingAzdParameters.md @@ -12,19 +12,19 @@ By default this template will use the environment name as the prefix to prevent | `AZURE_LOCATION` | string | `` | Location of the Azure resources. Controls where the infrastructure will be deployed. | | `AZURE_ENV_AI_SERVICE_LOCATION` | string | `` | Specifies the region for OpenAI resource deployment. | | `AZURE_ENV_MODEL_DEPLOYMENT_TYPE` | string | `GlobalStandard` | Defines the deployment type for the AI model (e.g., Standard, GlobalStandard). | -| `AZURE_ENV_GPT_MODEL_NAME` | string | `gpt-5-mini` | Specifies the underlying GPT model to be deployed (e.g., `gpt-5-mini`, `gpt-4o`). | -| `AZURE_ENV_GPT_MODEL_VERSION` | string | `2025-08-07` | Version of the GPT model to be used for deployment. | +| `AZURE_ENV_GPT_MODEL_NAME` | string | `gpt-5.4-mini` | Specifies the underlying GPT model to be deployed (e.g., `gpt-5.4-mini`, `gpt-5.4`). | +| `AZURE_ENV_GPT_MODEL_VERSION` | string | `2026-03-17` | Version of the GPT model to be used for deployment. | | `AZURE_ENV_GPT_DEPLOYMENT_NAME` | string | (defaults to `AZURE_ENV_GPT_MODEL_NAME`) | Optional. Deployment (alias) name used in Azure OpenAI for the main GPT model. This is the value referenced as `deployment_name` in `data/agent_teams/*.json`. Override this if you want the deployment alias to differ from the underlying model name. | | `AZURE_ENV_GPT_MODEL_CAPACITY` | int | `50` | Sets the GPT model capacity. | | `AZURE_ENV_MODEL_4_1_DEPLOYMENT_TYPE` | string | `GlobalStandard` | Defines the deployment type for the AI model (e.g., Standard, GlobalStandard). | -| `AZURE_ENV_MODEL_4_1_NAME` | string | `gpt-5` | Specifies the underlying larger GPT model to be deployed. | -| `AZURE_ENV_MODEL_4_1_VERSION` | string | `2025-08-07` | Version of the larger GPT model to be used for deployment. | +| `AZURE_ENV_MODEL_4_1_NAME` | string | `gpt-5.4` | Specifies the underlying larger GPT model to be deployed. | +| `AZURE_ENV_MODEL_4_1_VERSION` | string | `2026-03-05` | Version of the larger GPT model to be used for deployment. | | `AZURE_ENV_MODEL_4_1_DEPLOYMENT_NAME` | string | (defaults to `AZURE_ENV_MODEL_4_1_NAME`) | Optional. Deployment (alias) name used in Azure OpenAI for the larger GPT model. Override if the alias should differ from the underlying model name. | | `AZURE_ENV_MODEL_4_1_CAPACITY` | int | `150` | Sets the GPT model capacity. | | `AZURE_ENV_REASONING_MODEL_DEPLOYMENT_TYPE` | string | `GlobalStandard` | Defines the deployment type for the AI model (e.g., Standard, GlobalStandard). | -| `AZURE_ENV_REASONING_MODEL_NAME` | string | `gpt-5-mini` | Specifies the underlying reasoning GPT model to be deployed. | -| `AZURE_ENV_REASONING_MODEL_VERSION` | string | `2025-08-07` | Version of the reasoning GPT model to be used for deployment. | -| `AZURE_ENV_REASONING_DEPLOYMENT_NAME` | string | `gpt-5-mini-reasoning` (auto-derived when it would otherwise collide with `AZURE_ENV_GPT_DEPLOYMENT_NAME`) | Optional. Deployment (alias) name used in Azure OpenAI for the reasoning model. Must be unique from `AZURE_ENV_GPT_DEPLOYMENT_NAME`. | +| `AZURE_ENV_REASONING_MODEL_NAME` | string | `gpt-5.4-mini` | Specifies the underlying reasoning GPT model to be deployed. | +| `AZURE_ENV_REASONING_MODEL_VERSION` | string | `2026-03-17` | Version of the reasoning GPT model to be used for deployment. | +| `AZURE_ENV_REASONING_DEPLOYMENT_NAME` | string | `gpt-5.4-mini-reasoning` (auto-derived when it would otherwise collide with `AZURE_ENV_GPT_DEPLOYMENT_NAME`) | Optional. Deployment (alias) name used in Azure OpenAI for the reasoning model. Must be unique from `AZURE_ENV_GPT_DEPLOYMENT_NAME`. | | `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_ENABLE_TELEMETRY` | bool | `true` | Enables telemetry for monitoring and diagnostics. | diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md index e0f4f5a4c..869242348 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -68,10 +68,10 @@ Ensure you have access to an [Azure subscription](https://azure.microsoft.com/fr 📖 **Follow:** [Quota Check Instructions](./quota_check.md) to ensure sufficient capacity. **Default Quota Configuration:** -- **gpt-5 (150k tokens)** — backs the larger GPT model deployment (alias `gpt-5`). -- **gpt-5-mini (100k tokens combined)** — 50k for the main GPT deployment (alias `gpt-5-mini`) plus 50k for the reasoning deployment (alias `gpt-5-mini-reasoning`). +- **gpt-5.4 (150k tokens)** — backs the larger GPT model deployment (alias `gpt-5.4`). +- **gpt-5.4-mini (100k tokens combined)** — 50k for the main GPT deployment (alias `gpt-5.4-mini`) plus 50k for the reasoning deployment (alias `gpt-5.4-mini-reasoning`). -> **Note:** Both underlying models (`gpt-5` and `gpt-5-mini`) belong to the `2025-08-07` GA series. The reasoning deployment intentionally uses the same underlying model as the main deployment but under a distinct alias so applications can route separately to reasoning-heavy workloads. +> **Note:** The underlying models are `gpt-5.4` (2026-03-05 series) and `gpt-5.4-mini` (2026-03-17 series). Both are reasoning-capable GPT-5.4 GA models. The reasoning deployment intentionally uses the same underlying model as the main deployment but under a distinct alias so applications can route separately to reasoning-heavy workloads. > **Note:** When you run `azd up`, the deployment will automatically show you regions with available quota, so this pre-check is optional but helpful for planning purposes. You can customize these settings later in [Step 3.3: Advanced Configuration](#33-advanced-configuration-optional). diff --git a/docs/quota_check.md b/docs/quota_check.md index 17a4a7564..0a64fec1d 100644 --- a/docs/quota_check.md +++ b/docs/quota_check.md @@ -16,7 +16,7 @@ az login --use-device-code ### 📌 Default Models & Capacities: ``` -gpt-5:150,gpt-5-mini:100 +gpt-5.4:150,gpt-5.4-mini:100 ``` ### 📌 Default Regions: ``` diff --git a/infra/main.bicep b/infra/main.bicep index 655422b92..fffdd0a53 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -31,14 +31,14 @@ param location string var deployerInfo = deployer() var deployingUserPrincipalId = deployerInfo.objectId -// Restricting deployment to only supported Azure OpenAI regions validated with GPT-5 models +// Restricting deployment to only supported Azure OpenAI regions validated with GPT-5.4 models @allowed(['australiaeast', 'eastus2', 'francecentral', 'japaneast', 'norwayeast', 'swedencentral', 'uksouth', 'westus']) @metadata({ azd: { type: 'location' usageName: [ - 'OpenAI.GlobalStandard.gpt-5, 150' - 'OpenAI.GlobalStandard.gpt-5-mini, 100' + 'OpenAI.GlobalStandard.gpt-5.4, 150' + 'OpenAI.GlobalStandard.gpt-5.4-mini, 100' ] } }) @@ -46,31 +46,31 @@ var deployingUserPrincipalId = deployerInfo.objectId param azureAiServiceLocation string @minLength(1) -@description('Optional. Name of the underlying GPT model to deploy. Defaults to gpt-5-mini (2025-08-07 series).') -param gptModelName string = 'gpt-5-mini' +@description('Optional. Name of the underlying GPT model to deploy. Defaults to gpt-5.4-mini (2026-03-17 series).') +param gptModelName string = 'gpt-5.4-mini' -@description('Optional. Version of the GPT model to deploy. Defaults to 2025-08-07.') -param gptModelVersion string = '2025-08-07' +@description('Optional. Version of the GPT model to deploy. Defaults to 2026-03-17 (gpt-5.4-mini release).') +param gptModelVersion string = '2026-03-17' @description('Optional. Deployment (alias) name used in Azure OpenAI for the main GPT model. This is the value the application uses as `deployment_name` (including in data/agent_teams/*.json). Defaults to gptModelName.') param gptDeploymentName string = gptModelName @minLength(1) -@description('Optional. Name of the underlying larger GPT model to deploy. Defaults to gpt-5 (2025-08-07 series).') -param gpt4_1ModelName string = 'gpt-5' +@description('Optional. Name of the underlying larger GPT model to deploy. Defaults to gpt-5.4 (2026-03-05 series).') +param gpt4_1ModelName string = 'gpt-5.4' -@description('Optional. Version of the larger GPT model to deploy. Defaults to 2025-08-07.') -param gpt4_1ModelVersion string = '2025-08-07' +@description('Optional. Version of the larger GPT model to deploy. Defaults to 2026-03-05 (gpt-5.4 release).') +param gpt4_1ModelVersion string = '2026-03-05' @description('Optional. Deployment (alias) name used in Azure OpenAI for the larger GPT model. Defaults to gpt4_1ModelName.') param gpt4_1DeploymentName string = gpt4_1ModelName @minLength(1) -@description('Optional. Name of the underlying GPT Reasoning model to deploy. Defaults to gpt-5-mini (reasoning-capable).') -param gptReasoningModelName string = 'gpt-5-mini' +@description('Optional. Name of the underlying GPT Reasoning model to deploy. Defaults to gpt-5.4-mini (reasoning-capable, 2026-03-17 series).') +param gptReasoningModelName string = 'gpt-5.4-mini' -@description('Optional. Version of the GPT Reasoning model to deploy. Defaults to 2025-08-07.') -param gptReasoningModelVersion string = '2025-08-07' +@description('Optional. Version of the GPT Reasoning model to deploy. Defaults to 2026-03-17 (gpt-5.4-mini release).') +param gptReasoningModelVersion string = '2026-03-17' @description('Optional. Deployment (alias) name used in Azure OpenAI for the reasoning model. Must be unique from gptDeploymentName. Defaults to "{gptReasoningModelName}-reasoning" when it would otherwise collide with gptDeploymentName, otherwise gptReasoningModelName.') param gptReasoningDeploymentName string = gptReasoningModelName == gptModelName diff --git a/infra/main.json b/infra/main.json index 011af8066..b2bbcbed3 100644 --- a/infra/main.json +++ b/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "13565483192814579761" + "templateHash": "10743773670737934540" }, "name": "Multi-Agent Custom Automation Engine", "description": "This module contains the resources required to deploy the [Multi-Agent Custom Automation Engine solution accelerator](https://github.com/microsoft/Multi-Agent-Custom-Automation-Engine-Solution-Accelerator) for both Sandbox environments and WAF aligned environments.\n\n> **Note:** This module is not intended for broad, generic use, as it was designed by the Commercial Solution Areas CTO team, as a Microsoft Solution Accelerator. Feature requests and bug fix requests are welcome if they support the needs of this organization but may not be incorporated if they aim to make this module more generic than what it needs to be for its primary use case. This module will likely be updated to leverage AVM resource modules in the future. This may result in breaking changes in upcoming versions when these features are implemented.\n" @@ -64,8 +64,8 @@ "azd": { "type": "location", "usageName": [ - "OpenAI.GlobalStandard.gpt-5, 150", - "OpenAI.GlobalStandard.gpt-5-mini, 100" + "OpenAI.GlobalStandard.gpt-5.4, 150", + "OpenAI.GlobalStandard.gpt-5.4-mini, 100" ] }, "description": "Required. Location for all AI service resources. This should be one of the supported Azure AI Service locations." @@ -73,17 +73,17 @@ }, "gptModelName": { "type": "string", - "defaultValue": "gpt-5-mini", + "defaultValue": "gpt-5.4-mini", "minLength": 1, "metadata": { - "description": "Optional. Name of the underlying GPT model to deploy. Defaults to gpt-5-mini (2025-08-07 series)." + "description": "Optional. Name of the underlying GPT model to deploy. Defaults to gpt-5.4-mini (2026-03-17 series)." } }, "gptModelVersion": { "type": "string", - "defaultValue": "2025-08-07", + "defaultValue": "2026-03-17", "metadata": { - "description": "Optional. Version of the GPT model to deploy. Defaults to 2025-08-07." + "description": "Optional. Version of the GPT model to deploy. Defaults to 2026-03-17 (gpt-5.4-mini release)." } }, "gptDeploymentName": { @@ -95,17 +95,17 @@ }, "gpt4_1ModelName": { "type": "string", - "defaultValue": "gpt-5", + "defaultValue": "gpt-5.4", "minLength": 1, "metadata": { - "description": "Optional. Name of the underlying larger GPT model to deploy. Defaults to gpt-5 (2025-08-07 series)." + "description": "Optional. Name of the underlying larger GPT model to deploy. Defaults to gpt-5.4 (2026-03-05 series)." } }, "gpt4_1ModelVersion": { "type": "string", - "defaultValue": "2025-08-07", + "defaultValue": "2026-03-05", "metadata": { - "description": "Optional. Version of the larger GPT model to deploy. Defaults to 2025-08-07." + "description": "Optional. Version of the larger GPT model to deploy. Defaults to 2026-03-05 (gpt-5.4 release)." } }, "gpt4_1DeploymentName": { @@ -117,17 +117,17 @@ }, "gptReasoningModelName": { "type": "string", - "defaultValue": "gpt-5-mini", + "defaultValue": "gpt-5.4-mini", "minLength": 1, "metadata": { - "description": "Optional. Name of the underlying GPT Reasoning model to deploy. Defaults to gpt-5-mini (reasoning-capable)." + "description": "Optional. Name of the underlying GPT Reasoning model to deploy. Defaults to gpt-5.4-mini (reasoning-capable, 2026-03-17 series)." } }, "gptReasoningModelVersion": { "type": "string", - "defaultValue": "2025-08-07", + "defaultValue": "2026-03-17", "metadata": { - "description": "Optional. Version of the GPT Reasoning model to deploy. Defaults to 2025-08-07." + "description": "Optional. Version of the GPT Reasoning model to deploy. Defaults to 2026-03-17 (gpt-5.4-mini release)." } }, "gptReasoningDeploymentName": { diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index 186a28e75..223e80e02 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -31,14 +31,14 @@ param location string var deployerInfo = deployer() var deployingUserPrincipalId = deployerInfo.objectId -// Restricting deployment to only supported Azure OpenAI regions validated with GPT-5 models +// Restricting deployment to only supported Azure OpenAI regions validated with GPT-5.4 models @allowed(['australiaeast', 'eastus2', 'francecentral', 'japaneast', 'norwayeast', 'swedencentral', 'uksouth', 'westus']) @metadata({ azd: { type: 'location' usageName: [ - 'OpenAI.GlobalStandard.gpt-5, 150' - 'OpenAI.GlobalStandard.gpt-5-mini, 100' + 'OpenAI.GlobalStandard.gpt-5.4, 150' + 'OpenAI.GlobalStandard.gpt-5.4-mini, 100' ] } }) @@ -46,31 +46,31 @@ var deployingUserPrincipalId = deployerInfo.objectId param azureAiServiceLocation string @minLength(1) -@description('Optional. Name of the underlying GPT model to deploy. Defaults to gpt-5-mini (2025-08-07 series).') -param gptModelName string = 'gpt-5-mini' +@description('Optional. Name of the underlying GPT model to deploy. Defaults to gpt-5.4-mini (2026-03-17 series).') +param gptModelName string = 'gpt-5.4-mini' -@description('Optional. Version of the GPT model to deploy. Defaults to 2025-08-07.') -param gptModelVersion string = '2025-08-07' +@description('Optional. Version of the GPT model to deploy. Defaults to 2026-03-17 (gpt-5.4-mini release).') +param gptModelVersion string = '2026-03-17' @description('Optional. Deployment (alias) name used in Azure OpenAI for the main GPT model. This is the value the application uses as `deployment_name` (including in data/agent_teams/*.json). Defaults to gptModelName.') param gptDeploymentName string = gptModelName @minLength(1) -@description('Optional. Name of the underlying larger GPT model to deploy. Defaults to gpt-5 (2025-08-07 series).') -param gpt4_1ModelName string = 'gpt-5' +@description('Optional. Name of the underlying larger GPT model to deploy. Defaults to gpt-5.4 (2026-03-05 series).') +param gpt4_1ModelName string = 'gpt-5.4' -@description('Optional. Version of the larger GPT model to deploy. Defaults to 2025-08-07.') -param gpt4_1ModelVersion string = '2025-08-07' +@description('Optional. Version of the larger GPT model to deploy. Defaults to 2026-03-05 (gpt-5.4 release).') +param gpt4_1ModelVersion string = '2026-03-05' @description('Optional. Deployment (alias) name used in Azure OpenAI for the larger GPT model. Defaults to gpt4_1ModelName.') param gpt4_1DeploymentName string = gpt4_1ModelName @minLength(1) -@description('Optional. Name of the underlying GPT Reasoning model to deploy. Defaults to gpt-5-mini (reasoning-capable).') -param gptReasoningModelName string = 'gpt-5-mini' +@description('Optional. Name of the underlying GPT Reasoning model to deploy. Defaults to gpt-5.4-mini (reasoning-capable, 2026-03-17 series).') +param gptReasoningModelName string = 'gpt-5.4-mini' -@description('Optional. Version of the GPT Reasoning model to deploy. Defaults to 2025-08-07.') -param gptReasoningModelVersion string = '2025-08-07' +@description('Optional. Version of the GPT Reasoning model to deploy. Defaults to 2026-03-17 (gpt-5.4-mini release).') +param gptReasoningModelVersion string = '2026-03-17' @description('Optional. Deployment (alias) name used in Azure OpenAI for the reasoning model. Must be unique from gptDeploymentName. Defaults to "{gptReasoningModelName}-reasoning" when it would otherwise collide with gptDeploymentName, otherwise gptReasoningModelName.') param gptReasoningDeploymentName string = gptReasoningModelName == gptModelName diff --git a/src/backend/.env.sample b/src/backend/.env.sample index 20a000531..58b66f522 100644 --- a/src/backend/.env.sample +++ b/src/backend/.env.sample @@ -3,15 +3,15 @@ COSMOSDB_DATABASE=macae COSMOSDB_CONTAINER=memory AZURE_OPENAI_ENDPOINT= -AZURE_OPENAI_MODEL_NAME=gpt-4.1-mini -AZURE_OPENAI_DEPLOYMENT_NAME=gpt-4.1-mini +AZURE_OPENAI_MODEL_NAME=gpt-5.4-mini +AZURE_OPENAI_DEPLOYMENT_NAME=gpt-5.4-mini AZURE_OPENAI_API_VERSION=2024-12-01-preview APPLICATIONINSIGHTS_INSTRUMENTATION_KEY= AZURE_AI_SUBSCRIPTION_ID= AZURE_AI_RESOURCE_GROUP= AZURE_AI_PROJECT_NAME= -AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4.1-mini +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-5.4-mini APPLICATIONINSIGHTS_CONNECTION_STRING= AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME=gpt-4o AZURE_OPENAI_RAI_DEPLOYMENT_NAME= diff --git a/src/backend/common/config/app_config.py b/src/backend/common/config/app_config.py index 88849390d..29dea29ee 100644 --- a/src/backend/common/config/app_config.py +++ b/src/backend/common/config/app_config.py @@ -48,17 +48,17 @@ def __init__(self): # Azure OpenAI settings self.AZURE_OPENAI_DEPLOYMENT_NAME = self._get_required( - "AZURE_OPENAI_DEPLOYMENT_NAME", "gpt-4o" + "AZURE_OPENAI_DEPLOYMENT_NAME", "gpt-5.4-mini" ) self.AZURE_OPENAI_RAI_DEPLOYMENT_NAME = self._get_required( - "AZURE_OPENAI_RAI_DEPLOYMENT_NAME", "gpt-5" + "AZURE_OPENAI_RAI_DEPLOYMENT_NAME", "gpt-5.4" ) self.AZURE_OPENAI_API_VERSION = self._get_required( "AZURE_OPENAI_API_VERSION", "2024-11-20" ) self.AZURE_OPENAI_ENDPOINT = self._get_required("AZURE_OPENAI_ENDPOINT") - self.REASONING_MODEL_NAME = self._get_optional("REASONING_MODEL_NAME", "gpt-5-mini-reasoning") + self.REASONING_MODEL_NAME = self._get_optional("REASONING_MODEL_NAME", "gpt-5.4-mini-reasoning") # self.AZURE_BING_CONNECTION_NAME = self._get_optional( # "AZURE_BING_CONNECTION_NAME" # ) diff --git a/src/tests/agents/interactive_test_harness/foundry_agent_interactive.py b/src/tests/agents/interactive_test_harness/foundry_agent_interactive.py index d9a52f251..988284ba5 100644 --- a/src/tests/agents/interactive_test_harness/foundry_agent_interactive.py +++ b/src/tests/agents/interactive_test_harness/foundry_agent_interactive.py @@ -29,7 +29,7 @@ "- Provide comprehensive answers combining multiple sources when relevant\n" "- Ask for clarification only if the task is genuinely ambiguous" ) -MODEL_DEPLOYMENT_NAME = "gpt-5" +MODEL_DEPLOYMENT_NAME = "gpt-5.4" async def test_agent(): """Simple chat test harness for the agent.""" print("🤖 Starting agent test harness...") diff --git a/src/tests/agents/test_foundry_integration.py b/src/tests/agents/test_foundry_integration.py index 5e12618ef..093da0c99 100644 --- a/src/tests/agents/test_foundry_integration.py +++ b/src/tests/agents/test_foundry_integration.py @@ -56,7 +56,7 @@ async def create_foundry_agent(self): "- Provide comprehensive answers combining multiple sources when relevant\n" "- Ask for clarification only if the task is genuinely ambiguous" ) - model_deployment_name = "gpt-5" + model_deployment_name = "gpt-5.4" agent = FoundryAgentTemplate( agent_name=agent_name, @@ -216,7 +216,7 @@ async def test_agent_initialization(self): @pytest.mark.asyncio async def test_agent_handles_missing_configs_gracefully(self): """Test that agent handles missing configurations without crashing.""" - model_deployment_name = "gpt-5" + model_deployment_name = "gpt-5.4" agent = FoundryAgentTemplate( agent_name="TestAgent", From 9fd3e4d35fb0a71c6dba7c36473a84103d94dd7d Mon Sep 17 00:00:00 2001 From: Roopan-Microsoft Date: Fri, 3 Jul 2026 16:14:48 +0530 Subject: [PATCH 03/12] Updated team config with isdefault and logic to display teams is updated --- .../agent_teams/contract_compliance_team.json | 1 + data/agent_teams/hr.json | 1 + data/agent_teams/marketing.json | 1 + data/agent_teams/retail.json | 1 + data/agent_teams/rfp_analysis_team.json | 1 + src/backend/common/database/cosmosdb.py | 27 +- .../common/database/database_factory.py | 66 ++-- src/backend/common/models/messages_af.py | 1 + .../v4/common/services/team_service.py | 1 + .../common/database/test_database_factory.py | 354 ++++++++++-------- 10 files changed, 265 insertions(+), 189 deletions(-) diff --git a/data/agent_teams/contract_compliance_team.json b/data/agent_teams/contract_compliance_team.json index 6b9dce2de..ec1a94c54 100644 --- a/data/agent_teams/contract_compliance_team.json +++ b/data/agent_teams/contract_compliance_team.json @@ -6,6 +6,7 @@ "created": "", "created_by": "", "deployment_name": "gpt-5.4-mini", + "is_default": true, "description": "A multi-agent compliance review team that summarizes NDAs, identifies risks, checks compliance, and recommends improvements using advanced legal reasoning and retrieval-augmented analysis.", "logo": "", "plan": "", diff --git a/data/agent_teams/hr.json b/data/agent_teams/hr.json index 83ff3e817..5991113ff 100644 --- a/data/agent_teams/hr.json +++ b/data/agent_teams/hr.json @@ -6,6 +6,7 @@ "created": "", "created_by": "", "deployment_name": "gpt-5.4-mini", + "is_default": true, "agents": [ { "input_key": "", diff --git a/data/agent_teams/marketing.json b/data/agent_teams/marketing.json index d01e76785..3753c2bfa 100644 --- a/data/agent_teams/marketing.json +++ b/data/agent_teams/marketing.json @@ -6,6 +6,7 @@ "created": "", "created_by": "", "deployment_name": "gpt-5.4-mini", + "is_default": true, "agents": [ { "input_key": "", diff --git a/data/agent_teams/retail.json b/data/agent_teams/retail.json index 85ce73ebc..8b66f3370 100644 --- a/data/agent_teams/retail.json +++ b/data/agent_teams/retail.json @@ -6,6 +6,7 @@ "created": "", "created_by": "", "deployment_name": "gpt-5.4-mini", + "is_default": true, "agents": [ { "input_key": "", diff --git a/data/agent_teams/rfp_analysis_team.json b/data/agent_teams/rfp_analysis_team.json index ae8b2a87d..44e1ee76a 100644 --- a/data/agent_teams/rfp_analysis_team.json +++ b/data/agent_teams/rfp_analysis_team.json @@ -6,6 +6,7 @@ "created": "", "created_by": "", "deployment_name": "gpt-5.4-mini", + "is_default": true, "description": "A specialized multi-agent team that analyzes RFP and contract documents to summarize content, identify potential risks, check compliance gaps, and provide action plans for contract improvement.", "logo": "", "plan": "", diff --git a/src/backend/common/database/cosmosdb.py b/src/backend/common/database/cosmosdb.py index 2dfc31d60..8e8c39c98 100644 --- a/src/backend/common/database/cosmosdb.py +++ b/src/backend/common/database/cosmosdb.py @@ -273,10 +273,11 @@ async def get_team(self, team_id: str) -> Optional[TeamConfiguration]: Returns: TeamConfiguration object or None if not found """ - query = "SELECT * FROM c WHERE c.team_id=@team_id AND c.data_type=@data_type" + query = "SELECT * FROM c WHERE c.team_id=@team_id AND c.data_type=@data_type AND (c.user_id=@user_id OR c.is_default=true)" parameters = [ {"name": "@team_id", "value": team_id}, {"name": "@data_type", "value": DataType.team_config}, + {"name": "@user_id", "value": self.user_id}, ] teams = await self.query_items(query, parameters, TeamConfiguration) return teams[0] if teams else None @@ -290,26 +291,25 @@ async def get_team_by_id(self, team_id: str) -> Optional[TeamConfiguration]: Returns: TeamConfiguration object or None if not found """ - query = "SELECT * FROM c WHERE c.team_id=@team_id AND c.data_type=@data_type" + query = "SELECT * FROM c WHERE c.team_id=@team_id AND c.data_type=@data_type AND (c.user_id=@user_id OR c.is_default=true)" parameters = [ {"name": "@team_id", "value": team_id}, {"name": "@data_type", "value": DataType.team_config}, + {"name": "@user_id", "value": self.user_id}, ] teams = await self.query_items(query, parameters, TeamConfiguration) return teams[0] if teams else None async def get_all_teams(self) -> List[TeamConfiguration]: - """Retrieve all team configurations for a specific user. - - Args: - user_id: The user_id to get team configurations for + """Retrieve all team configurations visible to the current user. Returns: - List of TeamConfiguration objects + List of TeamConfiguration objects: default teams plus user-specific teams """ - query = "SELECT * FROM c WHERE c.data_type=@data_type ORDER BY c.created DESC" + query = "SELECT * FROM c WHERE c.data_type=@data_type AND (c.user_id=@user_id OR c.is_default=true) ORDER BY c.created DESC" parameters = [ {"name": "@data_type", "value": DataType.team_config}, + {"name": "@user_id", "value": self.user_id}, ] teams = await self.query_items(query, parameters, TeamConfiguration) return teams @@ -317,6 +317,8 @@ async def get_all_teams(self) -> List[TeamConfiguration]: async def delete_team(self, team_id: str) -> bool: """Delete a team configuration by team_id. + Only user-owned teams can be deleted; default teams cannot be deleted. + Args: team_id: The team_id of the team configuration to delete @@ -328,9 +330,12 @@ async def delete_team(self, team_id: str) -> bool: try: # First find the team to get its document id and partition key team = await self.get_team(team_id) - print(team) - if team: - await self.delete_item(item_id=team.id, partition_key=team.session_id) + if not team: + return False + # Prevent deletion of default teams + if team.is_default: + return False + await self.delete_item(item_id=team.id, partition_key=team.session_id) return True except Exception as e: logging.exception(f"Failed to delete team from Cosmos DB: {e}") diff --git a/src/backend/common/database/database_factory.py b/src/backend/common/database/database_factory.py index 8c2f9fb0e..790a1af27 100644 --- a/src/backend/common/database/database_factory.py +++ b/src/backend/common/database/database_factory.py @@ -10,9 +10,16 @@ class DatabaseFactory: - """Factory class for creating database instances.""" + """Factory class for creating database instances. - _instance: Optional[DatabaseBase] = None + Caches the expensive CosmosDB connection infrastructure (client, database, + container) while creating per-request CosmosDBClient instances scoped to + the calling user_id. This ensures ownership filters in queries always + reference the correct user without race conditions across concurrent + asyncio tasks. + """ + + _shared_instance: Optional[CosmosDBClient] = None _logger = logging.getLogger(__name__) @staticmethod @@ -21,24 +28,24 @@ async def get_database( force_new: bool = False, ) -> DatabaseBase: """ - Get a database instance. + Get a database instance scoped to the given user_id. + + The underlying CosmosDB connection (client, database, container) is + shared across all requests. Each call returns a lightweight wrapper + bound to *user_id* so that query-level ownership predicates are + always correct — even under concurrent async execution. Args: - endpoint: CosmosDB endpoint URL - credential: Azure credential for authentication - database_name: Name of the CosmosDB database - container_name: Name of the CosmosDB container - session_id: Session ID for partitioning - user_id: User ID for data isolation - force_new: Force creation of new instance + user_id: User ID for data isolation (required for ownership checks) + force_new: Force re-creation of the shared connection Returns: - DatabaseBase: Database instance + DatabaseBase: Database instance scoped to user_id """ - # Create new instance if forced or if singleton doesn't exist - if force_new or DatabaseFactory._instance is None: - cosmos_db_client = CosmosDBClient( + # Ensure the shared connection infrastructure is initialized + if force_new or DatabaseFactory._shared_instance is None: + shared = CosmosDBClient( endpoint=config.COSMOSDB_ENDPOINT, credential=config.get_azure_credentials(), database_name=config.COSMOSDB_DATABASE, @@ -46,19 +53,30 @@ async def get_database( session_id="", user_id=user_id, ) + await shared.initialize() + DatabaseFactory._shared_instance = shared - await cosmos_db_client.initialize() - - if not force_new: - DatabaseFactory._instance = cosmos_db_client - - return cosmos_db_client + # Create a per-request instance that shares the connection but is + # bound to the caller's user_id + instance = CosmosDBClient( + endpoint=config.COSMOSDB_ENDPOINT, + credential=config.get_azure_credentials(), + database_name=config.COSMOSDB_DATABASE, + container_name=config.COSMOSDB_CONTAINER, + session_id="", + user_id=user_id, + ) + # Share the already-initialized connection objects + instance.client = DatabaseFactory._shared_instance.client + instance.database = DatabaseFactory._shared_instance.database + instance.container = DatabaseFactory._shared_instance.container + instance._initialized = True - return DatabaseFactory._instance + return instance @staticmethod async def close_all(): """Close all database connections.""" - if DatabaseFactory._instance: - await DatabaseFactory._instance.close() - DatabaseFactory._instance = None + if DatabaseFactory._shared_instance: + await DatabaseFactory._shared_instance.close() + DatabaseFactory._shared_instance = None diff --git a/src/backend/common/models/messages_af.py b/src/backend/common/models/messages_af.py index 493683966..2e3c61671 100644 --- a/src/backend/common/models/messages_af.py +++ b/src/backend/common/models/messages_af.py @@ -203,6 +203,7 @@ class TeamConfiguration(BaseDataModel): plan: str = "" starting_tasks: List[StartingTask] = Field(default_factory=list) user_id: str # who uploaded this configuration + is_default: bool = False # default teams are visible to all users class PlanWithSteps(Plan): diff --git a/src/backend/v4/common/services/team_service.py b/src/backend/v4/common/services/team_service.py index 3c78811cf..901add8db 100644 --- a/src/backend/v4/common/services/team_service.py +++ b/src/backend/v4/common/services/team_service.py @@ -112,6 +112,7 @@ async def validate_and_parse_team_config( plan=json_data.get("plan", ""), starting_tasks=starting_tasks, user_id=user_id, + is_default=json_data.get("is_default", False), ) self.logger.info( diff --git a/src/tests/backend/common/database/test_database_factory.py b/src/tests/backend/common/database/test_database_factory.py index e58be8672..f0e7c8558 100644 --- a/src/tests/backend/common/database/test_database_factory.py +++ b/src/tests/backend/common/database/test_database_factory.py @@ -58,9 +58,9 @@ class TestDatabaseFactoryInitialization: def test_database_factory_class_attributes(self): """Test that DatabaseFactory has correct class attributes.""" - assert hasattr(DatabaseFactory, '_instance') + assert hasattr(DatabaseFactory, '_shared_instance') assert hasattr(DatabaseFactory, '_logger') - assert DatabaseFactory._instance is None # Should start as None + assert DatabaseFactory._shared_instance is None # Should start as None assert isinstance(DatabaseFactory._logger, logging.Logger) def test_database_factory_is_static(self): @@ -78,19 +78,19 @@ def test_database_factory_is_static(self): assert get_database_method is not None assert close_all_method is not None - def test_singleton_instance_management(self): - """Test that singleton instance is properly managed.""" + def test_shared_instance_management(self): + """Test that shared instance is properly managed.""" # Reset instance to ensure clean state - DatabaseFactory._instance = None - assert DatabaseFactory._instance is None + DatabaseFactory._shared_instance = None + assert DatabaseFactory._shared_instance is None # Set a mock instance mock_instance = Mock(spec=DatabaseBase) - DatabaseFactory._instance = mock_instance - assert DatabaseFactory._instance is mock_instance + DatabaseFactory._shared_instance = mock_instance + assert DatabaseFactory._shared_instance is mock_instance # Reset for other tests - DatabaseFactory._instance = None + DatabaseFactory._shared_instance = None class TestDatabaseFactoryGetDatabase: @@ -98,19 +98,21 @@ class TestDatabaseFactoryGetDatabase: def setup_method(self): """Setup for each test method.""" - # Reset singleton instance before each test - DatabaseFactory._instance = None + DatabaseFactory._shared_instance = None def teardown_method(self): """Cleanup after each test method.""" - # Reset singleton instance after each test - DatabaseFactory._instance = None + DatabaseFactory._shared_instance = None @pytest.mark.asyncio - async def test_get_database_creates_new_instance_when_none_exists(self): - """Test that get_database creates new instance when singleton is None.""" + async def test_get_database_creates_shared_instance_when_none_exists(self): + """Test that get_database initializes the shared connection when none exists.""" mock_cosmos_client = Mock(spec=CosmosDBClient) mock_cosmos_client.initialize = AsyncMock() + mock_cosmos_client.client = Mock() + mock_cosmos_client.database = Mock() + mock_cosmos_client.container = Mock() + mock_cosmos_client._initialized = True mock_config = Mock() mock_config.COSMOSDB_ENDPOINT = "https://test.documents.azure.com:443/" @@ -122,49 +124,96 @@ async def test_get_database_creates_new_instance_when_none_exists(self): with patch('backend.common.database.database_factory.config', mock_config): result = await DatabaseFactory.get_database(user_id="test_user") - # Verify CosmosDBClient was created with correct parameters - mock_cosmos_class.assert_called_once_with( - endpoint="https://test.documents.azure.com:443/", - credential="mock_credentials", - database_name="test_db", - container_name="test_container", - session_id="", - user_id="test_user" - ) - - # Verify initialize was called + # Verify initialize was called on the shared instance mock_cosmos_client.initialize.assert_called_once() - # Verify instance is returned and stored as singleton - assert result is mock_cosmos_client - assert DatabaseFactory._instance is mock_cosmos_client + # Shared instance should be cached + assert DatabaseFactory._shared_instance is mock_cosmos_client + + # Result should have the correct user_id + assert result.user_id == "test_user" @pytest.mark.asyncio - async def test_get_database_returns_existing_singleton_instance(self): - """Test that get_database returns existing singleton instance.""" - # Set up existing singleton - existing_instance = Mock(spec=DatabaseBase) - DatabaseFactory._instance = existing_instance + async def test_get_database_returns_per_request_instance_with_correct_user_id(self): + """Test that each get_database call returns an instance scoped to the caller's user_id.""" + mock_shared = Mock(spec=CosmosDBClient) + mock_shared.initialize = AsyncMock() + mock_shared.client = Mock() + mock_shared.database = Mock() + mock_shared.container = Mock() + mock_shared._initialized = True + + mock_config = Mock() + mock_config.COSMOSDB_ENDPOINT = "https://test.documents.azure.com:443/" + mock_config.COSMOSDB_DATABASE = "test_db" + mock_config.COSMOSDB_CONTAINER = "test_container" + mock_config.get_azure_credentials.return_value = "mock_credentials" + + # Pre-set shared instance to simulate already-initialized state + DatabaseFactory._shared_instance = mock_shared with patch('backend.common.database.database_factory.CosmosDBClient') as mock_cosmos_class: - result = await DatabaseFactory.get_database(user_id="test_user") - - # Should not create new instance - mock_cosmos_class.assert_not_called() + # Make constructor return a real-enough mock with settable attributes + mock_per_request = Mock(spec=CosmosDBClient) + mock_per_request.user_id = "user_a" + mock_cosmos_class.return_value = mock_per_request - # Should return existing instance - assert result is existing_instance - assert DatabaseFactory._instance is existing_instance + with patch('backend.common.database.database_factory.config', mock_config): + result = await DatabaseFactory.get_database(user_id="user_a") + + # Per-request instance should share the connection from the shared instance + assert result.client is mock_shared.client + assert result.database is mock_shared.database + assert result.container is mock_shared.container @pytest.mark.asyncio - async def test_get_database_force_new_creates_new_instance(self): - """Test that get_database with force_new=True creates new instance.""" - # Set up existing singleton - existing_instance = Mock(spec=DatabaseBase) - DatabaseFactory._instance = existing_instance + async def test_get_database_different_users_get_different_instances(self): + """Test that different user_ids produce distinct instances.""" + mock_shared = Mock(spec=CosmosDBClient) + mock_shared.client = Mock() + mock_shared.database = Mock() + mock_shared.container = Mock() + mock_shared._initialized = True + DatabaseFactory._shared_instance = mock_shared - mock_cosmos_client = Mock(spec=CosmosDBClient) - mock_cosmos_client.initialize = AsyncMock() + mock_config = Mock() + mock_config.COSMOSDB_ENDPOINT = "https://test.documents.azure.com:443/" + mock_config.COSMOSDB_DATABASE = "test_db" + mock_config.COSMOSDB_CONTAINER = "test_container" + mock_config.get_azure_credentials.return_value = "mock_credentials" + + instances = [] + with patch('backend.common.database.database_factory.CosmosDBClient') as mock_cosmos_class: + def make_instance(**kwargs): + inst = Mock(spec=CosmosDBClient) + inst.user_id = kwargs.get('user_id', '') + return inst + mock_cosmos_class.side_effect = make_instance + + with patch('backend.common.database.database_factory.config', mock_config): + result1 = await DatabaseFactory.get_database(user_id="user_a") + result2 = await DatabaseFactory.get_database(user_id="user_b") + + # Should be different instances + assert result1 is not result2 + assert result1.user_id == "user_a" + assert result2.user_id == "user_b" + + @pytest.mark.asyncio + async def test_get_database_force_new_reinitializes_shared_connection(self): + """Test that force_new=True re-creates the shared connection.""" + old_shared = Mock(spec=CosmosDBClient) + old_shared.client = Mock() + old_shared.database = Mock() + old_shared.container = Mock() + DatabaseFactory._shared_instance = old_shared + + new_shared = Mock(spec=CosmosDBClient) + new_shared.initialize = AsyncMock() + new_shared.client = Mock() + new_shared.database = Mock() + new_shared.container = Mock() + new_shared._initialized = True mock_config = Mock() mock_config.COSMOSDB_ENDPOINT = "https://test.documents.azure.com:443/" @@ -172,32 +221,32 @@ async def test_get_database_force_new_creates_new_instance(self): mock_config.COSMOSDB_CONTAINER = "test_container" mock_config.get_azure_credentials.return_value = "mock_credentials" - with patch('backend.common.database.database_factory.CosmosDBClient', return_value=mock_cosmos_client) as mock_cosmos_class: + call_count = [0] + def make_instance(**kwargs): + call_count[0] += 1 + if call_count[0] == 1: + return new_shared + inst = Mock(spec=CosmosDBClient) + inst.user_id = kwargs.get('user_id', '') + return inst + + with patch('backend.common.database.database_factory.CosmosDBClient', side_effect=make_instance): with patch('backend.common.database.database_factory.config', mock_config): result = await DatabaseFactory.get_database(user_id="test_user", force_new=True) - # Verify new CosmosDBClient was created - mock_cosmos_class.assert_called_once_with( - endpoint="https://test.documents.azure.com:443/", - credential="mock_credentials", - database_name="test_db", - container_name="test_container", - session_id="", - user_id="test_user" - ) - - # Verify initialize was called - mock_cosmos_client.initialize.assert_called_once() - - # Verify new instance is returned but singleton is not updated - assert result is mock_cosmos_client - assert DatabaseFactory._instance is existing_instance # Should remain unchanged + # Shared instance should be replaced + assert DatabaseFactory._shared_instance is new_shared + new_shared.initialize.assert_called_once() @pytest.mark.asyncio async def test_get_database_with_empty_user_id(self): """Test that get_database works with empty user_id.""" mock_cosmos_client = Mock(spec=CosmosDBClient) mock_cosmos_client.initialize = AsyncMock() + mock_cosmos_client.client = Mock() + mock_cosmos_client.database = Mock() + mock_cosmos_client.container = Mock() + mock_cosmos_client._initialized = True mock_config = Mock() mock_config.COSMOSDB_ENDPOINT = "https://test.documents.azure.com:443/" @@ -209,17 +258,8 @@ async def test_get_database_with_empty_user_id(self): with patch('backend.common.database.database_factory.config', mock_config): result = await DatabaseFactory.get_database() # No user_id provided - # Verify CosmosDBClient was created with empty user_id - mock_cosmos_class.assert_called_once_with( - endpoint="https://test.documents.azure.com:443/", - credential="mock_credentials", - database_name="test_db", - container_name="test_container", - session_id="", - user_id="" - ) - - assert result is mock_cosmos_client + # Should still work; per-request instance created with empty user_id + assert result is not None @pytest.mark.asyncio async def test_get_database_initialization_error(self): @@ -238,8 +278,8 @@ async def test_get_database_initialization_error(self): with pytest.raises(Exception, match="Initialization failed"): await DatabaseFactory.get_database(user_id="test_user") - # Singleton should remain None after failure - assert DatabaseFactory._instance is None + # Shared instance should remain None after failure + assert DatabaseFactory._shared_instance is None class TestDatabaseFactoryCloseAll: @@ -247,58 +287,53 @@ class TestDatabaseFactoryCloseAll: def setup_method(self): """Setup for each test method.""" - # Reset singleton instance before each test - DatabaseFactory._instance = None + DatabaseFactory._shared_instance = None def teardown_method(self): """Cleanup after each test method.""" - # Reset singleton instance after each test - DatabaseFactory._instance = None + DatabaseFactory._shared_instance = None @pytest.mark.asyncio async def test_close_all_with_existing_instance(self): - """Test that close_all properly closes existing instance.""" - # Set up mock instance + """Test that close_all properly closes existing shared instance.""" mock_instance = Mock(spec=DatabaseBase) mock_instance.close = AsyncMock() - DatabaseFactory._instance = mock_instance + DatabaseFactory._shared_instance = mock_instance await DatabaseFactory.close_all() # Verify close was called mock_instance.close.assert_called_once() - # Verify singleton is reset to None - assert DatabaseFactory._instance is None + # Verify shared instance is reset to None + assert DatabaseFactory._shared_instance is None @pytest.mark.asyncio async def test_close_all_with_no_instance(self): """Test that close_all handles case when no instance exists.""" - # Ensure no instance exists - DatabaseFactory._instance = None + DatabaseFactory._shared_instance = None # Should not raise exception await DatabaseFactory.close_all() # Should remain None - assert DatabaseFactory._instance is None + assert DatabaseFactory._shared_instance is None @pytest.mark.asyncio async def test_close_all_handles_close_exception(self): """Test that close_all handles exceptions during close.""" - # Set up mock instance that raises exception on close mock_instance = Mock(spec=DatabaseBase) mock_instance.close = AsyncMock(side_effect=Exception("Close failed")) - DatabaseFactory._instance = mock_instance + DatabaseFactory._shared_instance = mock_instance # Should propagate the exception with pytest.raises(Exception, match="Close failed"): await DatabaseFactory.close_all() - # With exception, singleton may not be reset (depends on implementation) + # With exception, shared instance may not be reset (depends on implementation) # The current implementation doesn't use try-except, so the exception - # would prevent the _instance = None assignment - assert DatabaseFactory._instance is mock_instance + # would prevent the _shared_instance = None assignment + assert DatabaseFactory._shared_instance is mock_instance class TestDatabaseFactoryIntegration: @@ -306,19 +341,21 @@ class TestDatabaseFactoryIntegration: def setup_method(self): """Setup for each test method.""" - # Reset singleton instance before each test - DatabaseFactory._instance = None + DatabaseFactory._shared_instance = None def teardown_method(self): """Cleanup after each test method.""" - # Reset singleton instance after each test - DatabaseFactory._instance = None + DatabaseFactory._shared_instance = None @pytest.mark.asyncio - async def test_multiple_get_database_calls_return_same_instance(self): - """Test that multiple calls to get_database return the same instance.""" - mock_cosmos_client = Mock(spec=CosmosDBClient) - mock_cosmos_client.initialize = AsyncMock() + async def test_multiple_get_database_calls_return_different_user_scoped_instances(self): + """Test that multiple calls with different user_ids return separate instances.""" + mock_shared = Mock(spec=CosmosDBClient) + mock_shared.initialize = AsyncMock() + mock_shared.client = Mock() + mock_shared.database = Mock() + mock_shared.container = Mock() + mock_shared._initialized = True mock_config = Mock() mock_config.COSMOSDB_ENDPOINT = "https://test.documents.azure.com:443/" @@ -326,28 +363,37 @@ async def test_multiple_get_database_calls_return_same_instance(self): mock_config.COSMOSDB_CONTAINER = "test_container" mock_config.get_azure_credentials.return_value = "mock_credentials" - with patch('backend.common.database.database_factory.CosmosDBClient', return_value=mock_cosmos_client) as mock_cosmos_class: + call_count = [0] + def make_instance(**kwargs): + call_count[0] += 1 + if call_count[0] == 1: + # First call creates the shared instance + return mock_shared + inst = Mock(spec=CosmosDBClient) + inst.user_id = kwargs.get('user_id', '') + return inst + + with patch('backend.common.database.database_factory.CosmosDBClient', side_effect=make_instance): with patch('backend.common.database.database_factory.config', mock_config): - # First call result1 = await DatabaseFactory.get_database(user_id="user1") - - # Second call result2 = await DatabaseFactory.get_database(user_id="user2") - # Should only create one instance - mock_cosmos_class.assert_called_once() - - # Both calls should return the same instance - assert result1 is result2 - assert result1 is mock_cosmos_client + # Different user_ids should yield different instances + assert result1 is not result2 + # Both share the same underlying connection + assert result1.client is mock_shared.client + assert result2.client is mock_shared.client @pytest.mark.asyncio async def test_get_database_after_close_all(self): """Test that get_database works properly after close_all.""" - # First, create an instance mock_cosmos_client1 = Mock(spec=CosmosDBClient) mock_cosmos_client1.initialize = AsyncMock() mock_cosmos_client1.close = AsyncMock() + mock_cosmos_client1.client = Mock() + mock_cosmos_client1.database = Mock() + mock_cosmos_client1.container = Mock() + mock_cosmos_client1._initialized = True mock_config = Mock() mock_config.COSMOSDB_ENDPOINT = "https://test.documents.azure.com:443/" @@ -358,34 +404,43 @@ async def test_get_database_after_close_all(self): with patch('backend.common.database.database_factory.config', mock_config): with patch('backend.common.database.database_factory.CosmosDBClient', return_value=mock_cosmos_client1): result1 = await DatabaseFactory.get_database(user_id="test_user") - assert result1 is mock_cosmos_client1 - assert DatabaseFactory._instance is mock_cosmos_client1 + assert DatabaseFactory._shared_instance is mock_cosmos_client1 # Close all connections await DatabaseFactory.close_all() - assert DatabaseFactory._instance is None + assert DatabaseFactory._shared_instance is None # Create a new instance mock_cosmos_client2 = Mock(spec=CosmosDBClient) mock_cosmos_client2.initialize = AsyncMock() + mock_cosmos_client2.client = Mock() + mock_cosmos_client2.database = Mock() + mock_cosmos_client2.container = Mock() + mock_cosmos_client2._initialized = True with patch('backend.common.database.database_factory.config', mock_config): with patch('backend.common.database.database_factory.CosmosDBClient', return_value=mock_cosmos_client2): result2 = await DatabaseFactory.get_database(user_id="test_user") - # Should create new instance - assert result2 is mock_cosmos_client2 - assert DatabaseFactory._instance is mock_cosmos_client2 - assert result2 is not result1 + # Should create new shared instance + assert DatabaseFactory._shared_instance is mock_cosmos_client2 @pytest.mark.asyncio - async def test_force_new_does_not_affect_singleton(self): - """Test that force_new instances don't interfere with singleton.""" + async def test_force_new_replaces_shared_instance(self): + """Test that force_new replaces the shared connection instance.""" mock_cosmos_client1 = Mock(spec=CosmosDBClient) mock_cosmos_client1.initialize = AsyncMock() + mock_cosmos_client1.client = Mock() + mock_cosmos_client1.database = Mock() + mock_cosmos_client1.container = Mock() + mock_cosmos_client1._initialized = True - mock_cosmos_client2 = Mock(spec=CosmosDBClient) + mock_cosmos_client2 = Mock(spec=CosmosDBClient) mock_cosmos_client2.initialize = AsyncMock() + mock_cosmos_client2.client = Mock() + mock_cosmos_client2.database = Mock() + mock_cosmos_client2.container = Mock() + mock_cosmos_client2._initialized = True mock_config = Mock() mock_config.COSMOSDB_ENDPOINT = "https://test.documents.azure.com:443/" @@ -394,25 +449,15 @@ async def test_force_new_does_not_affect_singleton(self): mock_config.get_azure_credentials.return_value = "mock_credentials" with patch('backend.common.database.database_factory.config', mock_config): - # Create singleton instance + # Create initial shared instance with patch('backend.common.database.database_factory.CosmosDBClient', return_value=mock_cosmos_client1): - singleton = await DatabaseFactory.get_database(user_id="user1") - assert DatabaseFactory._instance is mock_cosmos_client1 + await DatabaseFactory.get_database(user_id="user1") + assert DatabaseFactory._shared_instance is mock_cosmos_client1 - # Create force_new instance + # force_new should replace the shared instance with patch('backend.common.database.database_factory.CosmosDBClient', return_value=mock_cosmos_client2): - force_new = await DatabaseFactory.get_database(user_id="user2", force_new=True) - - # force_new should return new instance - assert force_new is mock_cosmos_client2 - - # But singleton should remain unchanged - assert DatabaseFactory._instance is mock_cosmos_client1 - assert singleton is not force_new - - # Subsequent call should still return singleton - result = await DatabaseFactory.get_database(user_id="user3") - assert result is mock_cosmos_client1 + await DatabaseFactory.get_database(user_id="user2", force_new=True) + assert DatabaseFactory._shared_instance is mock_cosmos_client2 class TestDatabaseFactoryConfigurationHandling: @@ -420,13 +465,11 @@ class TestDatabaseFactoryConfigurationHandling: def setup_method(self): """Setup for each test method.""" - # Reset singleton instance before each test - DatabaseFactory._instance = None + DatabaseFactory._shared_instance = None def teardown_method(self): """Cleanup after each test method.""" - # Reset singleton instance after each test - DatabaseFactory._instance = None + DatabaseFactory._shared_instance = None @pytest.mark.asyncio async def test_config_values_passed_correctly(self): @@ -471,8 +514,8 @@ async def test_config_credential_error(self): with pytest.raises(Exception, match="Credential error"): await DatabaseFactory.get_database(user_id="test_user") - # Singleton should remain None after credential error - assert DatabaseFactory._instance is None + # Shared instance should remain None after credential error + assert DatabaseFactory._shared_instance is None class TestDatabaseFactoryLogging: @@ -497,11 +540,11 @@ class TestDatabaseFactoryErrorHandling: def setup_method(self): """Setup for each test method.""" - DatabaseFactory._instance = None + DatabaseFactory._shared_instance = None def teardown_method(self): """Cleanup after each test method.""" - DatabaseFactory._instance = None + DatabaseFactory._shared_instance = None @pytest.mark.asyncio async def test_cosmos_client_creation_failure(self): @@ -517,14 +560,14 @@ async def test_cosmos_client_creation_failure(self): with pytest.raises(Exception, match="Client creation failed"): await DatabaseFactory.get_database(user_id="test_user") - # Singleton should remain None - assert DatabaseFactory._instance is None + # Shared instance should remain None + assert DatabaseFactory._shared_instance is None @pytest.mark.asyncio async def test_state_consistency_after_errors(self): """Test that factory state remains consistent after various errors.""" # Start with clean state - assert DatabaseFactory._instance is None + assert DatabaseFactory._shared_instance is None # Simulate creation failure mock_config = Mock() @@ -535,11 +578,15 @@ async def test_state_consistency_after_errors(self): await DatabaseFactory.get_database() # State should remain clean - assert DatabaseFactory._instance is None + assert DatabaseFactory._shared_instance is None # Now create successful instance mock_cosmos_client = Mock(spec=CosmosDBClient) mock_cosmos_client.initialize = AsyncMock() + mock_cosmos_client.client = Mock() + mock_cosmos_client.database = Mock() + mock_cosmos_client.container = Mock() + mock_cosmos_client._initialized = True good_config = Mock() good_config.COSMOSDB_ENDPOINT = "https://test.documents.azure.com:443/" @@ -550,8 +597,7 @@ async def test_state_consistency_after_errors(self): with patch('backend.common.database.database_factory.CosmosDBClient', return_value=mock_cosmos_client): with patch('backend.common.database.database_factory.config', good_config): result = await DatabaseFactory.get_database() - assert result is mock_cosmos_client - assert DatabaseFactory._instance is mock_cosmos_client + assert DatabaseFactory._shared_instance is mock_cosmos_client if __name__ == "__main__": From 21fc8d19d0d0b62ecfabf4285565a3f79e4fbaab Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Fri, 3 Jul 2026 16:44:55 +0530 Subject: [PATCH 04/12] update deployment guide and azure.yaml --- azure.yaml | 47 ++++------- docs/DeploymentGuide.md | 94 +++++++-------------- docs/images/Deployment_success_message.png | Bin 53974 -> 51001 bytes 3 files changed, 46 insertions(+), 95 deletions(-) diff --git a/azure.yaml b/azure.yaml index 12a9cdc05..18e04ab40 100644 --- a/azure.yaml +++ b/azure.yaml @@ -1,37 +1,10 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json name: multi-agent-custom-automation-engine-solution-accelerator -# metadata: -# template: multi-agent-custom-automation-engine-solution-accelerator@1.0 +metadata: + template: multi-agent-custom-automation-engine-solution-accelerator@1.0 requiredVersions: azd: '>= 1.18.0 != 1.23.9' hooks: - postprovision: - windows: - run: | - # Build the backend, frontend and MCP images, push them to the ACR - # provisioned by this deployment and update the Container Apps / - # Web App to use the freshly built images. - # - # Set AZURE_ENV_SKIP_IMAGE_BUILD=true to skip this step. - # Set AZURE_ENV_BUILD_MODE=local to build with local Docker instead - # of the default remote 'az acr build'. - & "$PWD/infra/scripts/Build-And-Push-Images.ps1" - shell: pwsh - interactive: true - continueOnError: false - posix: - run: | - # Build the backend, frontend and MCP images, push them to the ACR - # provisioned by this deployment and update the Container Apps / - # Web App to use the freshly built images. - # - # Set AZURE_ENV_SKIP_IMAGE_BUILD=true to skip this step. - # Set AZURE_ENV_BUILD_MODE=local to build with local Docker instead - # of the default remote 'az acr build'. - bash "$PWD/infra/scripts/build_and_push_images.sh" - shell: sh - interactive: true - continueOnError: false postdeploy: windows: run: | @@ -44,6 +17,11 @@ hooks: Write-Host "===============================================================" -ForegroundColor Yellow Write-Host "" + Write-Host " Build and push the backend/frontend/mcp_server container images to ACR, then point the container app and webapp at them:" -ForegroundColor White + Write-Host " 👉 Run the following command in Bash:" -ForegroundColor White + Write-Host " bash infra/scripts/build_and_push_images.sh" -ForegroundColor Cyan + Write-Host "" + Write-Host " Upload Team Configurations and index sample data" -ForegroundColor White Write-Host " 👉 Run the following command in Bash:" -ForegroundColor White Write-Host " bash infra/scripts/selecting_team_config_and_data.sh" -ForegroundColor Cyan @@ -60,6 +38,11 @@ hooks: Write-Host "===============================================================" -ForegroundColor Yellow Write-Host "" + Write-Host " Build and push the backend/frontend/mcp_server container images to ACR, then point the container app and webapp at them:" -ForegroundColor White + Write-Host " 👉 Run the following command in PowerShell:" -ForegroundColor White + Write-Host " infra\scripts\Build-And-Push-Images.ps1" -ForegroundColor Cyan + Write-Host "" + Write-Host " Upload Team Configurations and index sample data" -ForegroundColor White Write-Host " 👉 Run the following command in PowerShell:" -ForegroundColor White Write-Host " infra\scripts\Selecting-Team-Config-And-Data.ps1" -ForegroundColor Cyan @@ -85,6 +68,10 @@ hooks: printf "${Green} POST-DEPLOYMENT STEPS (Bash)\n" printf "${Yellow}===============================================================${NC}\n\n" + printf "Build and push the backend/frontend/mcp_server container images to ACR, then point the container app and webapp at them:\n" + printf " 👉 Run the following command in Bash:\n" + printf " ${Blue}bash infra/scripts/build_and_push_images.sh${NC}\n\n" + printf "Upload Team Configurations and index sample data:\n" printf " 👉 Run the following command in Bash:\n" printf " ${Blue}bash infra/scripts/selecting_team_config_and_data.sh${NC}\n\n" @@ -93,4 +80,4 @@ hooks: printf " ${Blue}https://%s${NC}\n\n" "$webSiteDefaultHostname" shell: sh - interactive: true + interactive: true \ No newline at end of file diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md index 869242348..00410ce03 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -322,66 +322,8 @@ azd up **Expected Duration:** 9-10 minutes for default configuration -> **Note — Automatic container image build & push:** After the Bicep templates finish provisioning, `azd` runs an automatic **postprovision** hook that: -> -> 1. Builds the `backend`, `frontend` and `mcp` container images from `src/backend`, `src/App` and `src/mcp_server`. -> 2. Pushes them to the Azure Container Registry (ACR) that was just provisioned in your resource group. -> 3. Updates the two Container Apps and the frontend Web App to run the freshly pushed images. -> -> Add another 4-8 minutes for this step. See [Container image build behavior](#42a-container-image-build-behavior-optional) below to change how the images are built or to skip the step. -### 4.2.a Container image build behavior (optional) - -The postprovision hook is driven by environment variables you can set on the `azd` environment **before** running `azd up`. - -| `azd env set` key | Values | Default | Purpose | -|-------------------|--------|---------|---------| -| `AZURE_ENV_BUILD_MODE` | `remote` \| `local` | `remote` | `remote` builds each image with `az acr build` (an ACR Task — no local Docker required). `local` runs `docker build` + `docker push` from your machine. | -| `AZURE_ENV_IMAGE_TAG` | any tag | `latest` | Tag applied to every image pushed to the ACR. | -| `AZURE_ENV_SKIP_IMAGE_BUILD` | `true` \| unset | unset | When `true`, the postprovision hook is skipped entirely. Use this if you plan to push images yourself. | - -**Examples** - -```shell -# Default: remote ACR build, tag "latest" (no local Docker needed) -azd up - -# Local Docker build with a custom tag -azd env set AZURE_ENV_BUILD_MODE local -azd env set AZURE_ENV_IMAGE_TAG dev -azd up - -# Skip the automatic build step (I'll push my own images later) -azd env set AZURE_ENV_SKIP_IMAGE_BUILD true -azd up -``` - -**Prerequisites** - -- `remote` mode requires only the Azure CLI (`az`) that comes with the deployment environments listed in Step 2. -- `local` mode additionally requires [Docker Desktop](https://www.docker.com/products/docker-desktop/) to be running on the machine where `azd up` is executed. - -**Running the script manually** - -You can also invoke the same script directly, for example after a code change: - -```powershell -# PowerShell (Windows) -infra\scripts\Build-And-Push-Images.ps1 # remote build, tag "latest" -infra\scripts\Build-And-Push-Images.ps1 -BuildMode local # local Docker build -infra\scripts\Build-And-Push-Images.ps1 -ImageTag v1.2.0 # custom tag -``` - -```bash -# Bash (Linux / macOS / WSL) -bash infra/scripts/build_and_push_images.sh # remote build, tag "latest" -AZURE_ENV_BUILD_MODE=local bash infra/scripts/build_and_push_images.sh -AZURE_ENV_IMAGE_TAG=v1.2.0 bash infra/scripts/build_and_push_images.sh -``` - -The script reads the ACR name, resource group and target Container App / Web App names from the current `azd` environment (`azd env get-values`), so it must be run from the project root after a successful `azd provision` / `azd up`. - -- **Upon successful completion**, you will see a success message indicating that all resources have been deployed, along with the application URL and next steps for uploading team configurations and sample data. +- **Upon successful completion**, you will see a success message indicating that all resources have been deployed, along with the application URL and next steps for Build and push the backend/frontend/mcp_server container images to ACR, then point the container app and webapp at them and uploading team configurations and sample data. ![Deployment Success message](./images/Deployment_success_message.png) @@ -399,9 +341,27 @@ After successful deployment: ## Step 5: Post-Deployment Configuration -### 5.1 Run Post Deployment Script +When `azd up` finishes, the `postdeploy` hook (defined in `azure.yaml`) prints the exact commands to run next. Complete the following steps, in order, from the project root before accessing the application. + +### 5.1 Build and Push Container Images + +Build and push the backend, frontend, and mcp_server container images to ACR, then point the Container App and Web App at them. Run the command for your shell: + + - **For Bash (Linux/macOS/WSL):** + ```bash + bash infra/scripts/build_and_push_images.sh + ``` + + - **For PowerShell (Windows):** + ```powershell + infra\scripts\Build-And-Push-Images.ps1 + ``` -1. You can upload Team Configurations using command printed in the terminal. The command will look like one of the following. Run the appropriate command for your shell from the project root: +The script reads the ACR name, resource group and target Container App / Web App names from the current `azd` environment, so it must be run from the project root after a successful `azd up`. See [Step 4.2](#step-4-deploy-application) for build options (`remote` vs `local` mode and custom image tags). + +### 5.2 Upload Team Configurations and Index Sample Data + +1. Upload Team Configurations and index the sample data using the command printed in the terminal. Run the appropriate command for your shell from the project root: - **For Bash (Linux/macOS/WSL):** ```bash @@ -418,25 +378,29 @@ After successful deployment: ![Usecase selection](./images/Usecase_selection.png) +### 5.3 Access the Application + +Once both scripts complete, access your deployed frontend application at the URL printed by the `postdeploy` hook (`https://`), or retrieve it from the Azure Portal as described in [Step 4.3](#43-get-application-url). + -### 5.2 Configure Authentication (Optional) +### 5.4 Configure Authentication (Optional) 1. Follow [App Authentication Configuration](./azure_app_service_auth_setup.md) 2. Wait up to 10 minutes for authentication changes to take effect -### 5.3 Verify Deployment +### 5.5 Verify Deployment 1. Access your application using the URL from Step 4.3 2. Confirm the application loads successfully -### 5.4 Test the Application +### 5.6 Test the Application **Quick Test Steps:** 1. **Access the application** using the URL from Step 4.3 2. **Sign in** with your authenticated account -3. **Select a use case** from the available scenarios you uploaded in Step 5.1 +3. **Select a use case** from the available scenarios you uploaded in Step 5.2 4. **Ask a sample question** relevant to the selected use case 5. **Verify the response** includes appropriate multi-agent collaboration 6. **Check the logs** in Azure Portal to confirm backend processing diff --git a/docs/images/Deployment_success_message.png b/docs/images/Deployment_success_message.png index 679ead4da5f4d135010d667fa31dc47bb442313b..65935fb7ce6fc7b22cb47903176a90bd3193057f 100644 GIT binary patch literal 51001 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 LEyXhVH^Ki0$+^ih literal 53974 zcmdRUc{tQx|F@JBp;Y!IBnb)G%_tPI7DBcxk$uZL7*a`!B>SFy-*;n_Wvnyy!C_mv_kI8F=X(Bo{(G+HT-VI!dY^M{qs2o^DK6tp&4`4)_80jXuUNbn(@t#0^3VYf9yCATKKS@v!VEgt;~$qjaH+z zsQNLsJI^a#J<)OqiPhB0yV&|GKY;|=4R z|CMSU{l6a)_18}b-Qs}7zVV$6(bAw*4*1ICd1s91!QI%VoN$b}7@H|0()uv;WW5Mu z^+M~7kFPH;CR6a=&Br?fSRDZlOY1R_cy-l)tE7Y~e6&E#^UGwwzWMLn z!=38U&sNj&h1ba=#!1X>4*J3U#Dj5z7QzB(xUJyoe-$aptnXA~yQj$mIee~!=2-T< zONv?z*>(5h+geDQ)=_9oBUuU76+#EiX-fCoiU{MYm?c)T`5`mZ1L>1tCr5%A{x_>I zh;$>1vo_=4&(XfPX&n4|(e98$$F`#Mk#v)(pzBV?z-iditA5BW>npE<#q3qV5lNVp zS~$zfiN;Y-e-YF1dDJ>i>Fr~8ewUM7^ zwTdlReUjy(TQAAXgkvf=;p@8k2}eB^A-l5g!hA&$b8q!6rSBHs`;P&{bEuQ!#_Bbl zxU6h}T-}~+W*u+2Cnak{&*>Mc^jY94iahMtSkSMc=R8RdTrq;Bs!2WBuS{e`pqHL$ z8Xg|Z#=kqUV2j2(?tAg#pU2TgmeS0KaZV9s@3pu8h~NHFSm8)@l0kbk$@r#aZp>@$ zgXL3kJB3(fG_accdJgcp;x1?$T#hdi}fuuaXOpwJ`iPyqL`rM>e=~ewlH*+jUFU;T*Nj67?vz*F-9AYv<^m@`S zrQLSTwn{)m&r0I!*X->@rPulwTxU_81orReb+ons*M0BJ(v#@Ak7fII>JYtVqzB^} zoDpM>&l?$s;1#&LjzpxvQ(ecff%Dm2B(6Cm$3v*TO_AM-0DP zSQ4BDoS*KU=JapFbqp`NU9nhhW2rh}FixfretJt@{d;<2x9rVds3#Eo;A!ZvE`HwC z!!@Gvcg98ByWcy=aCear-&=;1iUP(HMyrt%rgxPw5?e1@;`gxFdl1U$~z}g+~wndpP*yBA|>Do0>E)e_B?d1p! zw~yb}9Z;!6Z!1=Q=hwg`Cz5hvHU1hxb#yeOcr64Kr7iPYZqv#`ieKylH7uBs5eP3> z90B_x8m#G$;#MlWm!pIKqE&*b2oW*4;fO=w4`uXrIf(X!eG(jdtwh>8i%>!w+H%}( zH`lqoe)#e?(sWj%w8-Unm7}E5_D9VR+ejOG8Hpg2*tb1IMPf?Q1GHKe=Zh^BdvM>J`r`XVv>-2!pRvhI^;0LVrZ!86nEs z?HVcbG2K1p7zg6MV$^)>Jm24#IWFanmcxc12$danwq)?9>wY^y(I3pR+4~)PXWe>} zm9wQeb>?Rv)rU6k8^-)vLsO6~m;?^b`vEv^`Fc5%93>W+@l6$blrmHoV~Xhcj04!$ zXMSC5?DV^u{kBxHZT&IEqR<|N;&kLr_e{S(5+QthHp8>M5DI97@PfiglQ5)x&CFMt zX2d3Jx}Ejo3?K3e<8k}0@9WFaaC)dGB{bn9-tFFJ8}cVI=B9Ryiprp+e205~TjtF` zy+VJk(}jhYQ2Wj_S)S=|K=z}mnS~jl^QYfV9cpa1t+~48J1HLD!BKfw3RJc#RV%NmJ`$Stq<4;15KNtJ`oM5FGCQnMLer8d% z&*sq!JW(hoA;t{94i!Z&G0j>rW+?Y9ObqiV(p!>Po0^KdqYbi`ZA6A8e~y(YnfA~Y z@(sYkraKqZt(BR#-*mCS+Wi%5u7hq)h7!I!x#?;wQaEK|=>fc2J6K{}K|ASc{E+|d zD?eo^|9!@K2bn4+zUcY3P#T_E8R@U#_t+(iH^drATCB?iY2PejB%~?n3b#F;$v6y< zWL+oVodvGSnck79iL0^Y=R0Q1w8)GKY6onQMdw5Ey_Vdkm$rwP*vbLbA6i_wMs6*D zZI)Dw40GXKS7IC{ItL{LiVQO)!i zgAKg|NMtPhiu8V4_Wtsu*n<)FgCun_&Yu4tzoQb_9fbK?buC=}=xpJauWP%p94*q( zf0IX~_b(jTllMn_ijkofHxPF&_xX9IpD%7ak~@M^G)GQQDxP;h%iM-uRHmHJUwfw< zZv+&AU!OmE$Q>s#^Y*)=lX^+H1(LLD8GssomC;u%E7yDR_;<;wq3PB39MZi`5(rq! zhL8;oO3M>irWzb;gJ@Vuy3bp3!RxJnO~-ilb`;G$Z=K}535BLF z!~E-3+hg8}Qc~4~@Bncq8Bc)n4{cXK_EO!HzTHZlCoyC}TeX_q&W_>Ql^fwM+pyY@ z(dCEI{FlsySd%Lq+(<5%^G~WgM0z`9zfQTm-ZHX|m{j+F3Y1M^s&|~Wn4&Gck(_g{ z`vuZRk}RIo?izIzM^9z-BW0ftBX-;x%X!>(@iBCTJ0cYpG>2;?)HdY=@qJ0CqS{_5 z+UDJIwH9X~v~bwW{&6-cFxfj^+G2Q`7Wp+-YKL%<{PZFeEvf<1Md{4v+Sk}Rr6~U8 z>Z5!OdGFqHI%E9ojU|$X`*B#G5Bc>ZtWDDwc%;6Wufdf+?BI}tr~BS83ST&qdo>l5 z0z)M)wN`BMrYsDpI^qf*LsjXS4to5-Jlp~YYEAdtC7+xct?6!3C&tJieeVdJOSuguw@pAkmW zmWCrLsMMXG#7E9AGdF4;Wd(sR?T1fqY?K6-l)SOeH+}85dX#8=ZZdw_CDq2T+K$zD zSkPYeVRK~`F+8sP>y^)*;qmt}A>IqN$l* zWLWCl*HFFicBJz+i7eL1$l>Da+!c{@aWo!HbfQsO3>A7-(i?I)k8?L}#R%s)sy%)@ zob$e(Xvll!*oH4@tQ2`|k3!k4Z%Lr0Z=tiGPY&FE-1@p-VS$`udxgh_MGQW4&C7)B z$-S~5PS(J?zHYIfMoHN`^%^z7N}yKIu8kUl(}UK?Uv%`+eK|R#TH(g4!44-KP^{z; zGdEMUjQF%MUr);D;1uy|>9`X0nuNZ24AxF^rJDM>{O8#QuynxI&NKva%(pVx_k$U( z>(Mc{ccbbS7tC8!RK=39HoPqS)CG~dt>p0l;(Qr=xy~nO<=iyoi_>1oKLPUS{BltD zCXdNn)kU)p5UL= z6+TQ3=)^H}8`DbTH+NaJ*f>)k6>$977UN5E8-rTH_a(>oDwN7d1zJILFKs}S1~ock z(ff}lhnIt;WQKc}F#?bUW^vn!Q!friBNQV zj496-2Jc{1p;@kLOWEY$jPCv2a$;}w_=23<$4uxxtrpc;($WG6j{h1Xo-gMoT4IC* zTQAh79_)K`h57Q&a$&`=q+hf%Tt3LwkNQbOQ_Ghhq{_pl)&RN2taWo6ME91O3=@O&HO7~0wznR$XI@UO zNl7SLIHmo`{&+CAm#3b{O!B{9CoA;08UhB;b@FZMi2D;Tvg4ylhWpp*{U*_PV$Gz!|l;ETfTt}T;*w;I}}+b z82a5c@j(zB751q*C;eTR2r)wZBFIr~i6vz56`tFM0ZUmEtoO2L{|aTtdz#L-Woq&r12|$SGSXEklj@woZOC_NVyDgZbUaq};hGV)Oi`Z|~WuqNPV`uWW&6XPWkU9v5Ad?TX}2 zSEjw0+rY0r_g^c7Wll{mWj-#gqCUSp9$r7>bgi!ZoBvT7c%lZC3Aw@GQ74>+hsaP} z#a{zg!*RGNPSuke@6cAdPZd)lCo03M(V%wFG`JaW#-8(e!0OD#b9^dVut^suTyN$+ z8UZ)?{<(RH?0*}@a?mL^7{EYNSGF_1lO=iB4TG|P1xg>m-JQ07Jjtk#$vUbiO^Z`= z6O20z1q58|vsr!V>7|otDV_8ctGalFNwP5`V)72v&J1UJTGn5{d!}74bGkgdUKqdT zaIJKo7gX+tU2%YulM>n-VXXO*z-|M3$<#IFey44SUac`Eb>8ht+tqPihjItAoF^qA zY*k#n&tklP|F~u>JYpGDY0@4^xF8)I^kMg7>!skm)xm}_LOb|1i?FJ2G=7%$uvUtr zj15Hp1JAum z5ID6*19EfcPHgfx<@1JZ6Y zON%J9Jkv~UCceJRu0EKH+mF+x{+-WF_J4FUe8ZFztyggYfjHOgWAY&^yZPrzrUC;j zc3%}A@tqu(2m8Agt%8sEmH594VxRCQB`z}@ibT3swW-L97L4Ud89Ak{W<>(e4xg<$DXI19_F1V@oU?e`{C?Aa%axYC=8q5Q zL7vD8SDO<$f@ymEJ>ierOz%rC#5yb7*X9Jxo(`MgXzcBszFKI%cNRG{ro zGV*mjUm<7-+9ILW)R%kZ_wKRY_qT9C8w zWaHjabOLyhcaqpjZU?0dqC4&|`nrwwWNQM*!5#1VX4K)}e zHDaZxo71ktZyZ5QDQAqoN04lClvPnoXesk!vW8Nm5}k`Ano&mh{M-*&Ygp+DjB7uV$i&JZGq z(~mHmRuW73Z&g0d?jK(&)(_hF&4`CmIkbm-9KHDw6(wvNcKSWDjzG#_=4sLTy)3d3 zH~#UUKh!1>(RjF+g+%A%5?^a@a)`6<`Xu%Iz6bbc(asl`td9G)rxnf) ze0D6~|AnS=n|9eyZ53)FoW!U&89G{TmIv$ zV=W10*<5~a6r(CcEVUM!GNUOCzvdv&4}2zf)k2KbO1;e2MEsQSXswP&A#{QTj_wJ@ zr|amSF6)9CWMrY#@S41*(moSfPh+_T>6K~YGF2g=9Wvi!<_WUvryCip+=fp8CQmf} z)$TWrJGh^U@bkJ1_po4%O4cULy{GZ$UC$Vk=m*ZHokOnzRNi_l*kpTIa6|6 zlsbeP@65=V5Zlix{gv}S3a;@w1Lk5>mx^7V(PZb8N=<;bj4Gm;c$Qg3rDsrQq|f)& z{3vCcLY&#pet*7QP> z7GNBjiJ8vwBnpU8KgQjgIO8(*t}*N+(J+*<0qFim$zL+Z9ry$CqG&F z^W3muP4s_Am@_KI?(zRU=jeO(xLUa;8_1|%f1CA|L5`5|NACq9cDR;)c=rG zXBW2rQa=COE#kjC*8d+vKl_J={Zaz{vrPZIEi7J0bBoQ?z zX!394KKt%wpff4Z7?{V4%YTa&KB}0(sGrR|YcCi=Y63;)p^CN(R)Tuz4PUR33{nHw zbB*szyB(OP*aB|}KIvQB9r_jgJhIe6{t$LVFtdYovnuZdyD=`2 zzPN8~H7|N!7@3$ zIeLQ(IsPxr3DmUC9c51^tkwCxX-^>6r#b4b{n6z+2+~jIco%8SIPa^)+}y6f7efWo zvyNR;k~1NX^3CoU6@DdAC0`l(9a+@OwN}ZP~ zc6f^Bb1gl-;Z6^h*PuW_<#0WFCEL20wd4?6ph%3R%l%-3$p*$sk?7|ROu-5IHHmX1 zWipy=p}aMt8_{@@Pe@8`O7KHv6_B~*dh?TGZE0vzNmIm~X(AtfpZFXy#^kwA-W9LS z+HeBC(>5nfJ{&97Dtqbrm0!dXbqnp&ul)TFyj|tXE=2h@)_{L817zEtm%)c;KMJ#z z+O1Ljn2)0w*w?^5#pJ!lbdKGXkjK<^_xC#I283{bAc&mGb;o z{{D@%N!gp%4g+w`qCL1Wvy1fnVb|l5`cW`| z>NtC(Usy!Gt(vu~gzHbZ4TUDc8Uu@BGqfx%Za*pA{@LtPor2bygB4gs->o~39yv-i zX4ON)w_g|0wmwkL4J)rZS+INJ>;a7re)>f1_NKy%^2SWN#p9R3A^ta9<~l*_0yzwZ z+qv|j7Uw>MZYI$#k)NCL|#yyI$x~%TD{;&QpA@oxnzETQQ}(!WvYoka@s<6iT$I z3MHbd_iu61?0L7(6lV~tf6@}CyjN?DFHCJ|$x|R;A^t#WmjBo$H734v7`IXsWYwRA zj4s-|q1Q%U(pk1c1wcMc(TK#=zDrg#5lzAa$ePrlBpI6L2VC#k_ImtnO_>dxf9qPj zaWQ=W)iOj~wz$2stkv3~`JR-u>*n4vnDOBKGqu#v_>7&9SYwCX&vIGVK2%d(xu&0o z58ctT<{yUa_k*8C&zK%rx&4*C7|@Z)edepa4}b&XKiP!oElWBY5?td_ywB405*;sq(&g7SgE~q zxu&8eHd47rA^59ItZ8UL#BvtPFc>ETv+pWXDy1yTXr3MLu^)E=@W?}!bX6;(><6+U zv+Av}hl7y;lv+o{S*EIuVkM7eo7TBGOU)g)6Hh}F$clt8;%N$6{oTMjcaoB~vs)Tl znuqglZ$jJgWne?xHXp-g!3&O-h&4(qA{MLEK`qbBJNnlwexG(i9a51Qyc~$jCMbAt zZ?{txO?dsNVR6e<>4Y>&(no2=AQ^ZN$|;7Y3ufXsca)0vlRCWv+kJ>dXJAG1 zM8SlWx{ckx+g%_FjBjWIul#W8()7D&RsNTi?g?Zm&G!bWhhDlf>)H3u6u3t&gevWD znxaIucX{Fd7N7B$v`uuoB1(^fvX4a!RmF5BDUuMe6-^5p4S==j21>Y%5>!I0XY?qS z7`x9h9H7#C)C{8L7kvx;#)cz+zlrL>CbN(dsf|OU#mP0$Q*}J=XlRmREBM$GjocxP zjjsLchZ26ev&qX^=;~STvQ1fP)5K`#p}|f)h)vQj%eBOQ42Vz!rZ$hXfOGRmpM-!| z8|i?S4zn@bbbkp<J%EGw4*4-29-74@Hcwpd z5a>h)AOpgWRt{(XrX$gKrDOsoV8N>qEvKvm{O5mi-^4c69krndlXd5laX*{~d5}R3 zQ8_`(HL(zsLokL7CIcoHDMLp6Gu+2qQq8CRhYnANW6MHuVt6)v(A{eCryzGaU_b`e z{UHGaqraYsoOWti-VTl8(XkGokf=$Dcee2moce;Ao3cQ%xzxR=?^&TQ@!Y+kmVX<} zTcXN6h=49#3x%Ld>*jX3Lx1|!?;`GsrEl`xr5+@Q!QQ*)PShY?K7wd$h1D6dS4#YJ zWh`1yTTlym+9J=GZCjq-Fb8KSw%5Tn?9Rx@zd!)7Y|lW|X*PqXMZH+LE=Q$-;aLn= zTpk(UMydLj!5sVhf3Zi~D08AE+@fP4RN%_-x(OC)L7mxKh61V-_pNH}33YKwjSWX9 zKs(UL-u&@o2bO|pi}eicsN+C2;V3XketI-nb@@0ufj5e|+<)t&4)2Mc9jn zv2CC$Ub?UBU2r67=1sTG3Gi%io~ zCvc{=!>&zdh!3-=g*tLg0v zdG@_7iV_VM=&}ZIutNfrS0rcK~5 zlDakW@D4a&EJb-(6sN(*y`{=3^XDP`M8xnM<)`m-o~e0;mZ~mdujBBb=jWS;S;60# zq9C6%`GEajOKhIQ9->oMU40o|!gx-4IT*BrgXhgTkNLTe_?^1sgRX*Q^U{9)sMHV` zAS=bl(wLfJM@wt!{jI50qU#|}&y>l+c@<894Nr2k=a-tun877qki9tgZpK$Bn^^d# zWci0+hAU&uLV5MU*VivS-|Y|5A^-+y1s30)4v3-HJG z2q|>*Dq6uthfi_qd_1H+B5uo*i0(GX6s10yvbR?tPU`b4Y}WGVVTYEIrxiOB@9Tn` zz6YUoGESys{ra4FlvS6e6J&HRLo9vi4NT9C$uP(w+z+Q`JEedj_5z$mb$vq@Hejg< zABBUwz<0u$BZqiQ)s@xtG0eb_0(R|9HJiVMVR@}Us($9kH|wg9zStq2IXwl~CROq? zk3V!YE-I5q^H-O-&9q&u2|1}}EiSETDb(^NBq%O#W0&$2(VU6k07yd`XBp*MKuo~K zhH&e^%B-x)Z!~9Q%&>pdzi%{(WjPHE})4xysw zsgF#VD4Qg zMaMV10pe-P8x0$%$cc7hBGeh=S&E-UbL||vL5t|;2o_Pwd?!T&MRjN+BwG&~P8en~axiNUTj{U&{ZWL8TnR_aS}Cs^m^xtfGwm>1|MHu=`^KzZ>ITd#*>e?nC(MPD`D8LtR)1 zNE=icP_mrr4sY!{X0Y=eHRw^uutl0Tg%S+=?)0NcHl|x z!eU#G&cPC9)xvfHSJ$U{NW3ulaxZGz=o~qAd8d_`(zK6Kb60*`C2E*@^METG%HQI1 zi8$jV00ml{BY5{A*)V_`V{G|Ozd-n4$=aXvjEEnviILY z+j>2vehTW8pQv;+qjo>)7X%&BNP9MCwtOOeQewjCXCmUk%dt=MaE7}!{|WVK2%y;Z zZ#qBubZSI0{k*xL`i97`I47*l28-T`Xwg%&%Kc?ZENTMJawVq&ZSdwo&qsQOU{ z7Nbl#j2ZPMH}PYXKy%FA<)_0`mD&0rPxgwU4VHCL!?v5-;c+i8d)(iGS`cBi)`1C) zkLr}3?3O+I_S^v=oAJ`*8=rxx+@>bM=W(!dl50;f(86pQJ`Soks?(=A-I%O<;Bd&F z4^mX9x6g9YHw({&gJN4eCvwCCn=(i135)&89fnyA42a8u7Be2xY}qb^aGNhSvy^u|UD`){}zaNT10 zT=s&|^|auwYu*F!$| zANpD1?WmE;Z3wUPBMV31j?}(l`#Y!pP~v@RP170 zT(2^{Kz5U(s$B4}luGFrYJ1li!jtmGIHX&{>MZk_7+U<2d-`p1Sb3f~tno*HKCSWM z`jKo%>}1>QjLu$lU3oyL_P4D5Zk0XZ5*m8_eE>~jVEsOGp)x4;<*EI3w)yqcElQ;b z|KH0_afwW#a7AUvWZS3n+D+c-bU9P*a{&#$R7q>qMdejWZK{4koDVH_GwU+&{6t!! z^6~4cn)jxYue?=G97OKONfpr+_mx%o*%{{zo+}P85HSb~aA-Bs;|ZmYliSVgrj8z& zWKsh@@n;T85;n6_z^pmqR`d;TjD-wZraVSIm?O{_3gL^a=C_4yQ=2X%0D!>~=e^&^Ua*DyJGnnk1AO>t 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Ξfo-d>EtbpZZHq=qik4 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 From 56c041164ebe4ec6634adcf36860f7b63d3814da Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Fri, 3 Jul 2026 16:58:50 +0530 Subject: [PATCH 05/12] fix test cases --- .../backend/common/database/test_cosmosdb.py | 9 ++++++--- .../common/database/test_database_factory.py | 15 +++++++++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/tests/backend/common/database/test_cosmosdb.py b/src/tests/backend/common/database/test_cosmosdb.py index 31cfe34ce..79272950d 100644 --- a/src/tests/backend/common/database/test_cosmosdb.py +++ b/src/tests/backend/common/database/test_cosmosdb.py @@ -704,10 +704,11 @@ async def test_get_team_found(self, client): result = await client.get_team("test_team_id") assert result == mock_team - expected_query = "SELECT * FROM c WHERE c.team_id=@team_id AND c.data_type=@data_type" + expected_query = "SELECT * FROM c WHERE c.team_id=@team_id AND c.data_type=@data_type AND (c.user_id=@user_id OR c.is_default=true)" expected_params = [ {"name": "@team_id", "value": "test_team_id"}, {"name": "@data_type", "value": DataType.team_config}, + {"name": "@user_id", "value": "test_user"}, ] client.query_items.assert_called_once_with(expected_query, expected_params, TeamConfiguration) @@ -739,9 +740,10 @@ async def test_get_all_teams(self, client): result = await client.get_all_teams() assert result == mock_teams - expected_query = "SELECT * FROM c WHERE c.data_type=@data_type ORDER BY c.created DESC" + expected_query = "SELECT * FROM c WHERE c.data_type=@data_type AND (c.user_id=@user_id OR c.is_default=true) ORDER BY c.created DESC" expected_params = [ {"name": "@data_type", "value": DataType.team_config}, + {"name": "@user_id", "value": "test_user"}, ] client.query_items.assert_called_once_with(expected_query, expected_params, TeamConfiguration) @@ -751,6 +753,7 @@ async def test_delete_team_success(self, client): mock_team = Mock(spec=TeamConfiguration) mock_team.id = "test_id" mock_team.session_id = "test_session" + mock_team.is_default = False # Mock get_team to return the team with patch.object(client, 'get_team', return_value=mock_team): @@ -766,7 +769,7 @@ async def test_delete_team_not_found(self, client): with patch.object(client, 'get_team', return_value=None): result = await client.delete_team("test_team_id") - assert result is True + assert result is False client.delete_item.assert_not_called() diff --git a/src/tests/backend/common/database/test_database_factory.py b/src/tests/backend/common/database/test_database_factory.py index f0e7c8558..f7a277355 100644 --- a/src/tests/backend/common/database/test_database_factory.py +++ b/src/tests/backend/common/database/test_database_factory.py @@ -113,6 +113,7 @@ async def test_get_database_creates_shared_instance_when_none_exists(self): mock_cosmos_client.database = Mock() mock_cosmos_client.container = Mock() mock_cosmos_client._initialized = True + mock_cosmos_client.user_id = "test_user" mock_config = Mock() mock_config.COSMOSDB_ENDPOINT = "https://test.documents.azure.com:443/" @@ -476,6 +477,9 @@ async def test_config_values_passed_correctly(self): """Test that configuration values are passed correctly to CosmosDBClient.""" mock_cosmos_client = Mock(spec=CosmosDBClient) mock_cosmos_client.initialize = AsyncMock() + mock_cosmos_client.client = Mock() + mock_cosmos_client.database = Mock() + mock_cosmos_client.container = Mock() mock_credentials = Mock() mock_config = Mock() @@ -488,8 +492,11 @@ async def test_config_values_passed_correctly(self): with patch('backend.common.database.database_factory.config', mock_config): await DatabaseFactory.get_database(user_id="custom_user") - # Verify all config values were passed correctly - mock_cosmos_class.assert_called_once_with( + # get_database builds a shared instance and a per-request + # instance, so the client is constructed twice with the same + # config-derived arguments. + assert mock_cosmos_class.call_count == 2 + mock_cosmos_class.assert_called_with( endpoint="https://custom.documents.azure.com:443/", credential=mock_credentials, database_name="custom_database", @@ -498,8 +505,8 @@ async def test_config_values_passed_correctly(self): user_id="custom_user" ) - # Verify get_azure_credentials was called - mock_config.get_azure_credentials.assert_called_once() + # Verify get_azure_credentials was invoked for each construction + assert mock_config.get_azure_credentials.call_count == 2 @pytest.mark.asyncio async def test_config_credential_error(self): From 663f35016e8a8edcaff396f73eb02aaaf63162dc Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Mon, 6 Jul 2026 10:12:01 +0530 Subject: [PATCH 06/12] updated model name related docs and var --- .github/workflows/azd-template-validation.yml | 2 +- .github/workflows/azure-dev.yml | 2 +- .github/workflows/deploy-waf.yml | 2 +- TRANSPARENCY_FAQS.md | 2 +- docs/ACRBuildAndPushGuide.md | 6 ++++-- docs/AzureGPTQuotaSettings.md | 2 +- docs/CustomizingAzdParameters.md | 10 +++++----- docs/ManualAzureDeployment.md | 2 +- docs/quota_check.md | 10 +++++----- infra/scripts/checkquota.sh | 5 ++--- infra/scripts/quota_check_params.sh | 2 +- 11 files changed, 23 insertions(+), 22 deletions(-) diff --git a/.github/workflows/azd-template-validation.yml b/.github/workflows/azd-template-validation.yml index 62b01e2a7..8b87dbb0e 100644 --- a/.github/workflows/azd-template-validation.yml +++ b/.github/workflows/azd-template-validation.yml @@ -34,7 +34,7 @@ jobs: AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} AZURE_ENV_AI_SERVICE_LOCATION: ${{ vars.AZURE_AI_DEPLOYMENT_LOCATION }} AZURE_ENV_GPT_MODEL_CAPACITY: 1 - AZURE_ENV_MODEL_4_1_CAPACITY: 1 # keep low to avoid potential quota issues + AZURE_ENV_MODEL_5_4_CAPACITY: 1 # keep low to avoid potential quota issues GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: print result diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index 0b7da43c0..a73745595 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -18,7 +18,7 @@ jobs: AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} AZURE_ENV_AI_SERVICE_LOCATION: ${{ vars.AZURE_AI_DEPLOYMENT_LOCATION }} AZURE_ENV_GPT_MODEL_CAPACITY: 1 - AZURE_ENV_MODEL_4_1_CAPACITY: 1 + AZURE_ENV_MODEL_5_4_CAPACITY: 1 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} steps: diff --git a/.github/workflows/deploy-waf.yml b/.github/workflows/deploy-waf.yml index ace2f4dc2..b8595ae07 100644 --- a/.github/workflows/deploy-waf.yml +++ b/.github/workflows/deploy-waf.yml @@ -134,7 +134,7 @@ jobs: location="${{ env.AZURE_LOCATION }}" \ azureAiServiceLocation='${{ env.AZURE_LOCATION }}' \ gptDeploymentCapacity=${{ env.GPT_MIN_CAPACITY }} \ - gpt4_1ModelCapacity=${{ env.GPT41_MINI_MIN_CAPACITY }} \ + gpt5_4ModelCapacity=${{ env.GPT41_MINI_MIN_CAPACITY }} \ gptReasoningModelCapacity=${{ env.O4_MINI_MIN_CAPACITY }} \ enableTelemetry=true \ enableMonitoring=true \ diff --git a/TRANSPARENCY_FAQS.md b/TRANSPARENCY_FAQS.md index b7fd9593f..2518e99c8 100644 --- a/TRANSPARENCY_FAQS.md +++ b/TRANSPARENCY_FAQS.md @@ -26,4 +26,4 @@ The system allows users to review, reorder and approve steps generated in a plan ## What operational factors and settings allow for effective and responsible use of Multi Agent: Custom Automation Engine – Solution Accelerator? Effective and responsible use of the Multi Agent: Custom Automation Engine – Solution Accelerator depends on several operational factors and settings. The system is designed to perform reliably and safely across a range of business tasks that it was evaluated for. Users can customize certain settings, such as the planning language model used by the system, the types of tasks that agents are assigned, and the specific actions that agents can take (e.g., sending emails or scheduling orientation sessions for new employees). However, it's important to note that these choices may impact the system's behavior in real-world scenarios. -For example, selecting a planning language model that is not well-suited to the complexity of the tasks may result in lower accuracy and performance. Similarly, assigning tasks that are outside the system's intended scope may lead to errors or incomplete results. Users can choose the LLM that is optimized for responsible use. The default LLM is GPT-4o which inherits the existing RAI mechanisms and filters from the LLM provider. Caching is enabled by default to increase reliability and control cost. We encourage developers to review [OpenAI’s Usage policies](https://openai.com/policies/usage-policies/) and [Azure OpenAI’s Code of Conduct](https://learn.microsoft.com/en-us/legal/cognitive-services/openai/code-of-conduct) when using GPT-40. To ensure effective and responsible use of the accelerator, users should carefully consider their choices and use the system within its intended scope. \ No newline at end of file +For example, selecting a planning language model that is not well-suited to the complexity of the tasks may result in lower accuracy and performance. Similarly, assigning tasks that are outside the system's intended scope may lead to errors or incomplete results. Users can choose the LLM that is optimized for responsible use. The default LLM is GPT-5.4 which inherits the existing RAI mechanisms and filters from the LLM provider. Caching is enabled by default to increase reliability and control cost. We encourage developers to review [OpenAI’s Usage policies](https://openai.com/policies/usage-policies/) and [Azure OpenAI’s Code of Conduct](https://learn.microsoft.com/en-us/legal/cognitive-services/openai/code-of-conduct) when using GPT-5.4. To ensure effective and responsible use of the accelerator, users should carefully consider their choices and use the system within its intended scope. \ No newline at end of file diff --git a/docs/ACRBuildAndPushGuide.md b/docs/ACRBuildAndPushGuide.md index b836ebc7a..127e77b7a 100644 --- a/docs/ACRBuildAndPushGuide.md +++ b/docs/ACRBuildAndPushGuide.md @@ -2,6 +2,8 @@ This guide provides step-by-step instructions to build and push Docker images for **WebApp** and **Backend** services into Azure Container Registry (ACR). +> **💡 Tip:** For a standard `azd up` deployment you do **not** need to run these commands manually. The post-deployment scripts `infra/scripts/build_and_push_images.sh` (Bash) and `infra/scripts/Build-And-Push-Images.ps1` (PowerShell) build and push the backend, frontend, and mcp_server images to ACR and repoint the apps automatically. Use the manual steps below only for custom or one-off image builds. + ## 📋 Prerequisites Before starting, ensure you have: - An active [Azure Subscription](https://portal.azure.com/) @@ -33,7 +35,7 @@ az acr login --name $ACR_NAME ```bash az acr login --name -docker build --no-cache -f docker/Backend.Dockerfile -t /: . +docker build --no-cache -f src/backend/Dockerfile -t /: ./src/backend docker push /: ``` @@ -56,7 +58,7 @@ docker push /: ```bash az acr login --name -docker build --no-cache -f docker/Frontend.Dockerfile -t /: . +docker build --no-cache -f src/App/Dockerfile -t /: ./src/App docker push /: ``` diff --git a/docs/AzureGPTQuotaSettings.md b/docs/AzureGPTQuotaSettings.md index a8f7d6c5b..ae514eb9a 100644 --- a/docs/AzureGPTQuotaSettings.md +++ b/docs/AzureGPTQuotaSettings.md @@ -5,6 +5,6 @@ 3. **Go to** the `Management Center` from the bottom-left navigation menu. 4. Select `Quota` - Click on the `GlobalStandard` dropdown. - - Select the required **GPT model** (`GPT-4o`) + - Select the required **GPT model** (`gpt-5.4` / `gpt-5.4-mini`) - Choose the **region** where the deployment is hosted. 5. Request More Quota or delete any unused model deployments as needed. diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md index 6b238361b..d3f6c8f9c 100644 --- a/docs/CustomizingAzdParameters.md +++ b/docs/CustomizingAzdParameters.md @@ -16,11 +16,11 @@ By default this template will use the environment name as the prefix to prevent | `AZURE_ENV_GPT_MODEL_VERSION` | string | `2026-03-17` | Version of the GPT model to be used for deployment. | | `AZURE_ENV_GPT_DEPLOYMENT_NAME` | string | (defaults to `AZURE_ENV_GPT_MODEL_NAME`) | Optional. Deployment (alias) name used in Azure OpenAI for the main GPT model. This is the value referenced as `deployment_name` in `data/agent_teams/*.json`. Override this if you want the deployment alias to differ from the underlying model name. | | `AZURE_ENV_GPT_MODEL_CAPACITY` | int | `50` | Sets the GPT model capacity. | -| `AZURE_ENV_MODEL_4_1_DEPLOYMENT_TYPE` | string | `GlobalStandard` | Defines the deployment type for the AI model (e.g., Standard, GlobalStandard). | -| `AZURE_ENV_MODEL_4_1_NAME` | string | `gpt-5.4` | Specifies the underlying larger GPT model to be deployed. | -| `AZURE_ENV_MODEL_4_1_VERSION` | string | `2026-03-05` | Version of the larger GPT model to be used for deployment. | -| `AZURE_ENV_MODEL_4_1_DEPLOYMENT_NAME` | string | (defaults to `AZURE_ENV_MODEL_4_1_NAME`) | Optional. Deployment (alias) name used in Azure OpenAI for the larger GPT model. Override if the alias should differ from the underlying model name. | -| `AZURE_ENV_MODEL_4_1_CAPACITY` | int | `150` | Sets the GPT model capacity. | +| `AZURE_ENV_MODEL_5_4_DEPLOYMENT_TYPE` | string | `GlobalStandard` | Defines the deployment type for the AI model (e.g., Standard, GlobalStandard). | +| `AZURE_ENV_MODEL_5_4_NAME` | string | `gpt-5.4` | Specifies the underlying larger GPT model to be deployed. | +| `AZURE_ENV_MODEL_5_4_VERSION` | string | `2026-03-05` | Version of the larger GPT model to be used for deployment. | +| `AZURE_ENV_MODEL_5_4_DEPLOYMENT_NAME` | string | (defaults to `AZURE_ENV_MODEL_5_4_NAME`) | Optional. Deployment (alias) name used in Azure OpenAI for the larger GPT model. Override if the alias should differ from the underlying model name. | +| `AZURE_ENV_MODEL_5_4_CAPACITY` | int | `150` | Sets the GPT model capacity. | | `AZURE_ENV_REASONING_MODEL_DEPLOYMENT_TYPE` | string | `GlobalStandard` | Defines the deployment type for the AI model (e.g., Standard, GlobalStandard). | | `AZURE_ENV_REASONING_MODEL_NAME` | string | `gpt-5.4-mini` | Specifies the underlying reasoning GPT model to be deployed. | | `AZURE_ENV_REASONING_MODEL_VERSION` | string | `2026-03-17` | Version of the reasoning GPT model to be used for deployment. | diff --git a/docs/ManualAzureDeployment.md b/docs/ManualAzureDeployment.md index 19d6ba7e5..f70bfc220 100644 --- a/docs/ManualAzureDeployment.md +++ b/docs/ManualAzureDeployment.md @@ -90,7 +90,7 @@ To add your newly created backend image: value: name: 'AZURE_OPENAI_DEPLOYMENT_NAME' - value: 'gpt-4o' + value: 'gpt-5.4-mini' name: 'AZURE_OPENAI_API_VERSION' value: '2024-08-01-preview' diff --git a/docs/quota_check.md b/docs/quota_check.md index 0a64fec1d..de8d0e106 100644 --- a/docs/quota_check.md +++ b/docs/quota_check.md @@ -1,7 +1,7 @@ ## Check Quota Availability Before Deployment Before deploying the accelerator, **ensure sufficient quota availability** for the required model. -> **For Global Standard | GPT-4o - the capacity to at least 150k tokens for optimal performance.** +> **For Global Standard | gpt-5.4 - set the capacity to at least 150k tokens for optimal performance.** ### Login if you have not done so already ``` @@ -42,7 +42,7 @@ australiaeast, eastus2, francecentral, japaneast, norwayeast, swedencentral, uks ``` ✔️ Check specific model(s) in default regions: ``` - ./quota_check_params.sh --models gpt4.1:150 + ./quota_check_params.sh --models gpt-5.4:150 ``` ✔️ Check default models in specific region(s): ``` @@ -50,15 +50,15 @@ australiaeast, eastus2, francecentral, japaneast, norwayeast, swedencentral, uks ``` ✔️ Passing Both models and regions: ``` - ./quota_check_params.sh --models gpt4.1:150 --regions eastus2,westus + ./quota_check_params.sh --models gpt-5.4:150 --regions eastus2,westus ``` ✔️ All parameters combined: ``` - ./quota_check_params.sh --models gpt4.1:150 --regions eastus2,westus --verbose + ./quota_check_params.sh --models gpt-5.4:150 --regions eastus2,westus --verbose ``` ✔️ Multiple models with single region: ``` - ./quota_check_params.sh --models gpt4.1:150,gpt4.1-mini:50 --regions eastus2 --verbose + ./quota_check_params.sh --models gpt-5.4:150,gpt-5.4-mini:100 --regions eastus2 --verbose ``` ### **Sample Output** diff --git a/infra/scripts/checkquota.sh b/infra/scripts/checkquota.sh index b79815716..9150953e2 100644 --- a/infra/scripts/checkquota.sh +++ b/infra/scripts/checkquota.sh @@ -25,9 +25,8 @@ echo "✅ Azure subscription set successfully." # Define models and their minimum required capacities declare -A MIN_CAPACITY=( - ["OpenAI.GlobalStandard.o4-mini"]="${O4_MINI_MIN_CAPACITY}" - ["OpenAI.GlobalStandard.gpt4.1"]="${GPT_MIN_CAPACITY}" - ["OpenAI.GlobalStandard.gpt4.1-mini"]="${GPT41_MINI_MIN_CAPACITY}" + ["OpenAI.GlobalStandard.gpt-5.4-mini"]="$(( ${GPT_MIN_CAPACITY:-0} + ${O4_MINI_MIN_CAPACITY:-0} ))" + ["OpenAI.GlobalStandard.gpt-5.4"]="${GPT41_MINI_MIN_CAPACITY}" ) VALID_REGION="" diff --git a/infra/scripts/quota_check_params.sh b/infra/scripts/quota_check_params.sh index e46f88011..44d799312 100644 --- a/infra/scripts/quota_check_params.sh +++ b/infra/scripts/quota_check_params.sh @@ -47,7 +47,7 @@ log_verbose() { } # Default Models and Capacities (Comma-separated in "model:capacity" format) -DEFAULT_MODEL_CAPACITY="gpt4.1:150,o4-mini:50,gpt4.1-mini:50" +DEFAULT_MODEL_CAPACITY="gpt-5.4:150,gpt-5.4-mini:100" # Convert the comma-separated string into an array IFS=',' read -r -a MODEL_CAPACITY_PAIRS <<< "$DEFAULT_MODEL_CAPACITY" From 4541d7d761e43956ebfd976f0f3e205ff7b25ae1 Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Mon, 6 Jul 2026 10:35:36 +0530 Subject: [PATCH 07/12] updated var names according to gpt model --- infra/main.bicep | 66 ++++++++++++++++--------------- infra/main.json | 72 ++++++++++++++++++---------------- infra/main.parameters.json | 20 +++++----- infra/main.waf.parameters.json | 20 +++++----- infra/main_custom.bicep | 60 ++++++++++++++-------------- src/backend/.env.sample | 7 ++-- 6 files changed, 126 insertions(+), 119 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index fffdd0a53..bde78636a 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -57,13 +57,13 @@ param gptDeploymentName string = gptModelName @minLength(1) @description('Optional. Name of the underlying larger GPT model to deploy. Defaults to gpt-5.4 (2026-03-05 series).') -param gpt4_1ModelName string = 'gpt-5.4' +param gpt5_4ModelName string = 'gpt-5.4' @description('Optional. Version of the larger GPT model to deploy. Defaults to 2026-03-05 (gpt-5.4 release).') -param gpt4_1ModelVersion string = '2026-03-05' +param gpt5_4ModelVersion string = '2026-03-05' -@description('Optional. Deployment (alias) name used in Azure OpenAI for the larger GPT model. Defaults to gpt4_1ModelName.') -param gpt4_1DeploymentName string = gpt4_1ModelName +@description('Optional. Deployment (alias) name used in Azure OpenAI for the larger GPT model. Defaults to gpt5_4ModelName.') +param gpt5_4DeploymentName string = gpt5_4ModelName @minLength(1) @description('Optional. Name of the underlying GPT Reasoning model to deploy. Defaults to gpt-5.4-mini (reasoning-capable, 2026-03-17 series).') @@ -89,7 +89,7 @@ param azureAiAgentAPIVersion string = '2025-01-01-preview' 'GlobalStandard' ]) @description('Optional. GPT model deployment type. Defaults to GlobalStandard.') -param gpt4_1ModelDeploymentType string = 'GlobalStandard' +param gpt5_4ModelDeploymentType string = 'GlobalStandard' @minLength(1) @allowed([ @@ -111,7 +111,7 @@ param gptReasoningModelDeploymentType string = 'GlobalStandard' param gptDeploymentCapacity int = 50 @description('Optional. AI model deployment token capacity. Defaults to 150 for optimal performance.') -param gpt4_1ModelCapacity int = 150 +param gpt5_4ModelCapacity int = 150 @description('Optional. AI model deployment token capacity. Defaults to 50 for optimal performance.') param gptReasoningModelCapacity int = 50 @@ -789,14 +789,14 @@ var aiFoundryAiServicesModelDeployment = { } raiPolicyName: 'Microsoft.Default' } -var aiFoundryAiServices4_1ModelDeployment = { +var aiFoundryAiServices5_4ModelDeployment = { format: 'OpenAI' - deploymentName: gpt4_1DeploymentName - name: gpt4_1ModelName - version: gpt4_1ModelVersion + deploymentName: gpt5_4DeploymentName + name: gpt5_4ModelName + version: gpt5_4ModelVersion sku: { - name: gpt4_1ModelDeploymentType - capacity: gpt4_1ModelCapacity + name: gpt5_4ModelDeploymentType + capacity: gpt5_4ModelCapacity } raiPolicyName: 'Microsoft.Default' } @@ -838,16 +838,16 @@ module existingAiFoundryAiServicesDeployments 'modules/ai-services-deployments.b } } { - name: aiFoundryAiServices4_1ModelDeployment.deploymentName + name: aiFoundryAiServices5_4ModelDeployment.deploymentName model: { - format: aiFoundryAiServices4_1ModelDeployment.format - name: aiFoundryAiServices4_1ModelDeployment.name - version: aiFoundryAiServices4_1ModelDeployment.version + format: aiFoundryAiServices5_4ModelDeployment.format + name: aiFoundryAiServices5_4ModelDeployment.name + version: aiFoundryAiServices5_4ModelDeployment.version } - raiPolicyName: aiFoundryAiServices4_1ModelDeployment.raiPolicyName + raiPolicyName: aiFoundryAiServices5_4ModelDeployment.raiPolicyName sku: { - name: aiFoundryAiServices4_1ModelDeployment.sku.name - capacity: aiFoundryAiServices4_1ModelDeployment.sku.capacity + name: aiFoundryAiServices5_4ModelDeployment.sku.name + capacity: aiFoundryAiServices5_4ModelDeployment.sku.capacity } } { @@ -913,16 +913,16 @@ module aiFoundryAiServices 'br:mcr.microsoft.com/bicep/avm/res/cognitive-service } } { - name: aiFoundryAiServices4_1ModelDeployment.deploymentName + name: aiFoundryAiServices5_4ModelDeployment.deploymentName model: { - format: aiFoundryAiServices4_1ModelDeployment.format - name: aiFoundryAiServices4_1ModelDeployment.name - version: aiFoundryAiServices4_1ModelDeployment.version + format: aiFoundryAiServices5_4ModelDeployment.format + name: aiFoundryAiServices5_4ModelDeployment.name + version: aiFoundryAiServices5_4ModelDeployment.version } - raiPolicyName: aiFoundryAiServices4_1ModelDeployment.raiPolicyName + raiPolicyName: aiFoundryAiServices5_4ModelDeployment.raiPolicyName sku: { - name: aiFoundryAiServices4_1ModelDeployment.sku.name - capacity: aiFoundryAiServices4_1ModelDeployment.sku.capacity + name: aiFoundryAiServices5_4ModelDeployment.sku.name + capacity: aiFoundryAiServices5_4ModelDeployment.sku.capacity } } { @@ -1300,6 +1300,10 @@ module containerApp 'br/public:avm/res/app/container-app:0.22.0' = { memory: '4.0Gi' } env: [ + { + name: 'PORT' + value: '8000' + } { name: 'COSMOSDB_ENDPOINT' value: 'https://${cosmosDbResourceName}.documents.azure.com:443/' @@ -1326,7 +1330,7 @@ module containerApp 'br/public:avm/res/app/container-app:0.22.0' = { } { name: 'AZURE_OPENAI_RAI_DEPLOYMENT_NAME' - value: aiFoundryAiServices4_1ModelDeployment.deploymentName + value: aiFoundryAiServices5_4ModelDeployment.deploymentName } { name: 'AZURE_OPENAI_API_VERSION' @@ -1414,7 +1418,7 @@ module containerApp 'br/public:avm/res/app/container-app:0.22.0' = { } { name: 'SUPPORTED_MODELS' - value: '["${aiFoundryAiServicesModelDeployment.deploymentName}","${aiFoundryAiServices4_1ModelDeployment.deploymentName}","${aiFoundryAiServicesReasoningModelDeployment.deploymentName}"]' + value: '["${aiFoundryAiServicesModelDeployment.deploymentName}","${aiFoundryAiServices5_4ModelDeployment.deploymentName}","${aiFoundryAiServicesReasoningModelDeployment.deploymentName}"]' } { name: 'AZURE_STORAGE_BLOB_URL' @@ -1627,7 +1631,7 @@ module webSite 'modules/web-sites.bicep' = { // Port 80 matches the hello-world placeholder image above. // The postprovision script updates this to 3000 (FRONTEND_WEBSITES_PORT) // when it swaps in the real frontend image. - WEBSITES_PORT: '3000' + WEBSITES_PORT: '80' WEBSITES_CONTAINER_START_TIME_LIMIT: '1800' // 30 minutes, adjust as needed BACKEND_API_URL: 'https://${containerApp.outputs.fqdn}' AUTH_ENABLED: 'false' @@ -1892,7 +1896,7 @@ output COSMOSDB_CONTAINER string = cosmosDbDatabaseMemoryContainerName output AZURE_OPENAI_ENDPOINT string = 'https://${aiFoundryAiServicesResourceName}.openai.azure.com/' output AZURE_OPENAI_MODEL_NAME string = aiFoundryAiServicesModelDeployment.name output AZURE_OPENAI_DEPLOYMENT_NAME string = aiFoundryAiServicesModelDeployment.deploymentName -output AZURE_OPENAI_RAI_DEPLOYMENT_NAME string = aiFoundryAiServices4_1ModelDeployment.deploymentName +output AZURE_OPENAI_RAI_DEPLOYMENT_NAME string = aiFoundryAiServices5_4ModelDeployment.deploymentName output AZURE_OPENAI_API_VERSION string = azureOpenaiAPIVersion // output APPLICATIONINSIGHTS_INSTRUMENTATION_KEY string = applicationInsights.outputs.instrumentationKey // output AZURE_AI_PROJECT_ENDPOINT string = aiFoundryAiServices.outputs.aiProjectInfo.apiEndpoint @@ -1916,7 +1920,7 @@ output AZURE_COGNITIVE_SERVICES string = 'https://cognitiveservices.azure.com/.d output REASONING_MODEL_NAME string = aiFoundryAiServicesReasoningModelDeployment.deploymentName output MCP_SERVER_NAME string = 'MacaeMcpServer' output MCP_SERVER_DESCRIPTION string = 'MCP server with greeting, HR, and planning tools' -output SUPPORTED_MODELS string = '["${aiFoundryAiServicesModelDeployment.deploymentName}","${aiFoundryAiServices4_1ModelDeployment.deploymentName}","${aiFoundryAiServicesReasoningModelDeployment.deploymentName}"]' +output SUPPORTED_MODELS string = '["${aiFoundryAiServicesModelDeployment.deploymentName}","${aiFoundryAiServices5_4ModelDeployment.deploymentName}","${aiFoundryAiServicesReasoningModelDeployment.deploymentName}"]' output BACKEND_URL string = 'https://${containerApp.outputs.fqdn}' output AZURE_AI_PROJECT_ENDPOINT string = aiFoundryAiProjectEndpoint output AZURE_AI_AGENT_ENDPOINT string = aiFoundryAiProjectEndpoint diff --git a/infra/main.json b/infra/main.json index b2bbcbed3..e60ce30ae 100644 --- a/infra/main.json +++ b/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "10743773670737934540" + "templateHash": "17459100270519587144" }, "name": "Multi-Agent Custom Automation Engine", "description": "This module contains the resources required to deploy the [Multi-Agent Custom Automation Engine solution accelerator](https://github.com/microsoft/Multi-Agent-Custom-Automation-Engine-Solution-Accelerator) for both Sandbox environments and WAF aligned environments.\n\n> **Note:** This module is not intended for broad, generic use, as it was designed by the Commercial Solution Areas CTO team, as a Microsoft Solution Accelerator. Feature requests and bug fix requests are welcome if they support the needs of this organization but may not be incorporated if they aim to make this module more generic than what it needs to be for its primary use case. This module will likely be updated to leverage AVM resource modules in the future. This may result in breaking changes in upcoming versions when these features are implemented.\n" @@ -93,7 +93,7 @@ "description": "Optional. Deployment (alias) name used in Azure OpenAI for the main GPT model. This is the value the application uses as `deployment_name` (including in data/agent_teams/*.json). Defaults to gptModelName." } }, - "gpt4_1ModelName": { + "gpt5_4ModelName": { "type": "string", "defaultValue": "gpt-5.4", "minLength": 1, @@ -101,18 +101,18 @@ "description": "Optional. Name of the underlying larger GPT model to deploy. Defaults to gpt-5.4 (2026-03-05 series)." } }, - "gpt4_1ModelVersion": { + "gpt5_4ModelVersion": { "type": "string", "defaultValue": "2026-03-05", "metadata": { "description": "Optional. Version of the larger GPT model to deploy. Defaults to 2026-03-05 (gpt-5.4 release)." } }, - "gpt4_1DeploymentName": { + "gpt5_4DeploymentName": { "type": "string", - "defaultValue": "[parameters('gpt4_1ModelName')]", + "defaultValue": "[parameters('gpt5_4ModelName')]", "metadata": { - "description": "Optional. Deployment (alias) name used in Azure OpenAI for the larger GPT model. Defaults to gpt4_1ModelName." + "description": "Optional. Deployment (alias) name used in Azure OpenAI for the larger GPT model. Defaults to gpt5_4ModelName." } }, "gptReasoningModelName": { @@ -151,7 +151,7 @@ "description": "Optional. Version of the Azure AI Agent API version. Defaults to 2025-01-01-preview." } }, - "gpt4_1ModelDeploymentType": { + "gpt5_4ModelDeploymentType": { "type": "string", "defaultValue": "GlobalStandard", "allowedValues": [ @@ -194,7 +194,7 @@ "description": "Optional. AI model deployment token capacity. Defaults to 50 for optimal performance." } }, - "gpt4_1ModelCapacity": { + "gpt5_4ModelCapacity": { "type": "int", "defaultValue": 150, "metadata": { @@ -476,14 +476,14 @@ }, "raiPolicyName": "Microsoft.Default" }, - "aiFoundryAiServices4_1ModelDeployment": { + "aiFoundryAiServices5_4ModelDeployment": { "format": "OpenAI", - "deploymentName": "[parameters('gpt4_1DeploymentName')]", - "name": "[parameters('gpt4_1ModelName')]", - "version": "[parameters('gpt4_1ModelVersion')]", + "deploymentName": "[parameters('gpt5_4DeploymentName')]", + "name": "[parameters('gpt5_4ModelName')]", + "version": "[parameters('gpt5_4ModelVersion')]", "sku": { - "name": "[parameters('gpt4_1ModelDeploymentType')]", - "capacity": "[parameters('gpt4_1ModelCapacity')]" + "name": "[parameters('gpt5_4ModelDeploymentType')]", + "capacity": "[parameters('gpt5_4ModelCapacity')]" }, "raiPolicyName": "Microsoft.Default" }, @@ -24281,16 +24281,16 @@ } }, { - "name": "[variables('aiFoundryAiServices4_1ModelDeployment').deploymentName]", + "name": "[variables('aiFoundryAiServices5_4ModelDeployment').deploymentName]", "model": { - "format": "[variables('aiFoundryAiServices4_1ModelDeployment').format]", - "name": "[variables('aiFoundryAiServices4_1ModelDeployment').name]", - "version": "[variables('aiFoundryAiServices4_1ModelDeployment').version]" + "format": "[variables('aiFoundryAiServices5_4ModelDeployment').format]", + "name": "[variables('aiFoundryAiServices5_4ModelDeployment').name]", + "version": "[variables('aiFoundryAiServices5_4ModelDeployment').version]" }, - "raiPolicyName": "[variables('aiFoundryAiServices4_1ModelDeployment').raiPolicyName]", + "raiPolicyName": "[variables('aiFoundryAiServices5_4ModelDeployment').raiPolicyName]", "sku": { - "name": "[variables('aiFoundryAiServices4_1ModelDeployment').sku.name]", - "capacity": "[variables('aiFoundryAiServices4_1ModelDeployment').sku.capacity]" + "name": "[variables('aiFoundryAiServices5_4ModelDeployment').sku.name]", + "capacity": "[variables('aiFoundryAiServices5_4ModelDeployment').sku.capacity]" } }, { @@ -24716,16 +24716,16 @@ } }, { - "name": "[variables('aiFoundryAiServices4_1ModelDeployment').deploymentName]", + "name": "[variables('aiFoundryAiServices5_4ModelDeployment').deploymentName]", "model": { - "format": "[variables('aiFoundryAiServices4_1ModelDeployment').format]", - "name": "[variables('aiFoundryAiServices4_1ModelDeployment').name]", - "version": "[variables('aiFoundryAiServices4_1ModelDeployment').version]" + "format": "[variables('aiFoundryAiServices5_4ModelDeployment').format]", + "name": "[variables('aiFoundryAiServices5_4ModelDeployment').name]", + "version": "[variables('aiFoundryAiServices5_4ModelDeployment').version]" }, - "raiPolicyName": "[variables('aiFoundryAiServices4_1ModelDeployment').raiPolicyName]", + "raiPolicyName": "[variables('aiFoundryAiServices5_4ModelDeployment').raiPolicyName]", "sku": { - "name": "[variables('aiFoundryAiServices4_1ModelDeployment').sku.name]", - "capacity": "[variables('aiFoundryAiServices4_1ModelDeployment').sku.capacity]" + "name": "[variables('aiFoundryAiServices5_4ModelDeployment').sku.name]", + "capacity": "[variables('aiFoundryAiServices5_4ModelDeployment').sku.capacity]" } }, { @@ -28000,9 +28000,9 @@ }, "dependsOn": [ "aiFoundryAiServices", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').aiServices)]", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", "virtualNetwork" ] }, @@ -42384,6 +42384,10 @@ "memory": "4.0Gi" }, "env": [ + { + "name": "PORT", + "value": "8000" + }, { "name": "COSMOSDB_ENDPOINT", "value": "[format('https://{0}.documents.azure.com:443/', variables('cosmosDbResourceName'))]" @@ -42410,7 +42414,7 @@ }, { "name": "AZURE_OPENAI_RAI_DEPLOYMENT_NAME", - "value": "[variables('aiFoundryAiServices4_1ModelDeployment').deploymentName]" + "value": "[variables('aiFoundryAiServices5_4ModelDeployment').deploymentName]" }, { "name": "AZURE_OPENAI_API_VERSION", @@ -42494,7 +42498,7 @@ }, { "name": "SUPPORTED_MODELS", - "value": "[format('[\"{0}\",\"{1}\",\"{2}\"]', variables('aiFoundryAiServicesModelDeployment').deploymentName, variables('aiFoundryAiServices4_1ModelDeployment').deploymentName, variables('aiFoundryAiServicesReasoningModelDeployment').deploymentName)]" + "value": "[format('[\"{0}\",\"{1}\",\"{2}\"]', variables('aiFoundryAiServicesModelDeployment').deploymentName, variables('aiFoundryAiServices5_4ModelDeployment').deploymentName, variables('aiFoundryAiServicesReasoningModelDeployment').deploymentName)]" }, { "name": "AZURE_STORAGE_BLOB_URL", @@ -46449,7 +46453,7 @@ "properties": { "SCM_DO_BUILD_DURING_DEPLOYMENT": "true", "DOCKER_REGISTRY_SERVER_URL": "[format('https://{0}', reference('containerRegistry').outputs.loginServer.value)]", - "WEBSITES_PORT": "3000", + "WEBSITES_PORT": "80", "WEBSITES_CONTAINER_START_TIME_LIMIT": "1800", "BACKEND_API_URL": "[format('https://{0}', reference('containerApp').outputs.fqdn.value)]", "AUTH_ENABLED": "false", @@ -58886,7 +58890,7 @@ }, "AZURE_OPENAI_RAI_DEPLOYMENT_NAME": { "type": "string", - "value": "[variables('aiFoundryAiServices4_1ModelDeployment').deploymentName]" + "value": "[variables('aiFoundryAiServices5_4ModelDeployment').deploymentName]" }, "AZURE_OPENAI_API_VERSION": { "type": "string", @@ -58958,7 +58962,7 @@ }, "SUPPORTED_MODELS": { "type": "string", - "value": "[format('[\"{0}\",\"{1}\",\"{2}\"]', variables('aiFoundryAiServicesModelDeployment').deploymentName, variables('aiFoundryAiServices4_1ModelDeployment').deploymentName, variables('aiFoundryAiServicesReasoningModelDeployment').deploymentName)]" + "value": "[format('[\"{0}\",\"{1}\",\"{2}\"]', variables('aiFoundryAiServicesModelDeployment').deploymentName, variables('aiFoundryAiServices5_4ModelDeployment').deploymentName, variables('aiFoundryAiServicesReasoningModelDeployment').deploymentName)]" }, "BACKEND_URL": { "type": "string", diff --git a/infra/main.parameters.json b/infra/main.parameters.json index 53c5f72b3..d8d4c43ef 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -26,20 +26,20 @@ "gptDeploymentCapacity": { "value": "${AZURE_ENV_GPT_MODEL_CAPACITY}" }, - "gpt4_1ModelDeploymentType": { - "value": "${AZURE_ENV_MODEL_4_1_DEPLOYMENT_TYPE}" + "gpt5_4ModelDeploymentType": { + "value": "${AZURE_ENV_MODEL_5_4_DEPLOYMENT_TYPE}" }, - "gpt4_1ModelName": { - "value": "${AZURE_ENV_MODEL_4_1_NAME}" + "gpt5_4ModelName": { + "value": "${AZURE_ENV_MODEL_5_4_NAME}" }, - "gpt4_1ModelVersion": { - "value": "${AZURE_ENV_MODEL_4_1_VERSION}" + "gpt5_4ModelVersion": { + "value": "${AZURE_ENV_MODEL_5_4_VERSION}" }, - "gpt4_1DeploymentName": { - "value": "${AZURE_ENV_MODEL_4_1_DEPLOYMENT_NAME}" + "gpt5_4DeploymentName": { + "value": "${AZURE_ENV_MODEL_5_4_DEPLOYMENT_NAME}" }, - "gpt4_1ModelCapacity": { - "value": "${AZURE_ENV_MODEL_4_1_CAPACITY}" + "gpt5_4ModelCapacity": { + "value": "${AZURE_ENV_MODEL_5_4_CAPACITY}" }, "gptReasoningModelDeploymentType": { "value": "${AZURE_ENV_REASONING_MODEL_DEPLOYMENT_TYPE}" diff --git a/infra/main.waf.parameters.json b/infra/main.waf.parameters.json index 04d377647..9175b4888 100644 --- a/infra/main.waf.parameters.json +++ b/infra/main.waf.parameters.json @@ -26,20 +26,20 @@ "gptDeploymentCapacity": { "value": "${AZURE_ENV_GPT_MODEL_CAPACITY}" }, - "gpt4_1ModelDeploymentType": { - "value": "${AZURE_ENV_MODEL_4_1_DEPLOYMENT_TYPE}" + "gpt5_4ModelDeploymentType": { + "value": "${AZURE_ENV_MODEL_5_4_DEPLOYMENT_TYPE}" }, - "gpt4_1ModelName": { - "value": "${AZURE_ENV_MODEL_4_1_NAME}" + "gpt5_4ModelName": { + "value": "${AZURE_ENV_MODEL_5_4_NAME}" }, - "gpt4_1ModelVersion": { - "value": "${AZURE_ENV_MODEL_4_1_VERSION}" + "gpt5_4ModelVersion": { + "value": "${AZURE_ENV_MODEL_5_4_VERSION}" }, - "gpt4_1DeploymentName": { - "value": "${AZURE_ENV_MODEL_4_1_DEPLOYMENT_NAME}" + "gpt5_4DeploymentName": { + "value": "${AZURE_ENV_MODEL_5_4_DEPLOYMENT_NAME}" }, - "gpt4_1ModelCapacity": { - "value": "${AZURE_ENV_MODEL_4_1_CAPACITY}" + "gpt5_4ModelCapacity": { + "value": "${AZURE_ENV_MODEL_5_4_CAPACITY}" }, "gptReasoningModelDeploymentType": { "value": "${AZURE_ENV_REASONING_MODEL_DEPLOYMENT_TYPE}" diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index 223e80e02..4ec9aab3e 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -57,13 +57,13 @@ param gptDeploymentName string = gptModelName @minLength(1) @description('Optional. Name of the underlying larger GPT model to deploy. Defaults to gpt-5.4 (2026-03-05 series).') -param gpt4_1ModelName string = 'gpt-5.4' +param gpt5_4ModelName string = 'gpt-5.4' @description('Optional. Version of the larger GPT model to deploy. Defaults to 2026-03-05 (gpt-5.4 release).') -param gpt4_1ModelVersion string = '2026-03-05' +param gpt5_4ModelVersion string = '2026-03-05' -@description('Optional. Deployment (alias) name used in Azure OpenAI for the larger GPT model. Defaults to gpt4_1ModelName.') -param gpt4_1DeploymentName string = gpt4_1ModelName +@description('Optional. Deployment (alias) name used in Azure OpenAI for the larger GPT model. Defaults to gpt5_4ModelName.') +param gpt5_4DeploymentName string = gpt5_4ModelName @minLength(1) @description('Optional. Name of the underlying GPT Reasoning model to deploy. Defaults to gpt-5.4-mini (reasoning-capable, 2026-03-17 series).') @@ -89,7 +89,7 @@ param azureAiAgentAPIVersion string = '2025-01-01-preview' 'GlobalStandard' ]) @description('Optional. GPT model deployment type. Defaults to GlobalStandard.') -param gpt4_1ModelDeploymentType string = 'GlobalStandard' +param gpt5_4ModelDeploymentType string = 'GlobalStandard' @minLength(1) @allowed([ @@ -111,7 +111,7 @@ param gptReasoningModelDeploymentType string = 'GlobalStandard' param gptDeploymentCapacity int = 50 @description('Optional. AI model deployment token capacity. Defaults to 150 for optimal performance.') -param gpt4_1ModelCapacity int = 150 +param gpt5_4ModelCapacity int = 150 @description('Optional. AI model deployment token capacity. Defaults to 50 for optimal performance.') param gptReasoningModelCapacity int = 50 @@ -788,14 +788,14 @@ var aiFoundryAiServicesModelDeployment = { } raiPolicyName: 'Microsoft.Default' } -var aiFoundryAiServices4_1ModelDeployment = { +var aiFoundryAiServices5_4ModelDeployment = { format: 'OpenAI' - deploymentName: gpt4_1DeploymentName - name: gpt4_1ModelName - version: gpt4_1ModelVersion + deploymentName: gpt5_4DeploymentName + name: gpt5_4ModelName + version: gpt5_4ModelVersion sku: { - name: gpt4_1ModelDeploymentType - capacity: gpt4_1ModelCapacity + name: gpt5_4ModelDeploymentType + capacity: gpt5_4ModelCapacity } raiPolicyName: 'Microsoft.Default' } @@ -837,16 +837,16 @@ module existingAiFoundryAiServicesDeployments 'modules/ai-services-deployments.b } } { - name: aiFoundryAiServices4_1ModelDeployment.deploymentName + name: aiFoundryAiServices5_4ModelDeployment.deploymentName model: { - format: aiFoundryAiServices4_1ModelDeployment.format - name: aiFoundryAiServices4_1ModelDeployment.name - version: aiFoundryAiServices4_1ModelDeployment.version + format: aiFoundryAiServices5_4ModelDeployment.format + name: aiFoundryAiServices5_4ModelDeployment.name + version: aiFoundryAiServices5_4ModelDeployment.version } - raiPolicyName: aiFoundryAiServices4_1ModelDeployment.raiPolicyName + raiPolicyName: aiFoundryAiServices5_4ModelDeployment.raiPolicyName sku: { - name: aiFoundryAiServices4_1ModelDeployment.sku.name - capacity: aiFoundryAiServices4_1ModelDeployment.sku.capacity + name: aiFoundryAiServices5_4ModelDeployment.sku.name + capacity: aiFoundryAiServices5_4ModelDeployment.sku.capacity } } { @@ -912,16 +912,16 @@ module aiFoundryAiServices 'br:mcr.microsoft.com/bicep/avm/res/cognitive-service } } { - name: aiFoundryAiServices4_1ModelDeployment.deploymentName + name: aiFoundryAiServices5_4ModelDeployment.deploymentName model: { - format: aiFoundryAiServices4_1ModelDeployment.format - name: aiFoundryAiServices4_1ModelDeployment.name - version: aiFoundryAiServices4_1ModelDeployment.version + format: aiFoundryAiServices5_4ModelDeployment.format + name: aiFoundryAiServices5_4ModelDeployment.name + version: aiFoundryAiServices5_4ModelDeployment.version } - raiPolicyName: aiFoundryAiServices4_1ModelDeployment.raiPolicyName + raiPolicyName: aiFoundryAiServices5_4ModelDeployment.raiPolicyName sku: { - name: aiFoundryAiServices4_1ModelDeployment.sku.name - capacity: aiFoundryAiServices4_1ModelDeployment.sku.capacity + name: aiFoundryAiServices5_4ModelDeployment.sku.name + capacity: aiFoundryAiServices5_4ModelDeployment.sku.capacity } } { @@ -1324,7 +1324,7 @@ module containerApp 'br/public:avm/res/app/container-app:0.22.0' = { } { name: 'AZURE_OPENAI_RAI_DEPLOYMENT_NAME' - value: aiFoundryAiServices4_1ModelDeployment.deploymentName + value: aiFoundryAiServices5_4ModelDeployment.deploymentName } { name: 'AZURE_OPENAI_API_VERSION' @@ -1412,7 +1412,7 @@ module containerApp 'br/public:avm/res/app/container-app:0.22.0' = { } { name: 'SUPPORTED_MODELS' - value: '["${aiFoundryAiServicesModelDeployment.deploymentName}","${aiFoundryAiServices4_1ModelDeployment.deploymentName}","${aiFoundryAiServicesReasoningModelDeployment.deploymentName}"]' + value: '["${aiFoundryAiServicesModelDeployment.deploymentName}","${aiFoundryAiServices5_4ModelDeployment.deploymentName}","${aiFoundryAiServicesReasoningModelDeployment.deploymentName}"]' } { name: 'AZURE_STORAGE_BLOB_URL' @@ -1884,7 +1884,7 @@ output COSMOSDB_CONTAINER string = cosmosDbDatabaseMemoryContainerName output AZURE_OPENAI_ENDPOINT string = 'https://${aiFoundryAiServicesResourceName}.openai.azure.com/' output AZURE_OPENAI_MODEL_NAME string = aiFoundryAiServicesModelDeployment.name output AZURE_OPENAI_DEPLOYMENT_NAME string = aiFoundryAiServicesModelDeployment.deploymentName -output AZURE_OPENAI_RAI_DEPLOYMENT_NAME string = aiFoundryAiServices4_1ModelDeployment.deploymentName +output AZURE_OPENAI_RAI_DEPLOYMENT_NAME string = aiFoundryAiServices5_4ModelDeployment.deploymentName output AZURE_OPENAI_API_VERSION string = azureOpenaiAPIVersion // output APPLICATIONINSIGHTS_INSTRUMENTATION_KEY string = applicationInsights.outputs.instrumentationKey // output AZURE_AI_PROJECT_ENDPOINT string = aiFoundryAiServices.outputs.aiProjectInfo.apiEndpoint @@ -1908,7 +1908,7 @@ output AZURE_COGNITIVE_SERVICES string = 'https://cognitiveservices.azure.com/.d output REASONING_MODEL_NAME string = aiFoundryAiServicesReasoningModelDeployment.deploymentName output MCP_SERVER_NAME string = 'MacaeMcpServer' output MCP_SERVER_DESCRIPTION string = 'MCP server with greeting, HR, and planning tools' -output SUPPORTED_MODELS string = '["${aiFoundryAiServicesModelDeployment.deploymentName}","${aiFoundryAiServices4_1ModelDeployment.deploymentName}","${aiFoundryAiServicesReasoningModelDeployment.deploymentName}"]' +output SUPPORTED_MODELS string = '["${aiFoundryAiServicesModelDeployment.deploymentName}","${aiFoundryAiServices5_4ModelDeployment.deploymentName}","${aiFoundryAiServicesReasoningModelDeployment.deploymentName}"]' output BACKEND_URL string = 'https://${containerApp.outputs.fqdn}' output AZURE_AI_PROJECT_ENDPOINT string = aiFoundryAiProjectEndpoint output AZURE_AI_AGENT_ENDPOINT string = aiFoundryAiProjectEndpoint diff --git a/src/backend/.env.sample b/src/backend/.env.sample index 58b66f522..0f176c65d 100644 --- a/src/backend/.env.sample +++ b/src/backend/.env.sample @@ -13,14 +13,13 @@ AZURE_AI_RESOURCE_GROUP= AZURE_AI_PROJECT_NAME= AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-5.4-mini APPLICATIONINSIGHTS_CONNECTION_STRING= -AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME=gpt-4o +AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME=gpt-5.4-mini AZURE_OPENAI_RAI_DEPLOYMENT_NAME= -AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME=gpt-4.1-mini AZURE_COGNITIVE_SERVICES="https://cognitiveservices.azure.com/.default" AZURE_AI_PROJECT_ENDPOINT= AZURE_AI_AGENT_ENDPOINT= # AZURE_BING_CONNECTION_NAME= -REASONING_MODEL_NAME="o4-mini" +REASONING_MODEL_NAME="gpt-5.4-mini-reasoning" APP_ENV=dev MCP_SERVER_ENDPOINT=http://localhost:9000/mcp MCP_SERVER_NAME=MacaeMcpServer @@ -29,7 +28,7 @@ AZURE_TENANT_ID= AZURE_CLIENT_ID= BACKEND_API_URL=http://localhost:8000 FRONTEND_SITE_NAME=* -SUPPORTED_MODELS='["o3","o4-mini","gpt-4.1","gpt-4.1-mini"]' +SUPPORTED_MODELS='["gpt-5.4-mini","gpt-5.4","gpt-5.4-mini-reasoning"]' AZURE_AI_SEARCH_CONNECTION_NAME= AZURE_AI_SEARCH_ENDPOINT= BING_CONNECTION_NAME= From 584bcbca23d1eff26da233d062be8fc15b888368 Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Mon, 6 Jul 2026 13:53:08 +0530 Subject: [PATCH 08/12] update environment variable names for GPT models in workflows and scripts --- .github/workflows/deploy-waf.yml | 18 +++++++++--------- .github/workflows/deploy.yml | 12 ++++++------ .github/workflows/job-deploy.yml | 12 ++++++------ .github/workflows/job-send-notification.yml | 2 +- docs/ACRBuildAndPushGuide.md | 2 +- docs/DeploymentGuide.md | 4 +++- infra/main.bicep | 2 +- infra/scripts/checkquota.sh | 12 ++++++------ 8 files changed, 33 insertions(+), 31 deletions(-) diff --git a/.github/workflows/deploy-waf.yml b/.github/workflows/deploy-waf.yml index b8595ae07..1ce1aa830 100644 --- a/.github/workflows/deploy-waf.yml +++ b/.github/workflows/deploy-waf.yml @@ -22,9 +22,9 @@ jobs: runs-on: ubuntu-latest environment: production env: - GPT_MIN_CAPACITY: 1 - O4_MINI_MIN_CAPACITY: 1 - GPT41_MINI_MIN_CAPACITY: 1 + GPT_5_4_MINI_MIN_CAPACITY: 1 + GPT_REASONING_MIN_CAPACITY: 1 + GPT_5_4_MIN_CAPACITY: 1 steps: - name: Checkout Code uses: actions/checkout@v6 @@ -40,9 +40,9 @@ jobs: id: quota-check env: AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - GPT_MIN_CAPACITY: ${{ env.GPT_MIN_CAPACITY }} - O4_MINI_MIN_CAPACITY: ${{ env.O4_MINI_MIN_CAPACITY }} - GPT41_MINI_MIN_CAPACITY: ${{ env.GPT41_MINI_MIN_CAPACITY }} + GPT_5_4_MINI_MIN_CAPACITY: ${{ env.GPT_5_4_MINI_MIN_CAPACITY }} + GPT_REASONING_MIN_CAPACITY: ${{ env.GPT_REASONING_MIN_CAPACITY }} + GPT_5_4_MIN_CAPACITY: ${{ env.GPT_5_4_MIN_CAPACITY }} AZURE_REGIONS: ${{ vars.AZURE_REGIONS }} run: | chmod +x infra/scripts/checkquota.sh @@ -133,9 +133,9 @@ jobs: solutionName=${{ env.SOLUTION_PREFIX }} \ location="${{ env.AZURE_LOCATION }}" \ azureAiServiceLocation='${{ env.AZURE_LOCATION }}' \ - gptDeploymentCapacity=${{ env.GPT_MIN_CAPACITY }} \ - gpt5_4ModelCapacity=${{ env.GPT41_MINI_MIN_CAPACITY }} \ - gptReasoningModelCapacity=${{ env.O4_MINI_MIN_CAPACITY }} \ + gptDeploymentCapacity=${{ env.GPT_5_4_MINI_MIN_CAPACITY }} \ + gpt5_4ModelCapacity=${{ env.GPT_5_4_MIN_CAPACITY }} \ + gptReasoningModelCapacity=${{ env.GPT_REASONING_MIN_CAPACITY }} \ enableTelemetry=true \ enableMonitoring=true \ enablePrivateNetworking=true \ diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1e0bd7a2a..45da3a5d3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -17,9 +17,9 @@ on: - cron: "0 11,23 * * *" # Runs at 11:00 AM and 11:00 PM GMT workflow_dispatch: #Allow manual triggering env: - GPT_MIN_CAPACITY: 150 - O4_MINI_MIN_CAPACITY: 50 - GPT41_MINI_MIN_CAPACITY: 50 + GPT_5_4_MINI_MIN_CAPACITY: 50 + GPT_REASONING_MIN_CAPACITY: 50 + GPT_5_4_MIN_CAPACITY: 150 BRANCH_NAME: ${{ github.head_ref || github.ref_name }} jobs: @@ -47,9 +47,9 @@ jobs: id: quota-check env: AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - GPT_MIN_CAPACITY: ${{ env.GPT_MIN_CAPACITY }} - O4_MINI_MIN_CAPACITY: ${{ env.O4_MINI_MIN_CAPACITY }} - GPT41_MINI_MIN_CAPACITY: ${{ env.GPT41_MINI_MIN_CAPACITY }} + GPT_5_4_MINI_MIN_CAPACITY: ${{ env.GPT_5_4_MINI_MIN_CAPACITY }} + GPT_REASONING_MIN_CAPACITY: ${{ env.GPT_REASONING_MIN_CAPACITY }} + GPT_5_4_MIN_CAPACITY: ${{ env.GPT_5_4_MIN_CAPACITY }} AZURE_REGIONS: ${{ vars.AZURE_REGIONS }} run: | chmod +x infra/scripts/checkquota.sh diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index a5af4831a..bc774b1f1 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -98,9 +98,9 @@ on: value: ${{ jobs.azure-setup.outputs.QUOTA_FAILED }} env: - GPT_MIN_CAPACITY: 150 - O4_MINI_MIN_CAPACITY: 50 - GPT41_MINI_MIN_CAPACITY: 50 + GPT_5_4_MINI_MIN_CAPACITY: 50 + GPT_REASONING_MIN_CAPACITY: 50 + GPT_5_4_MIN_CAPACITY: 150 BRANCH_NAME: ${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }} WAF_ENABLED: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.waf_enabled || false) || false }} EXP: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.EXP || false) || false }} @@ -306,9 +306,9 @@ jobs: id: quota-check env: AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - GPT_MIN_CAPACITY: ${{ env.GPT_MIN_CAPACITY }} - O4_MINI_MIN_CAPACITY: ${{ env.O4_MINI_MIN_CAPACITY }} - GPT41_MINI_MIN_CAPACITY: ${{ env.GPT41_MINI_MIN_CAPACITY }} + GPT_5_4_MINI_MIN_CAPACITY: ${{ env.GPT_5_4_MINI_MIN_CAPACITY }} + GPT_REASONING_MIN_CAPACITY: ${{ env.GPT_REASONING_MIN_CAPACITY }} + GPT_5_4_MIN_CAPACITY: ${{ env.GPT_5_4_MIN_CAPACITY }} AZURE_REGIONS: ${{ vars.AZURE_REGIONS }} run: | chmod +x infra/scripts/checkquota.sh diff --git a/.github/workflows/job-send-notification.yml b/.github/workflows/job-send-notification.yml index 2bef97813..5ff3244ac 100644 --- a/.github/workflows/job-send-notification.yml +++ b/.github/workflows/job-send-notification.yml @@ -68,7 +68,7 @@ on: type: string env: - GPT_MIN_CAPACITY: 100 + GPT_5_4_MINI_MIN_CAPACITY: 100 BRANCH_NAME: ${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }} WAF_ENABLED: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.waf_enabled || false) || false }} EXP: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.EXP || false) || false }} diff --git a/docs/ACRBuildAndPushGuide.md b/docs/ACRBuildAndPushGuide.md index 127e77b7a..9d9005937 100644 --- a/docs/ACRBuildAndPushGuide.md +++ b/docs/ACRBuildAndPushGuide.md @@ -2,7 +2,7 @@ This guide provides step-by-step instructions to build and push Docker images for **WebApp** and **Backend** services into Azure Container Registry (ACR). -> **💡 Tip:** For a standard `azd up` deployment you do **not** need to run these commands manually. The post-deployment scripts `infra/scripts/build_and_push_images.sh` (Bash) and `infra/scripts/Build-And-Push-Images.ps1` (PowerShell) build and push the backend, frontend, and mcp_server images to ACR and repoint the apps automatically. Use the manual steps below only for custom or one-off image builds. +> **⚠️ Important:** `azd up` does **not** build or push your application images automatically. The backend and mcp_server Container Apps start with a temporary `hello-world` placeholder image and remain non-functional until you push the real images. When `azd up` finishes, the `postdeploy` hook (defined in `azure.yaml`) **prints** the commands to run — it does not execute them for you. The wrapper scripts `infra/scripts/build_and_push_images.sh` (Bash) and `infra/scripts/Build-And-Push-Images.ps1` (PowerShell) build and push the backend, frontend, and mcp_server images to ACR and repoint the apps. Run one of them after `azd up`. Use the manual steps below only for custom or one-off image builds. ## 📋 Prerequisites Before starting, ensure you have: diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md index 00410ce03..ff913301a 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -323,7 +323,7 @@ azd up **Expected Duration:** 9-10 minutes for default configuration -- **Upon successful completion**, you will see a success message indicating that all resources have been deployed, along with the application URL and next steps for Build and push the backend/frontend/mcp_server container images to ACR, then point the container app and webapp at them and uploading team configurations and sample data. +- **Upon successful completion**, you will see a success message indicating that all resources have been deployed, along with the application URL. The next steps are to build and push the backend, frontend, and mcp_server container images to ACR, point the container app and web app at them, and upload the team configurations and sample data. ![Deployment Success message](./images/Deployment_success_message.png) @@ -345,6 +345,8 @@ When `azd up` finishes, the `postdeploy` hook (defined in `azure.yaml`) prints t ### 5.1 Build and Push Container Images +> **⚠️ Required step:** `azd up` provisions the infrastructure but deploys a temporary `hello-world` placeholder image to the backend and mcp_server Container Apps. Until you complete this step, those services are non-functional and the application will not work. The `postdeploy` hook only prints these commands — you must run them yourself. + Build and push the backend, frontend, and mcp_server container images to ACR, then point the Container App and Web App at them. Run the command for your shell: - **For Bash (Linux/macOS/WSL):** diff --git a/infra/main.bicep b/infra/main.bicep index bde78636a..3ad03357f 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -1,4 +1,4 @@ -// // ========== main_custom.bicep ========== // +// // ========== main.bicep ========== // targetScope = 'resourceGroup' metadata name = 'Multi-Agent Custom Automation Engine' diff --git a/infra/scripts/checkquota.sh b/infra/scripts/checkquota.sh index 9150953e2..02349b91c 100644 --- a/infra/scripts/checkquota.sh +++ b/infra/scripts/checkquota.sh @@ -4,15 +4,15 @@ IFS=', ' read -ra REGIONS <<< "$AZURE_REGIONS" SUBSCRIPTION_ID="${AZURE_SUBSCRIPTION_ID}" -GPT_MIN_CAPACITY="${GPT_MIN_CAPACITY}" -O4_MINI_MIN_CAPACITY="${O4_MINI_MIN_CAPACITY}" -GPT41_MINI_MIN_CAPACITY="${GPT41_MINI_MIN_CAPACITY}" +GPT_5_4_MINI_MIN_CAPACITY="${GPT_5_4_MINI_MIN_CAPACITY}" +GPT_REASONING_MIN_CAPACITY="${GPT_REASONING_MIN_CAPACITY}" +GPT_5_4_MIN_CAPACITY="${GPT_5_4_MIN_CAPACITY}" echo "🔄 Validating required environment variables..." if [[ -z "$SUBSCRIPTION_ID" || -z "$REGIONS" ]]; then echo "❌ ERROR: Missing required environment variables." echo "Required: AZURE_SUBSCRIPTION_ID, AZURE_REGIONS" - echo "Optional: O4_MINI_MIN_CAPACITY (default: 50), GPT41_MINI_MIN_CAPACITY (default: 50)" + echo "Optional: GPT_5_4_MINI_MIN_CAPACITY (default: 50), GPT_REASONING_MIN_CAPACITY (default: 50), GPT_5_4_MIN_CAPACITY (default: 150)" exit 1 fi @@ -25,8 +25,8 @@ echo "✅ Azure subscription set successfully." # Define models and their minimum required capacities declare -A MIN_CAPACITY=( - ["OpenAI.GlobalStandard.gpt-5.4-mini"]="$(( ${GPT_MIN_CAPACITY:-0} + ${O4_MINI_MIN_CAPACITY:-0} ))" - ["OpenAI.GlobalStandard.gpt-5.4"]="${GPT41_MINI_MIN_CAPACITY}" + ["OpenAI.GlobalStandard.gpt-5.4-mini"]="$(( ${GPT_5_4_MINI_MIN_CAPACITY:-0} + ${GPT_REASONING_MIN_CAPACITY:-0} ))" + ["OpenAI.GlobalStandard.gpt-5.4"]="${GPT_5_4_MIN_CAPACITY}" ) VALID_REGION="" From 212f7c5b82a85ce47620ad7aea534094d0b00756 Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Mon, 6 Jul 2026 15:37:49 +0530 Subject: [PATCH 09/12] add instructions for uploading team configurations and indexing sample data after image build and push --- infra/scripts/Build-And-Push-Images.ps1 | 7 +++++++ infra/scripts/build_and_push_images.sh | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/infra/scripts/Build-And-Push-Images.ps1 b/infra/scripts/Build-And-Push-Images.ps1 index 1b08ed2a1..84705a6d8 100644 --- a/infra/scripts/Build-And-Push-Images.ps1 +++ b/infra/scripts/Build-And-Push-Images.ps1 @@ -234,3 +234,10 @@ if ($LASTEXITCODE -ne 0) { throw "Failed to restart Web App '$frontendApp'." } Write-Section 'Image build & push complete' Write-Host "All images built, pushed to '$acrEndpoint' with tag '$ImageTag', and services updated." -ForegroundColor Green + +Write-Section 'Next step: Upload Team Configurations and index sample data' +Write-Host "Run the following command from the project root to upload the team" -ForegroundColor White +Write-Host "configurations and index the sample data:" -ForegroundColor White +Write-Host "" +Write-Host " infra\scripts\Selecting-Team-Config-And-Data.ps1" -ForegroundColor Cyan +Write-Host "" diff --git a/infra/scripts/build_and_push_images.sh b/infra/scripts/build_and_push_images.sh index 58ade859f..1ef2e90e5 100644 --- a/infra/scripts/build_and_push_images.sh +++ b/infra/scripts/build_and_push_images.sh @@ -166,3 +166,10 @@ az webapp restart --name "${FRONTEND_APP}" --resource-group "${RESOURCE_GROUP}" section "Image build & push complete" echo "All images built, pushed to '${ACR_ENDPOINT}' with tag '${IMAGE_TAG}', and services updated." + +section "Next step: Upload Team Configurations and index sample data" +echo "Run the following command from the project root to upload the team" +echo "configurations and index the sample data:" +echo "" +echo " bash infra/scripts/selecting_team_config_and_data.sh" +echo "" From 01bc725ac0b88dd3726e5f13b88def2d1ba9c2c9 Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Tue, 7 Jul 2026 17:51:30 +0530 Subject: [PATCH 10/12] - Removed reasoning model parameters and related logic from Bicep and JSON templates. - Updated deployment capacity defaults for GPT models. - Added private endpoint configurations for Azure Container Registry based on networking settings. - Adjusted scripts for building and pushing images to temporarily relax ACR restrictions during WAF deployments. - Enhanced WebSocket handling in the frontend to coalesce streaming messages for improved UI performance. - Updated backend configuration to reflect changes in reasoning model naming and supported models. - Improved agent handling logic in the orchestration layer to prevent infinite loops during approval processes. --- .github/workflows/deploy-waf.yml | 3 - .github/workflows/deploy.yml | 6 +- .github/workflows/job-deploy.yml | 4 +- data/agent_teams/retail.json | 2 +- docs/CustomizingAzdParameters.md | 7 +- docs/DeploymentGuide.md | 4 +- infra/main.bicep | 93 ++++---------- infra/main.json | 115 +++--------------- infra/main.parameters.json | 15 --- infra/main.waf.parameters.json | 15 --- infra/main_custom.bicep | 70 +---------- infra/scripts/Build-And-Push-Images.ps1 | 25 ++++ infra/scripts/build_and_push_images.sh | 23 ++++ infra/scripts/checkquota.sh | 5 +- src/App/src/hooks/usePlanWebSocket.tsx | 54 +++++++- src/backend/.env.sample | 4 +- src/backend/common/config/app_config.py | 2 +- .../orchestration/human_approval_manager.py | 70 +++++++---- 18 files changed, 210 insertions(+), 307 deletions(-) diff --git a/.github/workflows/deploy-waf.yml b/.github/workflows/deploy-waf.yml index 1ce1aa830..7513a070d 100644 --- a/.github/workflows/deploy-waf.yml +++ b/.github/workflows/deploy-waf.yml @@ -23,7 +23,6 @@ jobs: environment: production env: GPT_5_4_MINI_MIN_CAPACITY: 1 - GPT_REASONING_MIN_CAPACITY: 1 GPT_5_4_MIN_CAPACITY: 1 steps: - name: Checkout Code @@ -41,7 +40,6 @@ jobs: env: AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} GPT_5_4_MINI_MIN_CAPACITY: ${{ env.GPT_5_4_MINI_MIN_CAPACITY }} - GPT_REASONING_MIN_CAPACITY: ${{ env.GPT_REASONING_MIN_CAPACITY }} GPT_5_4_MIN_CAPACITY: ${{ env.GPT_5_4_MIN_CAPACITY }} AZURE_REGIONS: ${{ vars.AZURE_REGIONS }} run: | @@ -135,7 +133,6 @@ jobs: azureAiServiceLocation='${{ env.AZURE_LOCATION }}' \ gptDeploymentCapacity=${{ env.GPT_5_4_MINI_MIN_CAPACITY }} \ gpt5_4ModelCapacity=${{ env.GPT_5_4_MIN_CAPACITY }} \ - gptReasoningModelCapacity=${{ env.GPT_REASONING_MIN_CAPACITY }} \ enableTelemetry=true \ enableMonitoring=true \ enablePrivateNetworking=true \ diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 45da3a5d3..18712e625 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -17,8 +17,7 @@ on: - cron: "0 11,23 * * *" # Runs at 11:00 AM and 11:00 PM GMT workflow_dispatch: #Allow manual triggering env: - GPT_5_4_MINI_MIN_CAPACITY: 50 - GPT_REASONING_MIN_CAPACITY: 50 + GPT_5_4_MINI_MIN_CAPACITY: 100 GPT_5_4_MIN_CAPACITY: 150 BRANCH_NAME: ${{ github.head_ref || github.ref_name }} @@ -48,7 +47,6 @@ jobs: env: AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} GPT_5_4_MINI_MIN_CAPACITY: ${{ env.GPT_5_4_MINI_MIN_CAPACITY }} - GPT_REASONING_MIN_CAPACITY: ${{ env.GPT_REASONING_MIN_CAPACITY }} GPT_5_4_MIN_CAPACITY: ${{ env.GPT_5_4_MIN_CAPACITY }} AZURE_REGIONS: ${{ vars.AZURE_REGIONS }} run: | @@ -153,7 +151,7 @@ jobs: backendContainerImageTag="${IMAGE_TAG}" \ frontendContainerImageTag="${IMAGE_TAG}" \ azureAiServiceLocation='${{ env.AZURE_LOCATION }}' \ - gptDeploymentCapacity=50 \ + gptDeploymentCapacity=100 \ createdBy="Pipeline" \ tags="{'Purpose':'Deploying and Cleaning Up Resources for Validation','CreatedDate':'$current_date'}" \ --output json diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index bc774b1f1..752ee24ab 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -98,8 +98,7 @@ on: value: ${{ jobs.azure-setup.outputs.QUOTA_FAILED }} env: - GPT_5_4_MINI_MIN_CAPACITY: 50 - GPT_REASONING_MIN_CAPACITY: 50 + GPT_5_4_MINI_MIN_CAPACITY: 100 GPT_5_4_MIN_CAPACITY: 150 BRANCH_NAME: ${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }} WAF_ENABLED: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.waf_enabled || false) || false }} @@ -307,7 +306,6 @@ jobs: env: AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} GPT_5_4_MINI_MIN_CAPACITY: ${{ env.GPT_5_4_MINI_MIN_CAPACITY }} - GPT_REASONING_MIN_CAPACITY: ${{ env.GPT_REASONING_MIN_CAPACITY }} GPT_5_4_MIN_CAPACITY: ${{ env.GPT_5_4_MIN_CAPACITY }} AZURE_REGIONS: ${{ vars.AZURE_REGIONS }} run: | diff --git a/data/agent_teams/retail.json b/data/agent_teams/retail.json index 8b66f3370..d4910994a 100644 --- a/data/agent_teams/retail.json +++ b/data/agent_teams/retail.json @@ -43,7 +43,7 @@ "input_key": "", "type": "", "name": "AnalysisRecommendationAgent", - "deployment_name": "gpt-5.4-mini-reasoning", + "deployment_name": "gpt-5.4-mini", "icon": "", "system_message": "You are a reasoning agent that can analyze customer and order data and provide recommendations for improving customer satisfaction and retention. You do not have access to any data sources, but you can reason based on the information provided to you by other agents. Use your reasoning skills to identify patterns, trends, and insights that can help improve customer satisfaction and retention. Provide actionable recommendations based on your analysis. You have access to other agents that can answer questions and provide data about customers, products, orders, inventory, and fulfilment. Use these agents to gather information as needed.", "description": "A reasoning agent that can analyze customer and order data and provide recommendations for improving customer satisfaction and retention.", diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md index d3f6c8f9c..1e559754e 100644 --- a/docs/CustomizingAzdParameters.md +++ b/docs/CustomizingAzdParameters.md @@ -15,17 +15,12 @@ By default this template will use the environment name as the prefix to prevent | `AZURE_ENV_GPT_MODEL_NAME` | string | `gpt-5.4-mini` | Specifies the underlying GPT model to be deployed (e.g., `gpt-5.4-mini`, `gpt-5.4`). | | `AZURE_ENV_GPT_MODEL_VERSION` | string | `2026-03-17` | Version of the GPT model to be used for deployment. | | `AZURE_ENV_GPT_DEPLOYMENT_NAME` | string | (defaults to `AZURE_ENV_GPT_MODEL_NAME`) | Optional. Deployment (alias) name used in Azure OpenAI for the main GPT model. This is the value referenced as `deployment_name` in `data/agent_teams/*.json`. Override this if you want the deployment alias to differ from the underlying model name. | -| `AZURE_ENV_GPT_MODEL_CAPACITY` | int | `50` | Sets the GPT model capacity. | +| `AZURE_ENV_GPT_MODEL_CAPACITY` | int | `100` | Sets the GPT model capacity. | | `AZURE_ENV_MODEL_5_4_DEPLOYMENT_TYPE` | string | `GlobalStandard` | Defines the deployment type for the AI model (e.g., Standard, GlobalStandard). | | `AZURE_ENV_MODEL_5_4_NAME` | string | `gpt-5.4` | Specifies the underlying larger GPT model to be deployed. | | `AZURE_ENV_MODEL_5_4_VERSION` | string | `2026-03-05` | Version of the larger GPT model to be used for deployment. | | `AZURE_ENV_MODEL_5_4_DEPLOYMENT_NAME` | string | (defaults to `AZURE_ENV_MODEL_5_4_NAME`) | Optional. Deployment (alias) name used in Azure OpenAI for the larger GPT model. Override if the alias should differ from the underlying model name. | | `AZURE_ENV_MODEL_5_4_CAPACITY` | int | `150` | Sets the GPT model capacity. | -| `AZURE_ENV_REASONING_MODEL_DEPLOYMENT_TYPE` | string | `GlobalStandard` | Defines the deployment type for the AI model (e.g., Standard, GlobalStandard). | -| `AZURE_ENV_REASONING_MODEL_NAME` | string | `gpt-5.4-mini` | Specifies the underlying reasoning GPT model to be deployed. | -| `AZURE_ENV_REASONING_MODEL_VERSION` | string | `2026-03-17` | Version of the reasoning GPT model to be used for deployment. | -| `AZURE_ENV_REASONING_DEPLOYMENT_NAME` | string | `gpt-5.4-mini-reasoning` (auto-derived when it would otherwise collide with `AZURE_ENV_GPT_DEPLOYMENT_NAME`) | Optional. Deployment (alias) name used in Azure OpenAI for the reasoning model. Must be unique from `AZURE_ENV_GPT_DEPLOYMENT_NAME`. | -| `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_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. | diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md index ff913301a..806f0d301 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -69,9 +69,9 @@ Ensure you have access to an [Azure subscription](https://azure.microsoft.com/fr **Default Quota Configuration:** - **gpt-5.4 (150k tokens)** — backs the larger GPT model deployment (alias `gpt-5.4`). -- **gpt-5.4-mini (100k tokens combined)** — 50k for the main GPT deployment (alias `gpt-5.4-mini`) plus 50k for the reasoning deployment (alias `gpt-5.4-mini-reasoning`). +- **gpt-5.4-mini (100k tokens)** — backs the smaller GPT model deployment (alias `gpt-5.4-mini`), also used for reasoning workloads. -> **Note:** The underlying models are `gpt-5.4` (2026-03-05 series) and `gpt-5.4-mini` (2026-03-17 series). Both are reasoning-capable GPT-5.4 GA models. The reasoning deployment intentionally uses the same underlying model as the main deployment but under a distinct alias so applications can route separately to reasoning-heavy workloads. +> **Note:** The underlying models are `gpt-5.4` (2026-03-05 series) and `gpt-5.4-mini` (2026-03-17 series). Both are reasoning-capable GPT-5.4 GA models. > **Note:** When you run `azd up`, the deployment will automatically show you regions with available quota, so this pre-check is optional but helpful for planning purposes. You can customize these settings later in [Step 3.3: Advanced Configuration](#33-advanced-configuration-optional). diff --git a/infra/main.bicep b/infra/main.bicep index 3ad03357f..a35be3976 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -65,18 +65,6 @@ param gpt5_4ModelVersion string = '2026-03-05' @description('Optional. Deployment (alias) name used in Azure OpenAI for the larger GPT model. Defaults to gpt5_4ModelName.') param gpt5_4DeploymentName string = gpt5_4ModelName -@minLength(1) -@description('Optional. Name of the underlying GPT Reasoning model to deploy. Defaults to gpt-5.4-mini (reasoning-capable, 2026-03-17 series).') -param gptReasoningModelName string = 'gpt-5.4-mini' - -@description('Optional. Version of the GPT Reasoning model to deploy. Defaults to 2026-03-17 (gpt-5.4-mini release).') -param gptReasoningModelVersion string = '2026-03-17' - -@description('Optional. Deployment (alias) name used in Azure OpenAI for the reasoning model. Must be unique from gptDeploymentName. Defaults to "{gptReasoningModelName}-reasoning" when it would otherwise collide with gptDeploymentName, otherwise gptReasoningModelName.') -param gptReasoningDeploymentName string = gptReasoningModelName == gptModelName - ? '${gptReasoningModelName}-reasoning' - : gptReasoningModelName - @description('Optional. Version of the Azure OpenAI service to deploy. Defaults to 2024-12-01-preview.') param azureOpenaiAPIVersion string = '2024-12-01-preview' @@ -99,23 +87,12 @@ param gpt5_4ModelDeploymentType string = 'GlobalStandard' @description('Optional. GPT model deployment type. Defaults to GlobalStandard.') param deploymentType string = 'GlobalStandard' -@minLength(1) -@allowed([ - 'Standard' - 'GlobalStandard' -]) -@description('Optional. GPT model deployment type. Defaults to GlobalStandard.') -param gptReasoningModelDeploymentType string = 'GlobalStandard' - @description('Optional. AI model deployment token capacity. Defaults to 50 for optimal performance.') -param gptDeploymentCapacity int = 50 +param gptDeploymentCapacity int = 100 @description('Optional. AI model deployment token capacity. Defaults to 150 for optimal performance.') param gpt5_4ModelCapacity int = 150 -@description('Optional. AI model deployment token capacity. Defaults to 50 for optimal performance.') -param gptReasoningModelCapacity int = 50 - @description('Optional. The tags to apply to all deployed Azure resources.') param tags resourceInput<'Microsoft.Resources/resourceGroups@2025-04-01'>.tags = {} @@ -717,6 +694,7 @@ var privateDnsZones = [ 'privatelink.documents.azure.com' 'privatelink.blob.core.windows.net' 'privatelink.search.windows.net' + 'privatelink.azurecr.io' ] // DNS Zone Index Constants @@ -727,6 +705,7 @@ var dnsZoneIndex = { cosmosDb: 3 blob: 4 search: 5 + containerRegistry: 6 } // List of DNS zone indices that correspond to AI-related services. @@ -800,17 +779,6 @@ var aiFoundryAiServices5_4ModelDeployment = { } raiPolicyName: 'Microsoft.Default' } -var aiFoundryAiServicesReasoningModelDeployment = { - format: 'OpenAI' - deploymentName: gptReasoningDeploymentName - name: gptReasoningModelName - version: gptReasoningModelVersion - sku: { - name: gptReasoningModelDeploymentType - capacity: gptReasoningModelCapacity - } - raiPolicyName: 'Microsoft.Default' -} var aiFoundryAiProjectDescription = 'AI Foundry Project' resource existingAiFoundryAiServices 'Microsoft.CognitiveServices/accounts@2025-12-01' existing = if (useExistingAiFoundryAiProject) { @@ -850,19 +818,6 @@ module existingAiFoundryAiServicesDeployments 'modules/ai-services-deployments.b capacity: aiFoundryAiServices5_4ModelDeployment.sku.capacity } } - { - name: aiFoundryAiServicesReasoningModelDeployment.deploymentName - model: { - format: aiFoundryAiServicesReasoningModelDeployment.format - name: aiFoundryAiServicesReasoningModelDeployment.name - version: aiFoundryAiServicesReasoningModelDeployment.version - } - raiPolicyName: aiFoundryAiServicesReasoningModelDeployment.raiPolicyName - sku: { - name: aiFoundryAiServicesReasoningModelDeployment.sku.name - capacity: aiFoundryAiServicesReasoningModelDeployment.sku.capacity - } - } ] roleAssignments: [ { @@ -925,19 +880,6 @@ module aiFoundryAiServices 'br:mcr.microsoft.com/bicep/avm/res/cognitive-service capacity: aiFoundryAiServices5_4ModelDeployment.sku.capacity } } - { - name: aiFoundryAiServicesReasoningModelDeployment.deploymentName - model: { - format: aiFoundryAiServicesReasoningModelDeployment.format - name: aiFoundryAiServicesReasoningModelDeployment.name - version: aiFoundryAiServicesReasoningModelDeployment.version - } - raiPolicyName: aiFoundryAiServicesReasoningModelDeployment.raiPolicyName - sku: { - name: aiFoundryAiServicesReasoningModelDeployment.sku.name - capacity: aiFoundryAiServicesReasoningModelDeployment.sku.capacity - } - } ] networkAcls: { defaultAction: 'Allow' @@ -1215,14 +1157,31 @@ module containerRegistry 'br/public:avm/res/container-registry/registry:0.12.0' params: { name: 'cr${solutionSuffix}' acrAdminUserEnabled: false - acrSku: 'Basic' + acrSku: enablePrivateNetworking ? 'Premium' : 'Basic' azureADAuthenticationAsArmPolicyStatus: 'enabled' - exportPolicyStatus: 'enabled' + exportPolicyStatus: enablePrivateNetworking ? 'disabled' : 'enabled' location: location softDeletePolicyDays: 7 softDeletePolicyStatus: 'disabled' tags: tags networkRuleBypassOptions: 'AzureServices' + // WAF: Premium SKU + private endpoint; public access disabled, pulls flow over the private endpoint. + publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' + networkRuleSetDefaultAction: enablePrivateNetworking ? 'Deny' : 'Allow' + privateEndpoints: enablePrivateNetworking + ? [ + { + name: 'pep-cr${solutionSuffix}' + customNetworkInterfaceName: 'nic-cr${solutionSuffix}' + privateDnsZoneGroup: { + privateDnsZoneGroupConfigs: [ + { privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.containerRegistry]!.outputs.resourceId } + ] + } + subnetResourceId: virtualNetwork!.outputs.backendSubnetResourceId + } + ] + : [] roleAssignments: [ { roleDefinitionIdOrName: acrPullRole @@ -1394,7 +1353,7 @@ module containerApp 'br/public:avm/res/app/container-app:0.22.0' = { } { name: 'REASONING_MODEL_NAME' - value: aiFoundryAiServicesReasoningModelDeployment.deploymentName + value: aiFoundryAiServicesModelDeployment.deploymentName } { name: 'MCP_SERVER_ENDPOINT' @@ -1418,7 +1377,7 @@ module containerApp 'br/public:avm/res/app/container-app:0.22.0' = { } { name: 'SUPPORTED_MODELS' - value: '["${aiFoundryAiServicesModelDeployment.deploymentName}","${aiFoundryAiServices5_4ModelDeployment.deploymentName}","${aiFoundryAiServicesReasoningModelDeployment.deploymentName}"]' + value: '["${aiFoundryAiServicesModelDeployment.deploymentName}","${aiFoundryAiServices5_4ModelDeployment.deploymentName}"]' } { name: 'AZURE_STORAGE_BLOB_URL' @@ -1917,10 +1876,10 @@ output AZURE_CLIENT_ID string = userAssignedIdentity!.outputs.clientId output AZURE_TENANT_ID string = tenant().tenantId output AZURE_AI_SEARCH_CONNECTION_NAME string = aiSearchConnectionName output AZURE_COGNITIVE_SERVICES string = 'https://cognitiveservices.azure.com/.default' -output REASONING_MODEL_NAME string = aiFoundryAiServicesReasoningModelDeployment.deploymentName +output REASONING_MODEL_NAME string = aiFoundryAiServicesModelDeployment.deploymentName output MCP_SERVER_NAME string = 'MacaeMcpServer' output MCP_SERVER_DESCRIPTION string = 'MCP server with greeting, HR, and planning tools' -output SUPPORTED_MODELS string = '["${aiFoundryAiServicesModelDeployment.deploymentName}","${aiFoundryAiServices5_4ModelDeployment.deploymentName}","${aiFoundryAiServicesReasoningModelDeployment.deploymentName}"]' +output SUPPORTED_MODELS string = '["${aiFoundryAiServicesModelDeployment.deploymentName}","${aiFoundryAiServices5_4ModelDeployment.deploymentName}"]' output BACKEND_URL string = 'https://${containerApp.outputs.fqdn}' output AZURE_AI_PROJECT_ENDPOINT string = aiFoundryAiProjectEndpoint output AZURE_AI_AGENT_ENDPOINT string = aiFoundryAiProjectEndpoint diff --git a/infra/main.json b/infra/main.json index e60ce30ae..489a6d1c9 100644 --- a/infra/main.json +++ b/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "17459100270519587144" + "templateHash": "8449191466193195939" }, "name": "Multi-Agent Custom Automation Engine", "description": "This module contains the resources required to deploy the [Multi-Agent Custom Automation Engine solution accelerator](https://github.com/microsoft/Multi-Agent-Custom-Automation-Engine-Solution-Accelerator) for both Sandbox environments and WAF aligned environments.\n\n> **Note:** This module is not intended for broad, generic use, as it was designed by the Commercial Solution Areas CTO team, as a Microsoft Solution Accelerator. Feature requests and bug fix requests are welcome if they support the needs of this organization but may not be incorporated if they aim to make this module more generic than what it needs to be for its primary use case. This module will likely be updated to leverage AVM resource modules in the future. This may result in breaking changes in upcoming versions when these features are implemented.\n" @@ -115,28 +115,6 @@ "description": "Optional. Deployment (alias) name used in Azure OpenAI for the larger GPT model. Defaults to gpt5_4ModelName." } }, - "gptReasoningModelName": { - "type": "string", - "defaultValue": "gpt-5.4-mini", - "minLength": 1, - "metadata": { - "description": "Optional. Name of the underlying GPT Reasoning model to deploy. Defaults to gpt-5.4-mini (reasoning-capable, 2026-03-17 series)." - } - }, - "gptReasoningModelVersion": { - "type": "string", - "defaultValue": "2026-03-17", - "metadata": { - "description": "Optional. Version of the GPT Reasoning model to deploy. Defaults to 2026-03-17 (gpt-5.4-mini release)." - } - }, - "gptReasoningDeploymentName": { - "type": "string", - "defaultValue": "[if(equals(parameters('gptReasoningModelName'), parameters('gptModelName')), format('{0}-reasoning', parameters('gptReasoningModelName')), parameters('gptReasoningModelName'))]", - "metadata": { - "description": "Optional. Deployment (alias) name used in Azure OpenAI for the reasoning model. Must be unique from gptDeploymentName. Defaults to \"{gptReasoningModelName}-reasoning\" when it would otherwise collide with gptDeploymentName, otherwise gptReasoningModelName." - } - }, "azureOpenaiAPIVersion": { "type": "string", "defaultValue": "2024-12-01-preview", @@ -175,21 +153,9 @@ "description": "Optional. GPT model deployment type. Defaults to GlobalStandard." } }, - "gptReasoningModelDeploymentType": { - "type": "string", - "defaultValue": "GlobalStandard", - "allowedValues": [ - "Standard", - "GlobalStandard" - ], - "minLength": 1, - "metadata": { - "description": "Optional. GPT model deployment type. Defaults to GlobalStandard." - } - }, "gptDeploymentCapacity": { "type": "int", - "defaultValue": 50, + "defaultValue": 100, "metadata": { "description": "Optional. AI model deployment token capacity. Defaults to 50 for optimal performance." } @@ -201,13 +167,6 @@ "description": "Optional. AI model deployment token capacity. Defaults to 150 for optimal performance." } }, - "gptReasoningModelCapacity": { - "type": "int", - "defaultValue": 50, - "metadata": { - "description": "Optional. AI model deployment token capacity. Defaults to 50 for optimal performance." - } - }, "tags": { "type": "object", "metadata": { @@ -445,7 +404,8 @@ "privatelink.services.ai.azure.com", "privatelink.documents.azure.com", "privatelink.blob.core.windows.net", - "privatelink.search.windows.net" + "privatelink.search.windows.net", + "privatelink.azurecr.io" ], "dnsZoneIndex": { "cognitiveServices": 0, @@ -453,7 +413,8 @@ "aiServices": 2, "cosmosDb": 3, "blob": 4, - "search": 5 + "search": 5, + "containerRegistry": 6 }, "aiRelatedDnsZoneIndices": [ "[variables('dnsZoneIndex').cognitiveServices]", @@ -487,17 +448,6 @@ }, "raiPolicyName": "Microsoft.Default" }, - "aiFoundryAiServicesReasoningModelDeployment": { - "format": "OpenAI", - "deploymentName": "[parameters('gptReasoningDeploymentName')]", - "name": "[parameters('gptReasoningModelName')]", - "version": "[parameters('gptReasoningModelVersion')]", - "sku": { - "name": "[parameters('gptReasoningModelDeploymentType')]", - "capacity": "[parameters('gptReasoningModelCapacity')]" - }, - "raiPolicyName": "Microsoft.Default" - }, "aiFoundryAiProjectDescription": "AI Foundry Project", "cosmosDbResourceName": "[format('cosmos-{0}', variables('solutionSuffix'))]", "cosmosDbDatabaseName": "macae", @@ -24292,19 +24242,6 @@ "name": "[variables('aiFoundryAiServices5_4ModelDeployment').sku.name]", "capacity": "[variables('aiFoundryAiServices5_4ModelDeployment').sku.capacity]" } - }, - { - "name": "[variables('aiFoundryAiServicesReasoningModelDeployment').deploymentName]", - "model": { - "format": "[variables('aiFoundryAiServicesReasoningModelDeployment').format]", - "name": "[variables('aiFoundryAiServicesReasoningModelDeployment').name]", - "version": "[variables('aiFoundryAiServicesReasoningModelDeployment').version]" - }, - "raiPolicyName": "[variables('aiFoundryAiServicesReasoningModelDeployment').raiPolicyName]", - "sku": { - "name": "[variables('aiFoundryAiServicesReasoningModelDeployment').sku.name]", - "capacity": "[variables('aiFoundryAiServicesReasoningModelDeployment').sku.capacity]" - } } ] }, @@ -24727,19 +24664,6 @@ "name": "[variables('aiFoundryAiServices5_4ModelDeployment').sku.name]", "capacity": "[variables('aiFoundryAiServices5_4ModelDeployment').sku.capacity]" } - }, - { - "name": "[variables('aiFoundryAiServicesReasoningModelDeployment').deploymentName]", - "model": { - "format": "[variables('aiFoundryAiServicesReasoningModelDeployment').format]", - "name": "[variables('aiFoundryAiServicesReasoningModelDeployment').name]", - "version": "[variables('aiFoundryAiServicesReasoningModelDeployment').version]" - }, - "raiPolicyName": "[variables('aiFoundryAiServicesReasoningModelDeployment').raiPolicyName]", - "sku": { - "name": "[variables('aiFoundryAiServicesReasoningModelDeployment').sku.name]", - "capacity": "[variables('aiFoundryAiServicesReasoningModelDeployment').sku.capacity]" - } } ] }, @@ -28000,9 +27924,9 @@ }, "dependsOn": [ "aiFoundryAiServices", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').aiServices)]", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", "virtualNetwork" ] }, @@ -38447,15 +38371,11 @@ "acrAdminUserEnabled": { "value": false }, - "acrSku": { - "value": "Basic" - }, + "acrSku": "[if(parameters('enablePrivateNetworking'), createObject('value', 'Premium'), createObject('value', 'Basic'))]", "azureADAuthenticationAsArmPolicyStatus": { "value": "enabled" }, - "exportPolicyStatus": { - "value": "enabled" - }, + "exportPolicyStatus": "[if(parameters('enablePrivateNetworking'), createObject('value', 'disabled'), createObject('value', 'enabled'))]", "location": { "value": "[parameters('location')]" }, @@ -38471,6 +38391,9 @@ "networkRuleBypassOptions": { "value": "AzureServices" }, + "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), createObject('value', 'Disabled'), createObject('value', 'Enabled'))]", + "networkRuleSetDefaultAction": "[if(parameters('enablePrivateNetworking'), createObject('value', 'Deny'), createObject('value', 'Allow'))]", + "privateEndpoints": "[if(parameters('enablePrivateNetworking'), createObject('value', createArray(createObject('name', format('pep-cr{0}', variables('solutionSuffix')), 'customNetworkInterfaceName', format('nic-cr{0}', variables('solutionSuffix')), 'privateDnsZoneGroup', createObject('privateDnsZoneGroupConfigs', createArray(createObject('privateDnsZoneResourceId', reference(format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').containerRegistry)).outputs.resourceId.value))), 'subnetResourceId', reference('virtualNetwork').outputs.backendSubnetResourceId.value))), createObject('value', createArray()))]", "roleAssignments": { "value": [ { @@ -42288,7 +42211,9 @@ } }, "dependsOn": [ - "userAssignedIdentity" + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').containerRegistry)]", + "userAssignedIdentity", + "virtualNetwork" ] }, "containerApp": { @@ -42474,7 +42399,7 @@ }, { "name": "REASONING_MODEL_NAME", - "value": "[variables('aiFoundryAiServicesReasoningModelDeployment').deploymentName]" + "value": "[variables('aiFoundryAiServicesModelDeployment').deploymentName]" }, { "name": "MCP_SERVER_ENDPOINT", @@ -42498,7 +42423,7 @@ }, { "name": "SUPPORTED_MODELS", - "value": "[format('[\"{0}\",\"{1}\",\"{2}\"]', variables('aiFoundryAiServicesModelDeployment').deploymentName, variables('aiFoundryAiServices5_4ModelDeployment').deploymentName, variables('aiFoundryAiServicesReasoningModelDeployment').deploymentName)]" + "value": "[format('[\"{0}\",\"{1}\"]', variables('aiFoundryAiServicesModelDeployment').deploymentName, variables('aiFoundryAiServices5_4ModelDeployment').deploymentName)]" }, { "name": "AZURE_STORAGE_BLOB_URL", @@ -58950,7 +58875,7 @@ }, "REASONING_MODEL_NAME": { "type": "string", - "value": "[variables('aiFoundryAiServicesReasoningModelDeployment').deploymentName]" + "value": "[variables('aiFoundryAiServicesModelDeployment').deploymentName]" }, "MCP_SERVER_NAME": { "type": "string", @@ -58962,7 +58887,7 @@ }, "SUPPORTED_MODELS": { "type": "string", - "value": "[format('[\"{0}\",\"{1}\",\"{2}\"]', variables('aiFoundryAiServicesModelDeployment').deploymentName, variables('aiFoundryAiServices5_4ModelDeployment').deploymentName, variables('aiFoundryAiServicesReasoningModelDeployment').deploymentName)]" + "value": "[format('[\"{0}\",\"{1}\"]', variables('aiFoundryAiServicesModelDeployment').deploymentName, variables('aiFoundryAiServices5_4ModelDeployment').deploymentName)]" }, "BACKEND_URL": { "type": "string", diff --git a/infra/main.parameters.json b/infra/main.parameters.json index d8d4c43ef..6141b7a30 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -41,21 +41,6 @@ "gpt5_4ModelCapacity": { "value": "${AZURE_ENV_MODEL_5_4_CAPACITY}" }, - "gptReasoningModelDeploymentType": { - "value": "${AZURE_ENV_REASONING_MODEL_DEPLOYMENT_TYPE}" - }, - "gptReasoningModelName": { - "value": "${AZURE_ENV_REASONING_MODEL_NAME}" - }, - "gptReasoningModelVersion": { - "value": "${AZURE_ENV_REASONING_MODEL_VERSION}" - }, - "gptReasoningDeploymentName": { - "value": "${AZURE_ENV_REASONING_DEPLOYMENT_NAME}" - }, - "gptReasoningModelCapacity": { - "value": "${AZURE_ENV_REASONING_MODEL_CAPACITY}" - }, "backendContainerImageTag": { "value": "${AZURE_ENV_IMAGE_TAG=latest_v4}" }, diff --git a/infra/main.waf.parameters.json b/infra/main.waf.parameters.json index 9175b4888..488abd69b 100644 --- a/infra/main.waf.parameters.json +++ b/infra/main.waf.parameters.json @@ -41,21 +41,6 @@ "gpt5_4ModelCapacity": { "value": "${AZURE_ENV_MODEL_5_4_CAPACITY}" }, - "gptReasoningModelDeploymentType": { - "value": "${AZURE_ENV_REASONING_MODEL_DEPLOYMENT_TYPE}" - }, - "gptReasoningModelName": { - "value": "${AZURE_ENV_REASONING_MODEL_NAME}" - }, - "gptReasoningModelVersion": { - "value": "${AZURE_ENV_REASONING_MODEL_VERSION}" - }, - "gptReasoningDeploymentName": { - "value": "${AZURE_ENV_REASONING_DEPLOYMENT_NAME}" - }, - "gptReasoningModelCapacity": { - "value": "${AZURE_ENV_REASONING_MODEL_CAPACITY}" - }, "backendContainerImageTag": { "value": "${AZURE_ENV_IMAGE_TAG=latest_v4}" }, diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index 4ec9aab3e..4721413de 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -65,18 +65,6 @@ param gpt5_4ModelVersion string = '2026-03-05' @description('Optional. Deployment (alias) name used in Azure OpenAI for the larger GPT model. Defaults to gpt5_4ModelName.') param gpt5_4DeploymentName string = gpt5_4ModelName -@minLength(1) -@description('Optional. Name of the underlying GPT Reasoning model to deploy. Defaults to gpt-5.4-mini (reasoning-capable, 2026-03-17 series).') -param gptReasoningModelName string = 'gpt-5.4-mini' - -@description('Optional. Version of the GPT Reasoning model to deploy. Defaults to 2026-03-17 (gpt-5.4-mini release).') -param gptReasoningModelVersion string = '2026-03-17' - -@description('Optional. Deployment (alias) name used in Azure OpenAI for the reasoning model. Must be unique from gptDeploymentName. Defaults to "{gptReasoningModelName}-reasoning" when it would otherwise collide with gptDeploymentName, otherwise gptReasoningModelName.') -param gptReasoningDeploymentName string = gptReasoningModelName == gptModelName - ? '${gptReasoningModelName}-reasoning' - : gptReasoningModelName - @description('Optional. Version of the Azure OpenAI service to deploy. Defaults to 2024-12-01-preview.') param azureOpenaiAPIVersion string = '2024-12-01-preview' @@ -99,23 +87,12 @@ param gpt5_4ModelDeploymentType string = 'GlobalStandard' @description('Optional. GPT model deployment type. Defaults to GlobalStandard.') param deploymentType string = 'GlobalStandard' -@minLength(1) -@allowed([ - 'Standard' - 'GlobalStandard' -]) -@description('Optional. GPT model deployment type. Defaults to GlobalStandard.') -param gptReasoningModelDeploymentType string = 'GlobalStandard' - @description('Optional. AI model deployment token capacity. Defaults to 50 for optimal performance.') -param gptDeploymentCapacity int = 50 +param gptDeploymentCapacity int = 100 @description('Optional. AI model deployment token capacity. Defaults to 150 for optimal performance.') param gpt5_4ModelCapacity int = 150 -@description('Optional. AI model deployment token capacity. Defaults to 50 for optimal performance.') -param gptReasoningModelCapacity int = 50 - @description('Optional. The tags to apply to all deployed Azure resources.') param tags resourceInput<'Microsoft.Resources/resourceGroups@2025-04-01'>.tags = {} @@ -799,17 +776,6 @@ var aiFoundryAiServices5_4ModelDeployment = { } raiPolicyName: 'Microsoft.Default' } -var aiFoundryAiServicesReasoningModelDeployment = { - format: 'OpenAI' - deploymentName: gptReasoningDeploymentName - name: gptReasoningModelName - version: gptReasoningModelVersion - sku: { - name: gptReasoningModelDeploymentType - capacity: gptReasoningModelCapacity - } - raiPolicyName: 'Microsoft.Default' -} var aiFoundryAiProjectDescription = 'AI Foundry Project' resource existingAiFoundryAiServices 'Microsoft.CognitiveServices/accounts@2025-12-01' existing = if (useExistingAiFoundryAiProject) { @@ -849,19 +815,6 @@ module existingAiFoundryAiServicesDeployments 'modules/ai-services-deployments.b capacity: aiFoundryAiServices5_4ModelDeployment.sku.capacity } } - { - name: aiFoundryAiServicesReasoningModelDeployment.deploymentName - model: { - format: aiFoundryAiServicesReasoningModelDeployment.format - name: aiFoundryAiServicesReasoningModelDeployment.name - version: aiFoundryAiServicesReasoningModelDeployment.version - } - raiPolicyName: aiFoundryAiServicesReasoningModelDeployment.raiPolicyName - sku: { - name: aiFoundryAiServicesReasoningModelDeployment.sku.name - capacity: aiFoundryAiServicesReasoningModelDeployment.sku.capacity - } - } ] roleAssignments: [ { @@ -924,19 +877,6 @@ module aiFoundryAiServices 'br:mcr.microsoft.com/bicep/avm/res/cognitive-service capacity: aiFoundryAiServices5_4ModelDeployment.sku.capacity } } - { - name: aiFoundryAiServicesReasoningModelDeployment.deploymentName - model: { - format: aiFoundryAiServicesReasoningModelDeployment.format - name: aiFoundryAiServicesReasoningModelDeployment.name - version: aiFoundryAiServicesReasoningModelDeployment.version - } - raiPolicyName: aiFoundryAiServicesReasoningModelDeployment.raiPolicyName - sku: { - name: aiFoundryAiServicesReasoningModelDeployment.sku.name - capacity: aiFoundryAiServicesReasoningModelDeployment.sku.capacity - } - } ] networkAcls: { defaultAction: 'Allow' @@ -1388,7 +1328,7 @@ module containerApp 'br/public:avm/res/app/container-app:0.22.0' = { } { name: 'REASONING_MODEL_NAME' - value: aiFoundryAiServicesReasoningModelDeployment.deploymentName + value: aiFoundryAiServicesModelDeployment.deploymentName } { name: 'MCP_SERVER_ENDPOINT' @@ -1412,7 +1352,7 @@ module containerApp 'br/public:avm/res/app/container-app:0.22.0' = { } { name: 'SUPPORTED_MODELS' - value: '["${aiFoundryAiServicesModelDeployment.deploymentName}","${aiFoundryAiServices5_4ModelDeployment.deploymentName}","${aiFoundryAiServicesReasoningModelDeployment.deploymentName}"]' + value: '["${aiFoundryAiServicesModelDeployment.deploymentName}","${aiFoundryAiServices5_4ModelDeployment.deploymentName}"]' } { name: 'AZURE_STORAGE_BLOB_URL' @@ -1905,10 +1845,10 @@ output AZURE_CLIENT_ID string = userAssignedIdentity!.outputs.clientId output AZURE_TENANT_ID string = tenant().tenantId output AZURE_AI_SEARCH_CONNECTION_NAME string = aiSearchConnectionName output AZURE_COGNITIVE_SERVICES string = 'https://cognitiveservices.azure.com/.default' -output REASONING_MODEL_NAME string = aiFoundryAiServicesReasoningModelDeployment.deploymentName +output REASONING_MODEL_NAME string = aiFoundryAiServicesModelDeployment.deploymentName output MCP_SERVER_NAME string = 'MacaeMcpServer' output MCP_SERVER_DESCRIPTION string = 'MCP server with greeting, HR, and planning tools' -output SUPPORTED_MODELS string = '["${aiFoundryAiServicesModelDeployment.deploymentName}","${aiFoundryAiServices5_4ModelDeployment.deploymentName}","${aiFoundryAiServicesReasoningModelDeployment.deploymentName}"]' +output SUPPORTED_MODELS string = '["${aiFoundryAiServicesModelDeployment.deploymentName}","${aiFoundryAiServices5_4ModelDeployment.deploymentName}"]' output BACKEND_URL string = 'https://${containerApp.outputs.fqdn}' output AZURE_AI_PROJECT_ENDPOINT string = aiFoundryAiProjectEndpoint output AZURE_AI_AGENT_ENDPOINT string = aiFoundryAiProjectEndpoint diff --git a/infra/scripts/Build-And-Push-Images.ps1 b/infra/scripts/Build-And-Push-Images.ps1 index 84705a6d8..5fbea1ab5 100644 --- a/infra/scripts/Build-And-Push-Images.ps1 +++ b/infra/scripts/Build-And-Push-Images.ps1 @@ -152,6 +152,21 @@ foreach ($img in $images) { } } +# --- WAF: temporarily relax ACR restrictions for build/push, restored in finally --- +$deploymentType = & az group show --name $resourceGroup --query "tags.Type" -o tsv 2>$null + +if ($deploymentType -eq 'WAF') { + Write-Section 'WAF deployment detected - temporarily relaxing ACR restrictions' + & az acr update --name $acrName --resource-group $resourceGroup --allow-exports true --output none + if ($LASTEXITCODE -ne 0) { throw "Failed to enable ACR exports." } + & az acr update --name $acrName --resource-group $resourceGroup --public-network-enabled true --output none + if ($LASTEXITCODE -ne 0) { throw "Failed to enable ACR public network access." } + & az acr update --name $acrName --resource-group $resourceGroup --default-action Allow --output none + if ($LASTEXITCODE -ne 0) { throw "Failed to set ACR default action to Allow." } + Write-Host 'ACR restrictions temporarily relaxed.' +} + +try { # --- Build & push ----------------------------------------------------------- Write-Section "Building and pushing images ($BuildMode)" @@ -241,3 +256,13 @@ Write-Host "configurations and index the sample data:" -ForegroundColor White Write-Host "" Write-Host " infra\scripts\Selecting-Team-Config-And-Data.ps1" -ForegroundColor Cyan Write-Host "" +} +finally { + if ($deploymentType -eq 'WAF') { + Write-Section 'Restoring WAF ACR configuration' + & az acr update --name $acrName --resource-group $resourceGroup --default-action Deny --output none + & az acr update --name $acrName --resource-group $resourceGroup --public-network-enabled false --output none + & az acr update --name $acrName --resource-group $resourceGroup --allow-exports false --output none + Write-Host 'ACR configuration restored.' + } +} diff --git a/infra/scripts/build_and_push_images.sh b/infra/scripts/build_and_push_images.sh index 1ef2e90e5..4e7d533d4 100644 --- a/infra/scripts/build_and_push_images.sh +++ b/infra/scripts/build_and_push_images.sh @@ -99,6 +99,29 @@ for ctx in "${IMAGE_CTXS[@]}"; do fi done +# --- WAF: temporarily relax ACR restrictions for build/push, restored on exit --- +DEPLOYMENT_TYPE="$(az group show --name "${RESOURCE_GROUP}" --query "tags.Type" -o tsv 2>/dev/null || true)" + +restore_acr_waf() { + if [ "${DEPLOYMENT_TYPE}" = "WAF" ]; then + echo "" + echo "Restoring WAF ACR configuration..." + az acr update --name "${ACR_NAME}" --resource-group "${RESOURCE_GROUP}" --default-action Deny --output none || true + az acr update --name "${ACR_NAME}" --resource-group "${RESOURCE_GROUP}" --public-network-enabled false --output none || true + az acr update --name "${ACR_NAME}" --resource-group "${RESOURCE_GROUP}" --allow-exports false --output none || true + echo "ACR configuration restored." + fi +} + +if [ "${DEPLOYMENT_TYPE}" = "WAF" ]; then + section "WAF deployment detected - temporarily relaxing ACR restrictions" + trap restore_acr_waf EXIT + az acr update --name "${ACR_NAME}" --resource-group "${RESOURCE_GROUP}" --allow-exports true --output none + az acr update --name "${ACR_NAME}" --resource-group "${RESOURCE_GROUP}" --public-network-enabled true --output none + az acr update --name "${ACR_NAME}" --resource-group "${RESOURCE_GROUP}" --default-action Allow --output none + echo "ACR restrictions temporarily relaxed." +fi + section "Building and pushing images (${BUILD_MODE})" for i in "${!IMAGE_NAMES[@]}"; do diff --git a/infra/scripts/checkquota.sh b/infra/scripts/checkquota.sh index 02349b91c..082f49fa6 100644 --- a/infra/scripts/checkquota.sh +++ b/infra/scripts/checkquota.sh @@ -5,14 +5,13 @@ IFS=', ' read -ra REGIONS <<< "$AZURE_REGIONS" SUBSCRIPTION_ID="${AZURE_SUBSCRIPTION_ID}" GPT_5_4_MINI_MIN_CAPACITY="${GPT_5_4_MINI_MIN_CAPACITY}" -GPT_REASONING_MIN_CAPACITY="${GPT_REASONING_MIN_CAPACITY}" GPT_5_4_MIN_CAPACITY="${GPT_5_4_MIN_CAPACITY}" echo "🔄 Validating required environment variables..." if [[ -z "$SUBSCRIPTION_ID" || -z "$REGIONS" ]]; then echo "❌ ERROR: Missing required environment variables." echo "Required: AZURE_SUBSCRIPTION_ID, AZURE_REGIONS" - echo "Optional: GPT_5_4_MINI_MIN_CAPACITY (default: 50), GPT_REASONING_MIN_CAPACITY (default: 50), GPT_5_4_MIN_CAPACITY (default: 150)" + echo "Optional: GPT_5_4_MINI_MIN_CAPACITY (default: 100), GPT_5_4_MIN_CAPACITY (default: 150)" exit 1 fi @@ -25,7 +24,7 @@ echo "✅ Azure subscription set successfully." # Define models and their minimum required capacities declare -A MIN_CAPACITY=( - ["OpenAI.GlobalStandard.gpt-5.4-mini"]="$(( ${GPT_5_4_MINI_MIN_CAPACITY:-0} + ${GPT_REASONING_MIN_CAPACITY:-0} ))" + ["OpenAI.GlobalStandard.gpt-5.4-mini"]="${GPT_5_4_MINI_MIN_CAPACITY:-0}" ["OpenAI.GlobalStandard.gpt-5.4"]="${GPT_5_4_MIN_CAPACITY}" ) diff --git a/src/App/src/hooks/usePlanWebSocket.tsx b/src/App/src/hooks/usePlanWebSocket.tsx index 234768ff2..d94834416 100644 --- a/src/App/src/hooks/usePlanWebSocket.tsx +++ b/src/App/src/hooks/usePlanWebSocket.tsx @@ -5,7 +5,7 @@ * Dispatches Redux actions for each event type so PlanPage no longer * needs 7+ useEffect blocks for WebSocket handling. */ -import React, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; import webSocketService from '@/store/WebSocketService'; import { PlanDataService } from '@/store/PlanDataService'; import { useAppDispatch, useAppSelector } from '@/store/hooks'; @@ -100,6 +100,11 @@ export function usePlanWebSocket({ const continueWithWebsocketFlow = useAppSelector(selectContinueWithWebsocketFlow); const streamingMessageBuffer = useAppSelector(selectStreamingMessageBuffer); + // Coalesce high-frequency streaming tokens into one flush per animation frame + // to avoid a synchronous re-render per token freezing the UI on fast streams. + const streamingChunkQueueRef = useRef([]); + const streamingFlushHandleRef = useRef(null); + // ── PLAN_APPROVAL_REQUEST ───────────────────────────────────── useEffect(() => { const unsub = webSocketService.on( @@ -129,15 +134,38 @@ export function usePlanWebSocket({ // ── AGENT_MESSAGE_STREAMING ─────────────────────────────────── useEffect(() => { + const flushStreamingChunks = () => { + streamingFlushHandleRef.current = null; + const chunks = streamingChunkQueueRef.current; + if (chunks.length === 0) return; + streamingChunkQueueRef.current = []; + dispatch(setShowBufferingText(true)); + dispatch(appendToStreamingBuffer(chunks.join(''))); + }; + const unsub = webSocketService.on( WebsocketMessageType.AGENT_MESSAGE_STREAMING, (msg: any) => { const line = PlanDataService.simplifyHumanClarification(msg.data.content); - dispatch(setShowBufferingText(true)); - dispatch(appendToStreamingBuffer(line)); + streamingChunkQueueRef.current.push(line); + if (streamingFlushHandleRef.current === null) { + streamingFlushHandleRef.current = requestAnimationFrame(flushStreamingChunks); + } }, ); - return unsub; + return () => { + unsub(); + // Cancel pending frame and flush leftovers so no streamed text is lost + if (streamingFlushHandleRef.current !== null) { + cancelAnimationFrame(streamingFlushHandleRef.current); + streamingFlushHandleRef.current = null; + } + if (streamingChunkQueueRef.current.length > 0) { + const remaining = streamingChunkQueueRef.current.join(''); + streamingChunkQueueRef.current = []; + dispatch(appendToStreamingBuffer(remaining)); + } + }; }, [dispatch]); // ── USER_CLARIFICATION_REQUEST ──────────────────────────────── @@ -218,6 +246,24 @@ export function usePlanWebSocket({ scrollToBottom(); showToast(errorContent, 'error'); webSocketService.disconnect(); + } else { + // Any other terminal status (e.g. "terminated"): clear the spinner so the UI doesn't hang after the answer arrived + const content = finalMessage.data?.content; + if (content) { + dispatch(addAgentMessage({ + agent: AgentType.GROUP_CHAT_MANAGER, + agent_type: AgentMessageType.AI_AGENT, + timestamp: Date.now(), + steps: [], + next_steps: [], + content, + raw_data: finalMessage, + })); + } + dispatch(setShowBufferingText(false)); + dispatch(setShowProcessingPlanSpinner(false)); + scrollToBottom(); + webSocketService.disconnect(); } }, ); diff --git a/src/backend/.env.sample b/src/backend/.env.sample index 0f176c65d..887fa58e5 100644 --- a/src/backend/.env.sample +++ b/src/backend/.env.sample @@ -19,7 +19,7 @@ AZURE_COGNITIVE_SERVICES="https://cognitiveservices.azure.com/.default" AZURE_AI_PROJECT_ENDPOINT= AZURE_AI_AGENT_ENDPOINT= # AZURE_BING_CONNECTION_NAME= -REASONING_MODEL_NAME="gpt-5.4-mini-reasoning" +REASONING_MODEL_NAME="gpt-5.4-mini" APP_ENV=dev MCP_SERVER_ENDPOINT=http://localhost:9000/mcp MCP_SERVER_NAME=MacaeMcpServer @@ -28,7 +28,7 @@ AZURE_TENANT_ID= AZURE_CLIENT_ID= BACKEND_API_URL=http://localhost:8000 FRONTEND_SITE_NAME=* -SUPPORTED_MODELS='["gpt-5.4-mini","gpt-5.4","gpt-5.4-mini-reasoning"]' +SUPPORTED_MODELS='["gpt-5.4-mini","gpt-5.4"]' AZURE_AI_SEARCH_CONNECTION_NAME= AZURE_AI_SEARCH_ENDPOINT= BING_CONNECTION_NAME= diff --git a/src/backend/common/config/app_config.py b/src/backend/common/config/app_config.py index 29dea29ee..c8d56f218 100644 --- a/src/backend/common/config/app_config.py +++ b/src/backend/common/config/app_config.py @@ -58,7 +58,7 @@ def __init__(self): "AZURE_OPENAI_API_VERSION", "2024-11-20" ) self.AZURE_OPENAI_ENDPOINT = self._get_required("AZURE_OPENAI_ENDPOINT") - self.REASONING_MODEL_NAME = self._get_optional("REASONING_MODEL_NAME", "gpt-5.4-mini-reasoning") + self.REASONING_MODEL_NAME = self._get_optional("REASONING_MODEL_NAME", "gpt-5.4-mini") # self.AZURE_BING_CONNECTION_NAME = self._get_optional( # "AZURE_BING_CONNECTION_NAME" # ) diff --git a/src/backend/v4/orchestration/human_approval_manager.py b/src/backend/v4/orchestration/human_approval_manager.py index d6c5e1ed8..35e47d28f 100644 --- a/src/backend/v4/orchestration/human_approval_manager.py +++ b/src/backend/v4/orchestration/human_approval_manager.py @@ -260,14 +260,30 @@ async def create_progress_ledger(self, magentic_context: MagenticContext): # Delegate to base for normal progress ledger creation ledger = await super().create_progress_ledger(magentic_context) - # --- Premature satisfaction guard --- - # If the LLM says the request is satisfied, verify that all planned - # (non-proxy, non-manager) agents have actually responded before allowing - # the workflow to terminate. This addresses the bug where the orchestrator - # marks satisfied=True after a single comprehensive agent response. + # --- Premature satisfaction guard (bounded to avoid infinite loops) --- if ledger.is_request_satisfied.answer: uncalled = self._get_uncalled_agents(magentic_context) - if uncalled: + total_agents = len(self._get_all_planned_agents(magentic_context)) + # Bound re-routing so an unrecognized agent can't loop until max_rounds + guard_round_budget = 2 * max(total_agents, 1) + self.max_stall_count + within_budget = magentic_context.round_count < guard_round_budget + if uncalled and not within_budget: + logger.warning( + "Premature satisfaction guard exhausted its round budget (%d) with " + "agent(s) still marked uncalled: %s. Allowing termination to avoid an " + "infinite loop (likely an author_name/participant-name mismatch).", + guard_round_budget, + uncalled, + ) + if uncalled and within_budget: + logger.warning( + "Premature satisfaction guard exhausted its round budget (%d) with " + "agent(s) still marked uncalled: %s. Allowing termination to avoid an " + "infinite loop (likely an author_name/participant-name mismatch).", + guard_round_budget, + uncalled, + ) + if uncalled and within_budget: next_agent = uncalled[0] logger.info( "Progress ledger marked satisfied but %d agent(s) have not responded yet: %s. " @@ -284,9 +300,6 @@ async def create_progress_ledger(self, magentic_context: MagenticContext): ledger.is_progress_being_made.reason = "Continuing to consult remaining agents" ledger.next_speaker.answer = next_agent ledger.next_speaker.reason = f"{next_agent} has not yet been consulted" - # Always override instruction with task-relevant prompt so that - # data agents (Azure AI Search, RAG) execute meaningful queries - # instead of receiving a stale finalization instruction. task_text = getattr(magentic_context.task, "text", str(magentic_context.task)) ledger.instruction_or_question.answer = ( f"Using your available tools and data sources, provide your response for the following task: {task_text}" @@ -298,25 +311,40 @@ async def create_progress_ledger(self, magentic_context: MagenticContext): return ledger @staticmethod - def _get_uncalled_agents(magentic_context: MagenticContext) -> list[str]: - """Return agent names from participant_descriptions that have not yet - authored a message in the chat_history (excluding ProxyAgent and the - MagenticManager).""" - skip_names = {"ProxyAgent", "MagenticManager", "magentic_manager"} - - all_agents = [ - name for name in magentic_context.participant_descriptions - if name not in skip_names + def _normalize_agent_name(name: str) -> str: + """Canonicalize an agent name (lowercase, alphanumeric-only) so service-side + name sanitization doesn't break responded-detection.""" + return "".join(ch for ch in (name or "").lower() if ch.isalnum()) + + @classmethod + def _get_all_planned_agents(cls, magentic_context: MagenticContext) -> list[str]: + """Return planned agent names, excluding ProxyAgent and the MagenticManager.""" + skip_names = { + cls._normalize_agent_name(n) + for n in ("ProxyAgent", "MagenticManager", "magentic_manager") + } + return [ + name + for name in magentic_context.participant_descriptions + if cls._normalize_agent_name(name) not in skip_names ] - # Collect author names that appear in chat_history + @classmethod + def _get_uncalled_agents(cls, magentic_context: MagenticContext) -> list[str]: + """Return planned agents that have not yet authored a message (normalized match).""" + all_agents = cls._get_all_planned_agents(magentic_context) + responded = set() for msg in magentic_context.chat_history: author = getattr(msg, "author_name", None) if author: - responded.add(author) + responded.add(cls._normalize_agent_name(author)) - return [name for name in all_agents if name not in responded] + return [ + name + for name in all_agents + if cls._normalize_agent_name(name) not in responded + ] async def _wait_for_user_approval( self, m_plan_id: Optional[str] = None From 1ab8138a05b7c7fe641c66f95c1d1cc3278dfd40 Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Tue, 7 Jul 2026 18:11:35 +0530 Subject: [PATCH 11/12] remove unused instances list from TestDatabaseFactoryGetDatabase --- src/tests/backend/common/database/test_database_factory.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tests/backend/common/database/test_database_factory.py b/src/tests/backend/common/database/test_database_factory.py index f7a277355..3ac8b5079 100644 --- a/src/tests/backend/common/database/test_database_factory.py +++ b/src/tests/backend/common/database/test_database_factory.py @@ -183,7 +183,6 @@ async def test_get_database_different_users_get_different_instances(self): mock_config.COSMOSDB_CONTAINER = "test_container" mock_config.get_azure_credentials.return_value = "mock_credentials" - instances = [] with patch('backend.common.database.database_factory.CosmosDBClient') as mock_cosmos_class: def make_instance(**kwargs): inst = Mock(spec=CosmosDBClient) From 91e331799be6e6e78af6174fa8bbc9bbbe3ff1e6 Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Tue, 7 Jul 2026 18:35:30 +0530 Subject: [PATCH 12/12] update minimum required capacities for GPT models in checkquota.sh; remove premature satisfaction guard logging in human_approval_manager.py --- infra/scripts/checkquota.sh | 4 ++-- src/backend/v4/orchestration/human_approval_manager.py | 8 -------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/infra/scripts/checkquota.sh b/infra/scripts/checkquota.sh index 082f49fa6..c32787c26 100644 --- a/infra/scripts/checkquota.sh +++ b/infra/scripts/checkquota.sh @@ -24,8 +24,8 @@ echo "✅ Azure subscription set successfully." # Define models and their minimum required capacities declare -A MIN_CAPACITY=( - ["OpenAI.GlobalStandard.gpt-5.4-mini"]="${GPT_5_4_MINI_MIN_CAPACITY:-0}" - ["OpenAI.GlobalStandard.gpt-5.4"]="${GPT_5_4_MIN_CAPACITY}" + ["OpenAI.GlobalStandard.gpt-5.4-mini"]="${GPT_5_4_MINI_MIN_CAPACITY:-100}" + ["OpenAI.GlobalStandard.gpt-5.4"]="${GPT_5_4_MIN_CAPACITY:-150}" ) VALID_REGION="" diff --git a/src/backend/v4/orchestration/human_approval_manager.py b/src/backend/v4/orchestration/human_approval_manager.py index 35e47d28f..2b9cd390c 100644 --- a/src/backend/v4/orchestration/human_approval_manager.py +++ b/src/backend/v4/orchestration/human_approval_manager.py @@ -275,14 +275,6 @@ async def create_progress_ledger(self, magentic_context: MagenticContext): guard_round_budget, uncalled, ) - if uncalled and within_budget: - logger.warning( - "Premature satisfaction guard exhausted its round budget (%d) with " - "agent(s) still marked uncalled: %s. Allowing termination to avoid an " - "infinite loop (likely an author_name/participant-name mismatch).", - guard_round_budget, - uncalled, - ) if uncalled and within_budget: next_agent = uncalled[0] logger.info(