diff --git a/azure.yaml b/azure.yaml index 40dcebf6a..e1eaa7600 100644 --- a/azure.yaml +++ b/azure.yaml @@ -91,4 +91,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/content_packs/content_gen/agent_teams/content_gen.json b/content_packs/content_gen/agent_teams/content_gen.json index 7d349e473..27ec3846b 100644 --- a/content_packs/content_gen/agent_teams/content_gen.json +++ b/content_packs/content_gen/agent_teams/content_gen.json @@ -6,6 +6,7 @@ "created": "", "created_by": "", "deployment_name": "gpt-5.4-mini", + "is_default": true, "agents": [ { "input_key": "CG_triage_agent", diff --git a/content_packs/contract_compliance/agent_teams/contract_compliance_team.json b/content_packs/contract_compliance/agent_teams/contract_compliance_team.json index bcfd37458..c692e2008 100644 --- a/content_packs/contract_compliance/agent_teams/contract_compliance_team.json +++ b/content_packs/contract_compliance/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/content_packs/hr_onboarding/agent_teams/hr.json b/content_packs/hr_onboarding/agent_teams/hr.json index 188b86123..b6b207490 100644 --- a/content_packs/hr_onboarding/agent_teams/hr.json +++ b/content_packs/hr_onboarding/agent_teams/hr.json @@ -6,6 +6,7 @@ "created": "", "created_by": "", "deployment_name": "gpt-5.4", + "is_default": true, "agents": [ { "input_key": "", diff --git a/content_packs/marketing_press_release/agent_teams/marketing.json b/content_packs/marketing_press_release/agent_teams/marketing.json index 3a209fe70..ebd8b0cfe 100644 --- a/content_packs/marketing_press_release/agent_teams/marketing.json +++ b/content_packs/marketing_press_release/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/content_packs/retail_customer/agent_teams/retail.json b/content_packs/retail_customer/agent_teams/retail.json index 0ea89abfd..d345f9f17 100644 --- a/content_packs/retail_customer/agent_teams/retail.json +++ b/content_packs/retail_customer/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/content_packs/rfp_evaluation/agent_teams/rfp_analysis_team.json b/content_packs/rfp_evaluation/agent_teams/rfp_analysis_team.json index 763b9ce34..7dd5d7a56 100644 --- a/content_packs/rfp_evaluation/agent_teams/rfp_analysis_team.json +++ b/content_packs/rfp_evaluation/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/docs/AzureGPTQuotaSettings.md b/docs/AzureGPTQuotaSettings.md index 1d7d0c7ca..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-5.4`) + - 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/DeploymentGuide.md b/docs/DeploymentGuide.md index 24a9c3e43..3690f4b00 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -68,8 +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.4-mini:** 100k tokens -- **GPT-5.4:** 150k tokens +- **gpt-5.4 (150k tokens)** β€” backs the larger GPT model deployment (alias `gpt-5.4`). +- **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. > **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). @@ -323,7 +325,8 @@ 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 building and pushing the application container images 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) @@ -391,6 +394,10 @@ The images are **built remotely in ACR** using `az acr build`, so no local Docke ![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.3 Configure Authentication (Optional) @@ -409,7 +416,7 @@ The images are **built remotely in ACR** using `az acr build`, so no local Docke 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/TroubleShootingSteps.md b/docs/TroubleShootingSteps.md index 7a52ca3f5..1cf473896 100644 --- a/docs/TroubleShootingSteps.md +++ b/docs/TroubleShootingSteps.md @@ -4,589 +4,166 @@ When deploying Azure resources, you may come across different error codes that s Use these as quick reference guides to unblock your deployments. -## Error Codes +## ⚑ Most Frequently Encountered Errors + +| Error Code | Common Cause | Full Details | +|------------|--------------|--------------| +| **InsufficientQuota** | Not enough quota available in subscription | [View Solution](#quota--capacity-limitations) | +| **MissingSubscriptionRegistration** | Required feature not registered in subscription | [View Solution](#subscription--access-issues) | +| **ResourceGroupNotFound** | RG doesn't exist or using old .env file | [View Solution](#resource-group--deployment-management) | +| **DeploymentModelNotSupported** | Model not available in selected region | [View Solution](#regional--location-issues) | +| **DeploymentNotFound** | Deployment record not found or was deleted | [View Solution](#resource-group--deployment-management) | +| **ResourceNotFound** | Resource does not exist or cannot be found | [View Solution](#resource-identification--references) | +| **SpecialFeatureOrQuotaIdRequired** | Subscription lacks access to specific model | [View Solution](#subscription--access-issues) | +| **ContainerAppOperationError** | Improperly built container image | [View Solution](#miscellaneous) | +| **ServiceUnavailable** | Service not available in selected region | [View Solution](#regional--location-issues) | +| **BadRequest - DatabaseAccount is in a failed provisioning state** | Previous deployment failed | [View Solution](#resource-state--provisioning) | +| **Unauthorized - Operation cannot be completed
without additional quota** | Insufficient quota for requested operation | [View Solution](#subscription--access-issues) | +| **ResourceGroupBeingDeleted** | Resource group deletion in progress | [View Solution](#resource-group--deployment-management) | +| **FlagMustBeSetForRestore** | Soft-deleted resource requires restore flag or purge | [View Solution](#miscellaneous) | +| **ParentResourceNotFound** | Parent resource does not exist or cannot be found | [View Solution](#resource-identification--references) | +| **AccountProvisioningStateInvalid** | Resource used before provisioning completed | [View Solution](#resource-state--provisioning) | +| **InternalSubscriptionIsOverQuotaForSku** | Subscription quota exceeded for the requested SKU | [View Solution](#quota--capacity-limitations) | +| **InvalidResourceGroup** | Invalid resource group configuration | [View Solution](#resource-group--deployment-management) | +| **RequestDisallowedByPolicy** | Azure Policy blocking the requested operation | [View Solution](#subscription--access-issues) | + +## πŸ“– Table of Contents + +- [Subscription & Access Issues](#subscription--access-issues) +- [Quota & Capacity Limitations](#quota--capacity-limitations) +- [Regional & Location Issues](#regional--location-issues) +- [Resource Naming & Validation](#resource-naming--validation) +- [Resource Identification & References](#resource-identification--references) +- [Network & Infrastructure Configuration](#network--infrastructure-configuration) +- [Configuration & Property Errors](#configuration--property-errors) +- [Resource State & Provisioning](#resource-state--provisioning) +- [Miscellaneous](#miscellaneous) + +## Subscription & Access Issues + +| Issue/Error Code | Description | Steps to Resolve | +|-----------|-------------|------------------| +| **ReadOnlyDisabledSubscription** | Subscription is disabled or in read-only state | | +| **MissingSubscriptionRegistration/
AllowBringYourOwnPublicIpAddress** | Required feature not registered in subscription | **Enable `AllowBringYourOwnPublicIpAddress` Feature**

Before deploying the resources, you may need to enable the **Bring Your Own Public IP Address** feature in Azure. This is required only once per subscription.

**Steps:**
πŸ’‘ Note: Feature registration may take several minutes to complete. This needs to be done only once per Azure subscription. | +| **Unauthorized - Operation cannot be completed without additional quota** | Insufficient quota for requested operation | | +| **CrossTenantDeploymentNotPermitted** | Deployment across different Azure AD tenants not allowed | | +| **RequestDisallowedByPolicy** | Azure Policy blocking the requested operation | | +| **SpecialFeatureOrQuotaIdRequired** | Subscription lacks access to specific Azure OpenAI models | This error occurs when your subscription does not have access to certain Azure OpenAI models.

**Example error message:**
`SpecialFeatureOrQuotaIdRequired: The current subscription does not have access to this model 'Format:OpenAI,Name:o3,Version:2025-04-16'.`

**Resolution:**
To gain access, submit a request using the official form:
πŸ‘‰ [Azure OpenAI Model Access Request](https://customervoice.microsoft.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR7en2Ais5pxKtso_Pz4b1_xUQ1VGQUEzRlBIMVU2UFlHSFpSNkpOR0paRSQlQCN0PWcu)

You'll need to use this form if you require access to the following restricted models:
Once your request is approved, redeploy your resource. | +| **ResourceProviderError** | Resource provider not registered in subscription | | + +-------------------------------- + +## Quota & Capacity Limitations + +| Issue/Error Code | Description | Steps to Resolve | +|-----------------|-------------|------------------| +| **InternalSubscriptionIsOverQuotaForSku/
ManagedEnvironmentProvisioningError** | Subscription quota exceeded for the requested SKU | Quotas are applied per resource group, subscriptions, accounts, and other scopes. For example, your subscription might be configured to limit the number of vCPUs for a region. If you attempt to deploy a virtual machine with more vCPUs than the permitted amount, you receive an error that the quota was exceeded.

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

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

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

**Common causes:**

**Resolution:**

**Reference:**
| +| **InsufficientQuota** | Not enough quota available in subscription | | +| **MaxNumberOfRegionalEnvironmentsInSubExceeded** | Maximum Container App Environments limit reached for region |This error occurs when you attempt to create more **Azure Container App Environments** than the regional quota limit allows for your subscription. Each Azure region has a specific limit on the number of Container App Environments that can be created per subscription.

**Common Causes:**

**Resolution:**

**Reference:**
| +| **SkuNotAvailable** | Requested SKU not available in selected location or zone | This error occurs when the resource SKU you've selected (such as VM size) isn't available for the target location or availability zone.

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

**Resolution:**

**Reference:**
| +| **Conflict - No available instances to satisfy this request** | Azure App Service has insufficient capacity in the region | This error occurs when Azure App Service doesn't have enough available compute instances in the selected region to provision or scale your app.

**Common Causes:**

**Resolution:**

**Reference:** [Azure App Service Plans](https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans) | + +-------------------------------- + +## Resource Group & Deployment Management + +| Issue/Error Code | Description | Steps to Resolve | +|-----------------|-------------|------------------| +| **ResourceGroupNotFound** | Specified resource group does not exist | **Option 1:**

**Option 2:**
| +| **ResourceGroupBeingDeleted** | Resource group is currently being deleted | **Steps:**
| +| **DeploymentActive** | Another deployment is already in progress in this resource group | | +| **DeploymentCanceled** | Deployment was canceled before completion |
πŸ’‘ **Note:** DeploymentCanceled is a wrapper error β€” check inner errors in deployment logs | +| **DeploymentCanceled(user.canceled)** | User manually canceled the deployment | | +| **DeploymentNotFound** | Deployment record not found or was deleted | | +| **ResourceGroupDeletionTimeout** | Resource group deletion exceeded timeout limit | | + +-------------------------------- + +## Regional & Location Issues + +| Issue/Error Code | Description | Steps to Resolve | +|-----------------|-------------|------------------| +| **LocationNotAvailableForResourceType** | Resource type not supported in selected region | This error occurs when you attempt to deploy a resource to a region that does not support that specific resource type or SKU.

**Resolution:**
| +| **InvalidResourceLocation** | Cannot change region for already deployed resources | This error occurs when you attempt to modify the location/region of a resource that has already been deployed. Azure resources **cannot change regions** after creation.

**Resolution:**

⚠️ **Important:** Backup critical data before deleting resources.

**Reference:** [Move Azure resources across regions](https://learn.microsoft.com/en-us/azure/resource-mover/overview) | +| **ServiceUnavailable/ResourceNotFound** | Service unavailable or restricted in selected region | | +| **ResourceOperationFailure/
ProvisioningDisabled** | Resource provisioning restricted or disabled in region | | +| **RedundancyConfigurationNotAvailableInRegion** | Redundancy configuration not supported in selected region | | +| **NoRegisteredProviderFound** | Unsupported API version for resource type in specified location | This error occurs when you attempt to deploy an Azure resource using an **API version that is not supported** for the specified resource type and location.

**Example error message:**
`NoRegisteredProviderFound: No registered resource provider found for location 'westeurope' and API version '2020-06-30' for type 'searchServices'. The supported api-versions are '2014-07-31-Preview, 2015-02-28, 2015-08-19, 2019-10-01-Preview, 2020-03-13, 2020-08-01, 2020-08-01-Preview, 2021-04-01-Preview, 2021-06-06-Preview, 2022-09-01, 2023-11-01, 2024-03-01-Preview, 2024-06-01-Preview, 2025-02-01-Preview, 2025-05-01'.`

**Common causes:**

**Resolution:**

**Reference:**
| + +-------------------------------- + +## Resource Naming & Validation + +| Issue/Error Code | Description | Steps to Resolve | +|-----------------|-------------|------------------| +| **ResourceNameInvalid** | Resource name violates naming convention rules | | +| **Workspace Name - InvalidParameter** | Workspace name does not meet required format | To avoid this errors in workspace ID follow below rules:
| +| **VaultNameNotValid** | Key Vault name does not meet naming requirements | In this template Vault name will be unique everytime, but if you trying to hard code the name then please make sure below points:
| +| **BadRequest: Dns record under zone Document is already taken** | DNS record name already in use | This error can occur only when user hardcoding the CosmosDB Service name. To avoid this you can try few below suggestions:
| + +--------------------------------- + +## Resource Identification & References + +| Issue/Error Code | Description | Steps to Resolve | +|-----------------|-------------|------------------| +| **LinkedInvalidPropertyId/
ResourceNotFound/
DeploymentOutputEvaluationFailed/
CanNotRestoreANonExistingResource/
The language expression property array index is out of bounds** | Invalid or non-existent resource ID reference | | +| **ParentResourceNotFound** | Parent resource does not exist or cannot be found | | +| **PrincipalNotFound** | Principal ID does not exist in Azure AD tenant | This error occurs when the **principal ID** (Service Principal, User, or Group) specified in a role assignment or deployment does not exist in the Azure Active Directory tenant. It can also happen due to **replication delays** right after creating a new principal.

**Example causes:**

**How to fix:**
| +| **SubscriptionDoesNotHaveServer** | Referenced SQL Server does not exist in subscription | This issue happens when you try to reference an **Azure SQL Server** (`Microsoft.Sql/servers`) that does not exist in the selected subscription.

**It can occur if:**

**Reproduce:**
Run an Azure CLI command with a non-existent server name:
`az sql db list --server sql-doesnotexist --resource-group myResourceGroup`
or
`az sql server show --name sql-caqfrhxr4i3hyj --resource-group myResourceGroup`

**Resolution:**
| + +--------------------------------- + +## Network & Infrastructure Configuration + +| Issue/Error Code | Description | Steps to Resolve | +|-----------------|-------------|------------------| +| **NetcfgSubnetRangeOutsideVnet** | Subnet IP range outside virtual network address space | | +| **DisableExport_PublicNetworkAccessMustBeDisabled** | Public network access must be disabled when export is disabled | | +| **VMSizeIsNotPermittedToEnableAcceleratedNetworking** | VM size does not support accelerated networking | This error occurs when you attempt to enable accelerated networking on a VM size that does not support it.

**Note:** This solution uses `Standard_D2s_v5` which **fully supports accelerated networking**, so this error should not occur with the default configuration.

**How to reproduce:**

**Resolution:**
| +**NetworkSecurityGroupNotCompliantForAzureBastionSubnet** / **SecurityRuleParameterContainsUnsupportedValue** | NSG rules blocking required Azure Bastion ports | This error occurs when the Network Security Group (NSG) attached to `AzureBastionSubnet` explicitly denies inbound TCP ports 443 and/or 4443, which Azure Bastion requires for management and tunneling.

**How to reproduce:**

**Resolution:**
| +| **RouteTableCannotBeAttachedForAzureBastionSubnet** | Route table attached to Azure Bastion subnet | This error occurs because Azure Bastion subnet (`AzureBastionSubnet`) has a platform restriction that prevents route tables from being attached.

**How to reproduce:**

**Resolution:**
| -
-ReadOnlyDisabledSubscription - -- Check if you have an active subscription before starting the deployment. - -
- -
- MissingSubscriptionRegistration/ AllowBringYourOwnPublicIpAddress - - -Enable `AllowBringYourOwnPublicIpAddress` Feature - -Before deploying the resources, you may need to enable the **Bring Your Own Public IP Address** feature in Azure. This is required only once per subscription. - -### Steps - -1. **Run the following command to register the feature:** - - ```bash - az feature register --namespace Microsoft.Network --name AllowBringYourOwnPublicIpAddress - ``` - -2. **Wait for the registration to complete.** - You can check the status using: - - ```bash - az feature show --namespace Microsoft.Network --name AllowBringYourOwnPublicIpAddress --query properties.state - ``` - -3. **The output should show:** - "Registered" - -4. **Once the feature is registered, refresh the provider:** - - ```bash - az provider register --namespace Microsoft.Network - ``` - - πŸ’‘ Note: Feature registration may take several minutes to complete. This needs to be done only once per Azure subscription. - -
- -
-ResourceGroupNotFound - -## Option 1 -### Steps - -1. Go to [Azure Portal](https://portal.azure.com/#home). - -2. Click on the **"Resource groups"** option available on the Azure portal home page. -![alt text](../docs/images/AzureHomePage.png) - -3. In the Resource Groups search bar, search for the resource group you intend to target for deployment. If it exists, you can proceed with using it. -![alt text](../docs/images/resourcegroup1.png) - - ## Option 2 - -- This error can occur if you deploy the template using the same .env file - from a previous deployment. -- To avoid this issue, create a new environment before redeploying. -- You can use the following command to create a new environment: - ``` - azd env new - ``` -
-
-ResourceGroupBeingDeleted - -To prevent this issue, please ensure that the resource group you are targeting for deployment is not currently being deleted. You can follow steps to verify resource group is being deleted or not. -### Steps: -1. Go to [Azure Portal](https://portal.azure.com/#home) -2. Go to resource group option and search for targeted resource group -3. If Targeted resource group is there and deletion for this is in progress, it means u cannot use this, you can create new or use any other resource group - -
- -
-InternalSubscriptionIsOverQuotaForSku/ManagedEnvironmentProvisioningError - -Quotas are applied per resource group, subscriptions, accounts, and other scopes. For example, your subscription might be configured to limit the number of vCPUs for a region. If you attempt to deploy a virtual machine with more vCPUs than the permitted amount, you receive an error that the quota was exceeded. -For PowerShell, use the `Get-AzVMUsage` cmdlet to find virtual machine quotas. -```ps -Get-AzVMUsage -Location "West US" -``` -based on available quota you can deploy application otherwise, you can request for more quota -
- -
-InsufficientQuota - -- Check if you have sufficient quota available in your subscription before deployment. -- To verify, refer to the [quota_check](../docs/quota_check.md) file for details. - -
- -
-DeploymentModelNotSupported/ ServiceModelDeprecated/ InvalidResourceProperties - - - The updated model may not be supported in the selected region. Please verify its availability in the [Azure AI Foundry models](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/concepts/models?tabs=global-standard%2Cstandard-chat-completions) document. - -
-
-LinkedInvalidPropertyId/ ResourceNotFound/DeploymentOutputEvaluationFailed/ CanNotRestoreANonExistingResource / The language expression property array index is out of bounds - -- Before using any resource ID, ensure it follows the correct format. -- Verify that the resource ID you are passing actually exists. -- Make sure there are no typos in the resource ID. -- Verify that the provisioning state of the existing resource is `Succeeded` by running the following command to avoid this error while deployment or restoring the resource. - - ``` - az resource show --ids --query "properties.provisioningState" - ``` -- Sample Resource IDs format - - Log Analytics Workspace Resource ID - ``` - /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName} - ``` - - Azure AI Foundry Project Resource ID - ``` - /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.MachineLearningServices/workspaces/{name} - ``` -- You may encounter the error `The language expression property array index '8' is out of bounds` if the resource ID is incomplete. Please ensure your resource ID is correct and contains all required information, as shown in sample resource IDs. - -- For more information refer [Resource Not Found errors solutions](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/error-not-found?tabs=bicep) - -
-
-ResourceNameInvalid - -- Ensure the resource name is within the allowed length and naming rules defined for that specific resource type, you can refer [Resource Naming Convention](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules) document. - -
-
-ServiceUnavailable/ResourceNotFound - - - Regions are restricted to guarantee compatibility with paired regions and replica locations for data redundancy and failover scenarios based on articles [Azure regions list](https://learn.microsoft.com/en-us/azure/reliability/regions-list) and [Azure Database for MySQL Flexible Server - Azure Regions](https://learn.microsoft.com/azure/mysql/flexible-server/overview#azure-regions). - - - You can request more quota, refer [Quota Request](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase) Documentation - - -
-
-Workspace Name - InvalidParameter - - To avoid this errors in workspace ID follow below rules. -1. Must start and end with an alphanumeric character (letter or number). -2. Allowed characters: - `a–z` - `0–9` - `- (hyphen)` -3. Cannot start or end with a hyphen -. -4. No spaces, underscores (_), periods (.), or special characters. -5. Must be unique within the Azure region & subscription. -6. Length: 3–33 characters (for AML workspaces). -
-
-BadRequest: Dns record under zone Document is already taken - -This error can occur only when user hardcoding the CosmosDB Service name. To avoid this you can try few below suggestions. -- Verify resource names are globally unique. -- If you already created an account/resource with same name in another subscription or resource group, check and delete it before reusing the name. -- By default in this template we are using unique prefix with every resource/account name to avoid this kind for errors. -
-
-NetcfgSubnetRangeOutsideVnet - -- Ensure the subnet’s IP address range falls within the virtual network’s address space. -- Always validate that the subnet CIDR block is a subset of the VNet range. -- For Azure Bastion, the AzureBastionSubnet must be at least /27. -- Confirm that the AzureBastionSubnet is deployed inside the VNet. -
-
-DisableExport_PublicNetworkAccessMustBeDisabled - -- Check container source: Confirm whether the deployment is using a Docker image or Azure Container Registry (ACR). -- Verify ACR configuration: If ACR is included, review its settings to ensure they comply with Azure requirements. -- Check export settings: If export is disabled in ACR, make sure public network access is also disabled. -- Dedeploy after fix: Correct the configuration and redeploy. This will prevent the Conflict error during deployment. -- For more information refer [ACR Data Loss Prevention](https://learn.microsoft.com/en-us/azure/container-registry/data-loss-prevention) document. -
-
-AccountProvisioningStateInvalid - -- The AccountProvisioningStateInvalid error occurs when you try to use resources while they are still in the Accepted provisioning state. -- This means the deployment has not yet fully completed. -- To avoid this error, wait until the provisioning state changes to Succeeded. -- Only use the resources once the deployment is fully completed. -
-
-VaultNameNotValid - - In this template Vault name will be unique everytime, but if you trying to hard code the name then please make sure below points. - 1. Check name length - - Ensure the Key Vault name is between 3 and 24 characters. - 2. Validate allowed characters - - The name can only contain letters (a–z, A–Z) and numbers (0–9). - - Hyphens are allowed, but not at the beginning or end, and not consecutive (--). -3. Ensure proper start and end - - The name must start with a letter. - - The name must end with a letter or digit (not a hyphen). -4. Test with a new name - - Example of a valid vault name: - βœ… `cartersaikeyvault1` - βœ… `securevaultdemo` - βœ… `kv-project123` -
-
-DeploymentCanceled - - There might be multiple reasons for this error you can follow below steps to troubleshoot. - 1. Check deployment history - - Go to Azure Portal β†’ Resource Group β†’ Deployments. - - Look at the detailed error message for the deployment that was canceled β€” this will show which resource failed and why. - 2. Identify the root cause - - A DeploymentCanceled usually means: - - A dependent resource failed to deploy. - - A validation error occurred earlier. - - A manual cancellation was triggered. - - Expand the failed deployment logs for inner error messages. -3. Validate your template (ARM/Bicep) - Run: - ``` - az deployment group validate --resource-group --template-file main.bicep - ``` -4. Check resource limits/quotas - - Ensure you have not exceeded quotas (vCPUs, IPs, storage accounts, etc.), which can silently cause cancellation. -5. Fix the failed dependency - - If a specific resource shows BadRequest, Conflict, or ValidationError, resolve that first. - - Re-run the deployment after fixing the root cause. -6. Retry deployment - Once corrected, redeploy with: - ``` - az deployment group create --resource-group --template-file main.bicep - ``` -Essentially: DeploymentCanceled itself is just a wrapper error β€” you need to check inner errors in the deployment logs to find the actual failure. -
-
-LocationNotAvailableForResourceType - -- You may encounter a LocationNotAvailableForResourceType error if you set the secondary location to 'Australia Central' in the main.bicep file. -- This happens because 'Australia Central' is not a supported region for that resource type. -- Always refer to the README file or Azure documentation to check the list of supported regions. -- Update the deployment with a valid supported region to resolve the issue. - -
- -
-InvalidResourceLocation - -- You may encounter an InvalidResourceLocation error if you change the region for Cosmos DB or the Storage Account (secondary location) multiple times in the main.bicep file and redeploy. -- Azure resources like Cosmos DB and Storage Accounts do not support changing regions after deployment. -- If you need to change the region again, first delete the existing deployment. -- Then redeploy the resources with the updated region configuration. - -
- -
- -DeploymentActive - -- This issue occurs when a deployment is already in progress and another deployment is triggered in the same resource group, causing a DeploymentActive error. -- Cancel the ongoing deployment before starting a new one. -- Do not initiate a new deployment in the same resource group until the previous one is completed. -
- -
-ResourceOperationFailure/ProvisioningDisabled - - - This error occurs when provisioning of a resource is restricted in the selected region. - It usually happens because the service is not available in that region or provisioning has been temporarily disabled. - - - Regions are restricted to guarantee compatibility with paired regions and replica locations for data redundancy and failover scenarios based on articles [Azure regions list](https://learn.microsoft.com/en-us/azure/reliability/regions-list) and [Azure Database for MySQL Flexible Server - Azure Regions](https://learn.microsoft.com/azure/mysql/flexible-server/overview#azure-regions). - -- If you need to use the same region, you can request a quota or provisioning exception. - Refer [Quota Request](https://docs.microsoft.com/en-us/azure/sql-database/quota-increase-request) for more details. - -
- -
-MaxNumberOfRegionalEnvironmentsInSubExceeded - -- This error occurs when you try to create more than the allowed number of **Azure Container App Environments (ACA Environments)** in the same region for a subscription. -- For example, in **Sweden Central**, only **1 Container App Environment** is allowed per subscription. - -The subscription 'xxxx-xxxx' cannot have more than 1 Container App Environments in Sweden Central. - -- To fix this, you can: - - Deploy the Container App Environment in a **different region**, OR - - Request a quota increase via Azure Support β†’ [Quota Increase Request](https://go.microsoft.com/fwlink/?linkid=2208872) - -
- -
-Unauthorized - Operation cannot be completed without additional quota - -- You can check your quota usage using `az vm list-usage`. - - ``` - az vm list-usage --location "" -o table - ``` -- To Request more quota refer [VM Quota Request](https://techcommunity.microsoft.com/blog/startupsatmicrosoftblog/how-to-increase-quota-for-specific-types-of-azure-virtual-machines/3792394). - -
- -
ParentResourceNotFound - -- You can refer to the [Parent Resource Not found](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/error-parent-resource?tabs=bicep) documentation if you encounter this error. - -
- -
ResourceProviderError - -- This error occurs when the resource provider is not registered in your subscription. -- To register it, refer to [Register Resource Provider](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/error-register-resource-provider?tabs=azure-cli) documentation. - -
- -
Conflict - Cannot use the SKU Basic with File Change Audit for site. - -- This error happens because File Change Audit logs aren’t supported on Basic SKU App Service Plans. - -- Upgrading to Premium/Isolated SKU (supports File Change Audit), or - -- Disabling File Change Audit in Diagnostic Settings if you must stay on Basic. -- Always cross-check the [supported log types](https://aka.ms/supported-log-types) - before adding diagnostic logs to your Bicep templates. - -
- -
- -AccountPropertyCannotBeUpdated - -- The property **`isHnsEnabled`** (Hierarchical Namespace for Data Lake Gen2) is **read-only** and can only be set during **storage account creation**. -- Once a storage account is created, this property **cannot be updated**. -- Trying to update it via ARM template, Bicep, CLI, or Portal will fail. - -- **Resolution** -- Create a **new storage account** with `isHnsEnabled=true` if you require hierarchical namespace. -- Migration may be needed if you already have data. -- Refer to [Storage Account Update Restrictions](https://aka.ms/storageaccountupdate) for more details. - -
- -
InvalidRequestContent - -- The deployment values either include values that aren't recognized, or required values are missing. Confirm the values for your resource type. -- You can refer [Invalid Request Content error](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/common-deployment-errors#:~:text=InvalidRequestContent,Template%20reference) documentation. - -
- -
ReadOnlyDisabledSubscription - -- Depending on the type of the Azure Subscription, the expiration date might have been reached. - -- You have to activate the Azure Subscription before creating any Azure resource. -- You can refer [Reactivate a disabled Azure subscription](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/subscription-disabled) Documentation. - -
- - -
SkuNotAvailable - -- You receive this error in the following scenarios: - - When the resource SKU you've selected, such as VM size, isn't available for a location or zone. - - If you're deploying an Azure Spot VM or Spot scale set instance, and there isn't any capacity for Azure Spot in this location. For more information, see Spot error messages. -
- -
CrossTenantDeploymentNotPermitted - -- Check tenant match: Ensure your deployment identity (user/SP) and the target resource group are in the same tenant. - ``` - az account show - az group show --name - ``` - -- Verify pipeline/service principal: If using CI/CD, confirm the service principal belongs to the same tenant and has permissions on the resource group. - -- Avoid cross-tenant references: Make sure your Bicep doesn’t reference subscriptions, resource groups, or resources in another tenant. - -- Test minimal deployment: Deploy a simple resource to the same resource group to confirm identity and tenant are correct. - -- Guest/external accounts: Avoid using guest users from other tenants; use native accounts or SPs in the tenant. - -
- -
RequestDisallowedByPolicy - -- This typically indicates that an Azure Policy is preventing the requested action due to policy restrictions in your subscription. - -- For more details and guidance on resolving this issue, please refer to the official Microsoft documentation: [RequestDisallowedByPolicy](https://learn.microsoft.com/en-us/troubleshoot/azure/azure-kubernetes/create-upgrade-delete/error-code-requestdisallowedbypolicy) - -
- -
-FlagMustBeSetForRestore/NameUnavailable/CustomDomainInUse - -- This error occurs when you try to deploy a Cognitive Services resource that was **soft-deleted** earlier. -- Azure requires you to explicitly set the **`restore` flag** to `true` if you want to recover the soft-deleted resource. -- If you don’t want to restore the resource, you must **purge the deleted resource** first before redeploying. -Example causes: -- Trying to redeploy a Cognitive Services account with the same name as a previously deleted one. -- The deleted resource still exists in a **soft-delete retention state**. -**How to fix:** -1. If you want to restore β†’ add `"restore": true` in your template properties. -2. If you want a fresh deployment β†’ purge the resource using: - ```bash - az cognitiveservices account purge \ - --name \ - --resource-group \ - --location - ``` -For more details, refer to [Soft delete and resource restore](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/delete-resource-group?tabs=azure-powershell). -
- -
-PrincipalNotFound - -- This error occurs when the **principal ID** (Service Principal, User, or Group) specified in a role assignment or deployment does not exist in the Azure Active Directory tenant. -- It can also happen due to **replication delays** right after creating a new principal. -**Example causes:** -- The specified **Object ID** is invalid or belongs to another tenant. -- The principal was recently created but Azure AD has not yet replicated it. -- Attempting to assign a role to a non-existing or deleted Service Principal/User/Group. -**How to fix:** -1. Verify that the **principal ID is correct** and exists in the same directory/tenant. - ```bash - az ad sp show --id - ``` -2. If the principal was just created, wait a few minutes and retry. -3. Explicitly set the principalType property (ServicePrincipal, User, or Group) in your ARM/Bicep template to avoid replication delays. -4. If the principal does not exist, create it again before assigning roles. -For more details, see [Azure PrincipalType documentation](https://learn.microsoft.com/en-us/azure/role-based-access-control/troubleshooting?tabs=bicep) -
-
-RedundancyConfigurationNotAvailableInRegion - -- This issue happens when you try to create a **Storage Account** with a redundancy configuration (e.g., `Standard_GRS`) that is **not supported in the selected Azure region**. -- Example: Creating a storage account with **GRS** in **italynorth** will fail with this error. -```bash -az storage account create -n mystorageacct123 -g myResourceGroup -l italynorth --sku Standard_GRS --kind StorageV2 -``` -- To check supported SKUs for your region: -```bash -az storage account list-skus -l italynorth -o table -``` -Use a supported redundancy option (e.g., Standard_LRS) in the same region -Or deploy the Storage Account in a region that supports your chosen redundancy. -For more details, refer to [Azure Storage redundancy documentation](https://learn.microsoft.com/en-us/azure/storage/common/storage-redundancy?utm_source=chatgpt.com). -
- -
DeploymentNotFound - -- This issue occurs when the user deletes a previous deployment along with the resource group (RG), and then redeploys the same RG with the same environment name but in a different location. - -- To avoid the DeploymentNotFound error, Do not change the location when redeploying a deleted RG, or Use new names for the RG and environment during redeployment. -
- -
DeploymentCanceled(user.canceled) - -- Indicates the deployment was manually canceled by the user (Portal, CLI, or pipeline). +--------------------------------- -- Check deployment history and logs to confirm who/when it was canceled. +## Configuration & Property Errors -- If accidental, retry the deployment. - -- For pipelines, ensure no automation or timeout is triggering cancellation. - -- Use deployment locks or retry logic to prevent accidental cancellations. - -
- -
ResourceGroupDeletionTimeout - -- Some resources in the resource group may be stuck deleting or have dependencies; check RG resources and status. - -- Ensure no resource locks or Azure Policies are blocking deletion. - -- Retry deletion via CLI/PowerShell `(az group delete --name --yes --no-wait)`. - -- Check Activity Log to identify failing resources; escalate to Azure Support if deletion is stuck. - -
- -
-SubscriptionDoesNotHaveServer - -- This issue happens when you try to reference an **Azure SQL Server** (`Microsoft.Sql/servers`) that does not exist in the selected subscription. -- It can occur if: - - The SQL server name is typed incorrectly. - - The SQL server was **deleted** but is still being referenced. - - You are working in the **wrong subscription context**. - - The server exists in a **different subscription/tenant** where you don’t have access. - -**Reproduce:** -1. Run an Azure CLI command with a non-existent server name: -```bash - az sql db list --server sql-doesnotexist --resource-group myResourceGroup -``` - - or - -```bash - az sql server show --name sql-caqfrhxr4i3hyj --resource-group myResourceGroup - -``` - -Resolution: - -Verify the SQL Server name exists in your subscription: - -```bash - az sql server list --output table -``` -Make sure you are targeting the correct subscription: +| Issue/Error Code | Description | Steps to Resolve | +|-----------------|-------------|------------------| +| **InvalidRequestContent** | Deployment contains unrecognized or missing required values | | +| **Conflict - Cannot use the SKU Basic with File Change Audit for site** | File Change Audit not supported on Basic SKU | | +| **AccountPropertyCannotBeUpdated** | Read-only property cannot be modified after creation | The property **`isHnsEnabled`** (Hierarchical Namespace for Data Lake Gen2) is **read-only** and can only be set during **storage account creation**. Once a storage account is created, this property **cannot be updated**. Trying to update it via ARM template, Bicep, CLI, or Portal will fail.

**Resolution:**
| +| **Conflict - Local authentication is disabled** | App Configuration store has local authentication disabled but application is using local auth mode | This error occurs when your Azure App Configuration store has **local authentication disabled** (`disableLocalAuth: true`) but your application is trying to access it using **connection strings or access keys** instead of **Azure AD/Managed Identity authentication**.

**Example error message:**
`The operation cannot be performed because the configuration store is using local authentication mode and local authentication is disabled. To enable access to data plane resources while local authentication is disabled, please use pass-through authentication mode.`

**Common causes:**

**Resolution:**

**Reference:**
| +| **PropertyChangeNotAllowed** | Immutable VM property cannot be changed after creation | This error occurs when you attempt to modify an immutable property (such as `osProfile.adminUsername`) on an existing VM.

**Cause (Azure Limitation):**
Once a VM is created, the `osProfile.adminUsername` property is immutable and cannot be changed. If you modify the VM username or password in the template and redeploy, this issue will occur.

**Resolution:**

**Note:** Some VM properties are immutable by design. Always plan credential and configuration changes before initial deployment. | -```bash - az account show - az account set --subscription -``` -If the server was deleted, either restore it (if possible) or update references to use a valid existing server. - -
- - -
DeploymentCanceled(user.canceled) - -- Indicates the deployment was manually canceled by the user (Portal, CLI, or pipeline). - -- Check deployment history and logs to confirm who/when it was canceled. - -- If accidental, retry the deployment. - -- For pipelines, ensure no automation or timeout is triggering cancellation. - -- Use deployment locks or retry logic to prevent accidental cancellations. - -
-
BadRequest - DatabaseAccount is in a failed provisioning state because the previous attempt to create it was not successful -- This error occurs when a user attempts to redeploy a resource that previously failed to provision. +---------------------------------- -- To resolve the issue, delete the failed deployment first, then start a new deployment. +## Resource State & Provisioning -- For guidance on deleting a resource from a Resource Group, refer to the following link: [Delete an Azure Cosmos DB account](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/manage-with-powershell#delete-account:~:text=%3A%24enableMultiMaster-,Delete%20an%20Azure%20Cosmos%20DB%20account,-This%20command%20deletes) +| Issue/Error Code | Description | Steps to Resolve | +|-----------------|-------------|------------------| +| **AccountProvisioningStateInvalid** | Resource used before provisioning completed | | +| **BadRequest - DatabaseAccount is in a failed provisioning state because the previous attempt to create it was not successful** | Database account failed to provision previously | | +| **ServiceDeleting** | Cannot provision service because deletion is still in progress | This error occurs when you attempt to create an Azure Search service with the same name as one that is currently being deleted. Azure Search services have a **soft-delete period** during which the service name remains reserved.

**Common causes:**

**Resolution:**
| +| **FailedIdentityOperation / ManagedEnvironmentScheduledForDelete** | Identity operation failed due to pending delete or resource conflict | This error occurs when you attempt to create or update an Azure Container Apps Managed Environment while it has a **pending delete operation** or the resource already exists in a conflicting state.

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

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

**Common causes:**

**Resolution:**

**Reference:**
| +| **BadRequest - Parent account does not provision correctly** | Parent AI Services/Cognitive Services account failed to provision | This error occurs when a **child resource** (such as an AI project, model deployment, or other dependent resource) attempts to be created on a **parent Cognitive Services/AI Services account** that has **failed to provision** or is in an incomplete state.

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

**Common causes:**

**Resolution:**

**Reference:**
| +--------------------------------- -
+## Miscellaneous -
+| Issue/Error Code | Description | Steps to Resolve | +|-------------|-------------|------------------| +| **DeploymentModelNotSupported/
ServiceModelDeprecated/
InvalidResourceProperties** | Model not supported or deprecated in selected region | | +| **FlagMustBeSetForRestore/
NameUnavailable/
CustomDomainInUse** | Soft-deleted resource requires restore flag or purge | This error occurs when you try to deploy a Cognitive Services resource that was **soft-deleted** earlier. Azure requires you to explicitly set the **`restore` flag** to `true` if you want to recover the soft-deleted resource. If you don't want to restore the resource, you must **purge the deleted resource** first before redeploying.

**Example causes:**

**How to fix:**
| +| **ContainerAppOperationError** | Container image build or deployment issue | | +| **LinkedAuthorizationFailed** | Service principal lacks permission to use a linked resource required for deployment | This error occurs when a service principal doesn't have permission to perform an action on a linked resource that is required for the operation (e.g., cluster creation).

**Common causes:**

**Resolution:**
| +| **ContainerOperationFailure** | Container image or storage resource does not exist | This error occurs when an operation fails because the **specified container resource does not exist**. This can happen with Azure Container Registry images or Azure Storage blob containers.

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

**Common causes:**

**Resolution:**

**Reference:**
| -SpecialFeatureOrQuotaIdRequired -This error occurs when your subscription does not have access to certain Azure OpenAI models. - -**Example error message:** -`SpecialFeatureOrQuotaIdRequired: The current subscription does not have access to this model 'Format:OpenAI,Name:o3,Version:2025-04-16'.` - -**Resolution:** -To gain access, submit a request using the official form: -πŸ‘‰ [Azure OpenAI Model Access Request](https://customervoice.microsoft.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR7en2Ais5pxKtso_Pz4b1_xUQ1VGQUEzRlBIMVU2UFlHSFpSNkpOR0paRSQlQCN0PWcu) - -You’ll need to use this form if you require access to the following restricted models: -- gpt-5 -- o3 -- o3-pro -- deep research -- reasoning summary -- gpt-image-1 - -Once your request is approved, redeploy your resource. - -
- -
-ContainerAppOperationError - -- The error is likely due to an improperly built container image. For resolution steps, refer to the [Azure Container Registry (ACR) – Build & Push Guide](./ACRBuildAndPushGuide.md) - -
+--------------------------------- πŸ’‘ Note: If you encounter any other issues, you can refer to the [Common Deployment Errors](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/common-deployment-errors) documentation. -If the problem persists, you can also raise an bug in our [MACAE Github Issues](https://github.com/microsoft/Multi-Agent-Custom-Automation-Engine-Solution-Accelerator/issues) for further support. +If the problem persists, you can also raise an bug in our [MACAE Github Issues](https://github.com/microsoft/Multi-Agent-Custom-Automation-Engine-Solution-Accelerator/issues) for further support. \ No newline at end of file diff --git a/docs/quota_check.md b/docs/quota_check.md index ba1647868..ab64daf5b 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-5.4 - 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 ``` @@ -16,7 +16,7 @@ az login --use-device-code ### πŸ“Œ Default Models & Capacities: ``` -gpt-5.4-mini:100,gpt-5.4:150 +gpt-5.4:150,gpt-5.4-mini:100 ``` ### πŸ“Œ Default Regions: ``` @@ -43,6 +43,7 @@ australiaeast, eastus2, francecentral, japaneast, norwayeast, swedencentral, uks βœ”οΈ Check specific model(s) in default regions: ``` ./quota_check_params.sh --models gpt-5.4:150 + ./quota_check_params.sh --models gpt-5.4:150 ``` βœ”οΈ Check default models in specific region(s): ``` diff --git a/infra/main.bicep b/infra/main.bicep index 816d8ce6a..8cac79de4 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -524,4 +524,3 @@ output MCP_IMAGE_NAME string = isAvm ? avmDeployment!.outputs.MCP_IMAGE_NAME : b @description('Port the frontend Web App container listens on.') output FRONTEND_WEBSITES_PORT string = isAvm ? avmDeployment!.outputs.FRONTEND_WEBSITES_PORT : bicepDeployment!.outputs.FRONTEND_WEBSITES_PORT - diff --git a/infra/main.json b/infra/main.json index 7d182b912..b1592e60f 100644 --- a/infra/main.json +++ b/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "2110057819441640559" + "templateHash": "17775288733861273358" }, "name": "Multi-Agent Custom Automation Engine - Deployment Router", "description": "Deployment router for the Multi-Agent Custom Automation Engine accelerator. Routes to either the AVM or vanilla Bicep orchestrator and preserves a unified deployment contract." @@ -561,7 +561,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "10080544909430391861" + "templateHash": "6216243501205527784" }, "name": "Multi-Agent Custom Automation Engine - AVM", "description": "AVM orchestrator for the Multi-Agent Custom Automation Engine accelerator. Deploys the same logical resources and preserves the same outputs as infra\\main.bicep using local AVM wrapper modules." @@ -1242,7 +1242,7 @@ "aiSearchIndexNameForRFPSummary": "macae-rfp-summary-index", "aiSearchIndexNameForRFPRisk": "macae-rfp-risk-index", "aiSearchIndexNameForRFPCompliance": "macae-rfp-compliance-index", - "virtualMachineAvailabilityZone": 1, + "virtualMachineAvailabilityZone": -1, "aiFoundryOpenAIEndpoint": "[format('https://{0}.openai.azure.com/', variables('aiFoundryAiServicesResourceName'))]" }, "resources": { @@ -29138,7 +29138,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "10180502016624897684" + "templateHash": "13733831222630151234" } }, "parameters": { @@ -29164,7 +29164,7 @@ "modelName": { "type": "string", "metadata": { - "description": "Required. Model name (e.g., gpt-4o, text-embedding-ada-002)." + "description": "Required. Model name (e.g., gpt-5.4-mini, text-embedding-ada-002)." } }, "modelVersion": { @@ -57521,10 +57521,18 @@ "name": "AZURE_OPENAI_IMAGE_DEPLOYMENT", "value": "[parameters('gptImageModelName')]" }, + { + "name": "AZURE_OPENAI_IMAGE_QUALITY", + "value": "high" + }, { "name": "AZURE_STORAGE_BLOB_URL", "value": "[reference('storage_account').outputs.blobEndpoint.value]" }, + { + "name": "AZURE_STORAGE_IMAGES_CONTAINER", + "value": "generated-images" + }, { "name": "BACKEND_URL", "value": "[format('https://{0}.{1}', variables('containerAppName'), reference('containerAppEnvironment').outputs.defaultDomain.value)]" @@ -60245,7 +60253,6 @@ "value": { "SCM_DO_BUILD_DURING_DEPLOYMENT": "true", "DOCKER_REGISTRY_SERVER_URL": "[format('https://{0}', if(variables('useExistingContainerRegistry'), format('{0}.azurecr.io', variables('existingContainerRegistryName')), reference('container_registry').outputs.loginServer.value))]", - "WEBSITES_PORT": "3000", "WEBSITES_CONTAINER_START_TIME_LIMIT": "1800", "BACKEND_API_URL": "[format('https://{0}', reference('containerApp').outputs.fqdn.value)]", "AUTH_ENABLED": "false", @@ -60253,6 +60260,12 @@ } }, "virtualNetworkSubnetId": "[if(parameters('enablePrivateNetworking'), createObject('value', reference('virtualNetwork').outputs.webserverfarmSubnetResourceId.value), createObject('value', ''))]", + "vnetRouteAllEnabled": { + "value": "[parameters('enablePrivateNetworking')]" + }, + "imagePullTraffic": { + "value": "[parameters('enablePrivateNetworking')]" + }, "publicNetworkAccess": { "value": "Enabled" }, @@ -60267,7 +60280,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "9621160936099183182" + "templateHash": "9504607717010385151" } }, "definitions": { @@ -60805,6 +60818,40 @@ } }, "resources": { + "site": { + "existing": true, + "type": "Microsoft.Web/sites", + "apiVersion": "2025-03-01", + "name": "[parameters('name')]" + }, + "logsConfig": { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2025-03-01", + "name": "[format('{0}/{1}', parameters('name'), 'logs')]", + "properties": { + "applicationLogs": { + "fileSystem": { + "level": "Verbose" + } + }, + "detailedErrorMessages": { + "enabled": true + }, + "failedRequestsTracing": { + "enabled": true + }, + "httpLogs": { + "fileSystem": { + "enabled": true, + "retentionInDays": 1, + "retentionInMb": 35 + } + } + }, + "dependsOn": [ + "appService" + ] + }, "appService": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", @@ -60848,6 +60895,7 @@ "healthCheckPath": "[if(not(empty(parameters('healthCheckPath'))), parameters('healthCheckPath'), null())]", "webSocketsEnabled": "[parameters('webSocketsEnabled')]", "appCommandLine": "[parameters('appCommandLine')]", + "vnetRouteAllEnabled": "[parameters('vnetRouteAllEnabled')]", "acrUseManagedIdentityCreds": "[not(empty(parameters('acrUserManagedIdentityClientId')))]", "acrUserManagedIdentityID": "[if(not(empty(parameters('acrUserManagedIdentityClientId'))), parameters('acrUserManagedIdentityClientId'), null())]" } @@ -60861,35 +60909,6 @@ "name": "appsettings", "properties": "[parameters('appSettings')]", "applicationInsightResourceId": "[if(not(empty(parameters('applicationInsightResourceId'))), parameters('applicationInsightResourceId'), null())]" - }, - { - "name": "logs", - "properties": { - "applicationLogs": { - "fileSystem": { - "level": "Verbose" - } - }, - "detailedErrorMessages": { - "enabled": true - }, - "failedRequestsTracing": { - "enabled": true - }, - "httpLogs": { - "fileSystem": { - "enabled": true, - "retentionInDays": 1, - "retentionInMb": 35 - } - } - } - }, - { - "name": "web", - "properties": { - "vnetRouteAllEnabled": "[parameters('vnetRouteAllEnabled')]" - } } ] }, @@ -76885,7 +76904,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "11051305577029776274" + "templateHash": "1904819476144963209" }, "name": "Multi-Agent Custom Automation Engine - Vanilla Bicep", "description": "Vanilla Bicep orchestrator for the Multi-Agent Custom Automation Engine accelerator. This deployment intentionally excludes WAF features such as private networking, scale-out, redundancy, bastion, and VM resources while keeping router-compatible outputs." @@ -78097,7 +78116,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "10180502016624897684" + "templateHash": "13733831222630151234" } }, "parameters": { @@ -78123,7 +78142,7 @@ "modelName": { "type": "string", "metadata": { - "description": "Required. Model name (e.g., gpt-4o, text-embedding-ada-002)." + "description": "Required. Model name (e.g., gpt-5.4-mini, text-embedding-ada-002)." } }, "modelVersion": { @@ -80082,10 +80101,18 @@ "name": "AZURE_OPENAI_IMAGE_DEPLOYMENT", "value": "[parameters('gptImageModelName')]" }, + { + "name": "AZURE_OPENAI_IMAGE_QUALITY", + "value": "high" + }, { "name": "AZURE_STORAGE_BLOB_URL", "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('module.storage-account.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.blobEndpoint.value]" }, + { + "name": "AZURE_STORAGE_IMAGES_CONTAINER", + "value": "[parameters('storageContainerNameGeneratedImages')]" + }, { "name": "BACKEND_URL", "value": "[format('https://{0}.{1}', variables('backendContainerAppName'), reference(resourceId('Microsoft.Resources/deployments', take(format('module.container-app-environment.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.defaultDomain.value)]" @@ -80499,7 +80526,6 @@ "value": { "SCM_DO_BUILD_DURING_DEPLOYMENT": "true", "DOCKER_REGISTRY_SERVER_URL": "[format('https://{0}', if(variables('useExistingContainerRegistry'), format('{0}.azurecr.io', variables('existingContainerRegistryName')), reference(resourceId('Microsoft.Resources/deployments', take(format('module.container-registry.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.loginServer.value))]", - "WEBSITES_PORT": "3000", "WEBSITES_CONTAINER_START_TIME_LIMIT": "1800", "BACKEND_API_URL": "[format('https://{0}', reference(resourceId('Microsoft.Resources/deployments', take(format('module.backend-container-app.{0}', variables('solutionSuffix')), 64)), '2025-04-01').outputs.fqdn.value)]", "AUTH_ENABLED": "false", diff --git a/src/App/src/hooks/usePlanWebSocket.tsx b/src/App/src/hooks/usePlanWebSocket.tsx index c7256fc13..75c073b17 100644 --- a/src/App/src/hooks/usePlanWebSocket.tsx +++ b/src/App/src/hooks/usePlanWebSocket.tsx @@ -423,4 +423,4 @@ export function usePlanWebSocket({ }, [dispatch, planId, continueWithWebsocketFlow]); } -export default usePlanWebSocket; +export default usePlanWebSocket; \ No newline at end of file diff --git a/src/backend/.env.sample b/src/backend/.env.sample index e1ad44c83..9ad12718c 100644 --- a/src/backend/.env.sample +++ b/src/backend/.env.sample @@ -3,6 +3,7 @@ COSMOSDB_DATABASE=macae COSMOSDB_CONTAINER=memory AZURE_OPENAI_ENDPOINT= +AZURE_OPENAI_MODEL_NAME=gpt-5.4-mini AZURE_OPENAI_DEPLOYMENT_NAME=gpt-5.4-mini AZURE_OPENAI_API_VERSION=2024-12-01-preview AZURE_OPENAI_RAI_DEPLOYMENT_NAME= diff --git a/src/backend/Dockerfile b/src/backend/Dockerfile index d82fe8a8e..aa80db09e 100644 --- a/src/backend/Dockerfile +++ b/src/backend/Dockerfile @@ -8,8 +8,10 @@ ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy WORKDIR /app -# Install the project's dependencies using the lockfile and settings +# 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 uv sync --frozen --no-install-project --no-dev # Backend app setup diff --git a/src/backend/auth/sample_user.py b/src/backend/auth/sample_user.py index 9353bcc1b..e1674a024 100644 --- a/src/backend/auth/sample_user.py +++ b/src/backend/auth/sample_user.py @@ -5,7 +5,6 @@ "Client-Ip": "22.222.222.2222:64379", "Content-Length": "192", "Content-Type": "application/json", - "Cookie": "AppServiceAuthSession=/AuR5ENU+pmpoN3jnymP8fzpmVBgphx9uPQrYLEWGcxjIITIeh8NZW7r3ePkG8yBcMaItlh1pX4nzg5TFD9o2mxC/5BNDRe/uuu0iDlLEdKecROZcVRY7QsFdHLjn9KB90Z3d9ZeLwfVIf0sZowWJt03BO5zKGB7vZgL+ofv3QY3AaYn1k1GtxSE9HQWJpWar7mOA64b7Lsy62eY3nxwg3AWDsP3/rAta+MnDCzpdlZMFXcJLj+rsCppW+w9OqGhKQ7uCs03BPeon3qZOdmE8cOJW3+i96iYlhneNQDItHyQqEi1CHbBTSkqwpeOwWP4vcwGM22ynxPp7YFyiRw/X361DGYy+YkgYBkXq1AEIDZ44BCBz9EEaEi0NU+m6yUOpNjEaUtrJKhQywcM2odojdT4XAY+HfTEfSqp0WiAkgAuE/ueCu2JDOfvxGjCgJ4DGWCoYdOdXAN1c+MenT4OSvkMO41YuPeah9qk9ixkJI5s80lv8rUu1J26QF6pstdDkYkAJAEra3RQiiO1eAH7UEb3xHXn0HW5lX8ZDX3LWiAFGOt5DIKxBKFymBKJGzbPFPYjfczegu0FD8/NQPLl2exAX3mI9oy/tFnATSyLO2E8DxwP5wnYVminZOQMjB/I4g3Go14betm0MlNXlUbU1fyS6Q6JxoCNLDZywCoU9Y65UzimWZbseKsXlOwYukCEpuQ5QPT55LuEAWhtYier8LSh+fvVUsrkqKS+bg0hzuoX53X6aqUr7YB31t0Z2zt5TT/V3qXpdyD8Xyd884PqysSkJYa553sYx93ETDKSsfDguanVfn2si9nvDpvUWf6/R02FmQgXiaaaykMgYyIuEmE77ptsivjH3hj/MN4VlePFWokcchF4ciqqzonmICmjEHEx5zpjU2Kwa+0y7J5ROzVVygcnO1jH6ZKDy9bGGYL547bXx/iiYBYqSIQzleOAkCeULrGN2KEHwckX5MpuRaqTpoxdZH9RJv0mIWxbDA0kwGsbMICQd0ZODBkPUnE84qhzvXInC+TL7MbutPEnGbzgxBAS1c2Ct4vxkkjykOeOxTPxqAhxoefwUfIwZZax6A9LbeYX2bsBpay0lScHcA==", "Disguised-Host": "your_app_service.azurewebsites.net", "Host": "your_app_service.azurewebsites.net", "Max-Forwards": "10", diff --git a/src/backend/common/database/cosmosdb.py b/src/backend/common/database/cosmosdb.py index 9caffa307..4eb7e6fe3 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) - self.logger.debug("delete_team: resolved team document id=%s", team.id if team else None) - 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.py b/src/backend/common/models/messages.py index 18d750fb3..36fbe8ac4 100644 --- a/src/backend/common/models/messages.py +++ b/src/backend/common/models/messages.py @@ -206,6 +206,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/services/team_service.py b/src/backend/services/team_service.py index 6d118c8cf..66a56c967 100644 --- a/src/backend/services/team_service.py +++ b/src/backend/services/team_service.py @@ -105,6 +105,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_cosmosdb.py b/src/tests/backend/common/database/test_cosmosdb.py index 49cdfa590..8aa901fef 100644 --- a/src/tests/backend/common/database/test_cosmosdb.py +++ b/src/tests/backend/common/database/test_cosmosdb.py @@ -701,10 +701,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) @@ -736,9 +737,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) @@ -748,6 +750,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): @@ -763,7 +766,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 82074a87b..67e2a225f 100644 --- a/src/tests/backend/common/database/test_database_factory.py +++ b/src/tests/backend/common/database/test_database_factory.py @@ -54,9 +54,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): @@ -74,19 +74,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: @@ -94,19 +94,22 @@ 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_cosmos_client.user_id = "test_user" mock_config = Mock() mock_config.COSMOSDB_ENDPOINT = "https://test.documents.azure.com:443/" @@ -118,49 +121,95 @@ 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" + + 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/" @@ -168,32 +217,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/" @@ -205,17 +254,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): @@ -234,8 +274,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: @@ -243,58 +283,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: @@ -302,19 +337,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/" @@ -322,28 +359,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/" @@ -354,34 +400,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/" @@ -390,25 +445,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: @@ -416,19 +461,20 @@ 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): """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() @@ -441,8 +487,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", @@ -451,8 +500,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): @@ -467,8 +516,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: @@ -493,11 +542,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): @@ -513,14 +562,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() @@ -531,11 +580,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/" @@ -546,8 +599,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__":