diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 92b76912..5f1feb28 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -46,7 +46,7 @@ jobs: id: quota-check env: AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - GPT_MIN_CAPACITY: "100" + GPT_MIN_CAPACITY: "300" AZURE_REGIONS: ${{ vars.AZURE_REGIONS }} run: | chmod +x infra/scripts/checkquota.sh diff --git a/.github/workflows/job-deploy-linux.yml b/.github/workflows/job-deploy-linux.yml index 002baa7b..7b58ab1e 100644 --- a/.github/workflows/job-deploy-linux.yml +++ b/.github/workflows/job-deploy-linux.yml @@ -34,6 +34,10 @@ on: AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: required: false type: string + AZURE_ENV_GPT_MODEL_CAPACITY: + required: false + type: string + default: "300" outputs: CONTAINER_WEB_APPURL: description: "Container Web App URL" @@ -45,6 +49,7 @@ jobs: environment: production env: AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} + AZURE_ENV_GPT_MODEL_CAPACITY: ${{ inputs.AZURE_ENV_GPT_MODEL_CAPACITY || '300' }} outputs: CONTAINER_WEB_APPURL: ${{ steps.get_output_linux.outputs.CONTAINER_WEB_APPURL }} steps: @@ -247,6 +252,8 @@ jobs: azd env set AZURE_LOCATION="$AZURE_LOCATION" azd env set AZURE_RESOURCE_GROUP="$RESOURCE_GROUP_NAME" azd env set AZURE_ENV_IMAGETAG="$IMAGE_TAG" + # Use available quota capacity discovered by checkquota.sh; fall back to 300 if not set + azd env set AZURE_ENV_GPT_MODEL_CAPACITY="${AZURE_ENV_GPT_MODEL_CAPACITY:-300}" if [[ "$BUILD_DOCKER_IMAGE" == "true" ]]; then ACR_NAME=$(echo "${{ secrets.ACR_TEST_LOGIN_SERVER }}") diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index c33b8c01..33630cca 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -34,6 +34,10 @@ on: AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: required: false type: string + AZURE_ENV_GPT_MODEL_CAPACITY: + required: false + type: string + default: "300" outputs: CONTAINER_WEB_APPURL: description: "Container Web App URL" @@ -45,6 +49,7 @@ jobs: environment: production env: AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} + AZURE_ENV_GPT_MODEL_CAPACITY: ${{ inputs.AZURE_ENV_GPT_MODEL_CAPACITY || '300' }} outputs: CONTAINER_WEB_APPURL: ${{ steps.get_output_windows.outputs.CONTAINER_WEB_APPURL }} steps: @@ -244,6 +249,9 @@ jobs: azd env set AZURE_LOCATION="$env:AZURE_LOCATION" azd env set AZURE_RESOURCE_GROUP="$env:RESOURCE_GROUP_NAME" azd env set AZURE_ENV_IMAGETAG="$env:IMAGE_TAG" + # Use available quota capacity discovered by checkquota.sh; fall back to 300 if not set + $gptCapacity = if ($env:AZURE_ENV_GPT_MODEL_CAPACITY) { $env:AZURE_ENV_GPT_MODEL_CAPACITY } else { "300" } + azd env set AZURE_ENV_GPT_MODEL_CAPACITY="$gptCapacity" # Set ACR name only when building Docker image if ($env:BUILD_DOCKER_IMAGE -eq "true") { diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index 8459acea..f58be0d8 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -110,6 +110,7 @@ jobs: ENV_NAME: ${{ steps.generate_env_name.outputs.ENV_NAME }} AZURE_LOCATION: ${{ steps.set_region.outputs.AZURE_LOCATION }} AZURE_ENV_OPENAI_LOCATION: ${{ steps.set_region.outputs.AZURE_ENV_OPENAI_LOCATION }} + AZURE_ENV_GPT_MODEL_CAPACITY: ${{ steps.set_region.outputs.AZURE_ENV_GPT_MODEL_CAPACITY }} IMAGE_TAG: ${{ steps.determine_image_tag.outputs.IMAGE_TAG }} QUOTA_FAILED: ${{ steps.quota_failure_output.outputs.QUOTA_FAILED }} EXP_ENABLED: ${{ steps.configure_exp.outputs.EXP_ENABLED }} @@ -359,8 +360,12 @@ jobs: INPUT_AZURE_LOCATION: ${{ inputs.azure_location }} run: | echo "Selected Region from Quota Check: $VALID_REGION" + echo "Available Capacity in $VALID_REGION: $AVAILABLE_CAPACITY" echo "AZURE_ENV_OPENAI_LOCATION=$VALID_REGION" >> $GITHUB_ENV echo "AZURE_ENV_OPENAI_LOCATION=$VALID_REGION" >> $GITHUB_OUTPUT + # Pass the actual available capacity so Bicep deploys only what is free + echo "AZURE_ENV_GPT_MODEL_CAPACITY=$AVAILABLE_CAPACITY" >> $GITHUB_ENV + echo "AZURE_ENV_GPT_MODEL_CAPACITY=$AVAILABLE_CAPACITY" >> $GITHUB_OUTPUT if [[ "$INPUT_TRIGGER_TYPE" == "workflow_dispatch" && -n "$INPUT_AZURE_LOCATION" ]]; then USER_SELECTED_LOCATION="$INPUT_AZURE_LOCATION" @@ -533,6 +538,7 @@ jobs: AZURE_LOCATION: ${{ needs.azure-setup.outputs.AZURE_LOCATION }} RESOURCE_GROUP_NAME: ${{ needs.azure-setup.outputs.RESOURCE_GROUP_NAME }} IMAGE_TAG: ${{ needs.azure-setup.outputs.IMAGE_TAG }} + AZURE_ENV_GPT_MODEL_CAPACITY: ${{ needs.azure-setup.outputs.AZURE_ENV_GPT_MODEL_CAPACITY }} BUILD_DOCKER_IMAGE: ${{ inputs.build_docker_image || 'false' }} EXP: ${{ needs.azure-setup.outputs.EXP_ENABLED }} WAF_ENABLED: ${{ inputs.waf_enabled == true && 'true' || 'false' }} @@ -551,6 +557,7 @@ jobs: AZURE_LOCATION: ${{ needs.azure-setup.outputs.AZURE_LOCATION }} RESOURCE_GROUP_NAME: ${{ needs.azure-setup.outputs.RESOURCE_GROUP_NAME }} IMAGE_TAG: ${{ needs.azure-setup.outputs.IMAGE_TAG }} + AZURE_ENV_GPT_MODEL_CAPACITY: ${{ needs.azure-setup.outputs.AZURE_ENV_GPT_MODEL_CAPACITY }} BUILD_DOCKER_IMAGE: ${{ inputs.build_docker_image || 'false' }} EXP: ${{ needs.azure-setup.outputs.EXP_ENABLED }} WAF_ENABLED: ${{ inputs.waf_enabled == true && 'true' || 'false' }} diff --git a/infra/main.bicep b/infra/main.bicep index c29e258f..2c9292fd 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -42,6 +42,7 @@ param contentUnderstandingLocation string = 'WestUS' 'japaneast' 'northeurope' 'southeastasia' + 'swedencentral' 'uksouth' ]) @description('Required. Location for the Azure AI Services deployment.') @@ -827,6 +828,11 @@ module cognitiveServicePrivateEndpoint 'br/public:avm/res/network/private-endpoi } subnetResourceId: virtualNetwork!.outputs.backendSubnetResourceId } + dependsOn: [ + avmAiServices + virtualNetwork + avmPrivateDnsZones + ] } module avmAiServices_cu 'br/public:avm/res/cognitive-services/account:0.14.1' = { diff --git a/infra/main.json b/infra/main.json index 4280795f..2cf7180c 100644 --- a/infra/main.json +++ b/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "5358772599129171911" + "templateHash": "2597731928932727268" }, "name": "Content Processing Solution Accelerator", "description": "Bicep template to deploy the Content Processing Solution Accelerator with AVM compliance." @@ -66,6 +66,7 @@ "japaneast", "northeurope", "southeastasia", + "swedencentral", "uksouth" ], "metadata": { @@ -35187,8 +35188,8 @@ "avmContainerApp_API", "avmContainerApp_Workflow", "avmManagedIdentity", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageQueue)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageBlob)]", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageQueue)]", "virtualNetwork" ] }, @@ -41721,10 +41722,7 @@ }, "dependsOn": [ "avmAiServices", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').contentUnderstanding)]", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').aiServices)]", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", + "avmPrivateDnsZones", "virtualNetwork" ] }, @@ -68558,6 +68556,13 @@ }, "value": "[reference('avmContainerRegistry').outputs.loginServer.value]" }, + "CONTENT_UNDERSTANDING_ACCOUNT_NAME": { + "type": "string", + "metadata": { + "description": "The name of the Content Understanding AI Services account." + }, + "value": "[reference('avmAiServices_cu').outputs.name.value]" + }, "AZURE_RESOURCE_GROUP": { "type": "string", "metadata": { diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index 3294106b..d491e348 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -830,6 +830,11 @@ module cognitiveServicePrivateEndpoint 'br/public:avm/res/network/private-endpoi } subnetResourceId: virtualNetwork!.outputs.backendSubnetResourceId } + dependsOn: [ + avmAiServices + virtualNetwork + avmPrivateDnsZones + ] } module avmAiServices_cu 'br/public:avm/res/cognitive-services/account:0.14.1' = { diff --git a/infra/scripts/checkquota.sh b/infra/scripts/checkquota.sh index a85b0db9..09dfaf08 100644 --- a/infra/scripts/checkquota.sh +++ b/infra/scripts/checkquota.sh @@ -1,7 +1,32 @@ #!/bin/bash -# List of Azure regions to check for quota (update as needed) -IFS=', ' read -ra REGIONS <<< "$AZURE_REGIONS" +# List of valid Azure regions for AI Services (must match Bicep @allowed values) +# These are the only regions where GPT-5.1 GlobalStandard is available +ALLOWED_REGIONS=("australiaeast" "centralus" "eastasia" "eastus2" "japaneast" "northeurope" "southeastasia" "swedencentral" "uksouth") + +# Get requested regions from environment variable, default to all allowed regions +# Supports comma-separated or space-separated (or mixed) AZURE_REGIONS values. +if [[ -n "$AZURE_REGIONS" ]]; then + IFS=', ' read -ra REQUESTED_REGIONS <<< "$AZURE_REGIONS" + # Filter requested regions to only include those in ALLOWED_REGIONS + REGIONS=() + for req_region in "${REQUESTED_REGIONS[@]}"; do + req_region=$(echo "$req_region" | xargs) # trim whitespace + [[ -z "$req_region" ]] && continue # skip empty tokens from double-delimiters + for allowed in "${ALLOWED_REGIONS[@]}"; do + if [[ "$req_region" == "$allowed" ]]; then + REGIONS+=("$req_region") + break + fi + done + done + if [[ ${#REGIONS[@]} -eq 0 ]]; then + echo "⚠️ WARNING: No valid regions found in AZURE_REGIONS. Using all allowed regions." + REGIONS=("${ALLOWED_REGIONS[@]}") + fi +else + REGIONS=("${ALLOWED_REGIONS[@]}") +fi SUBSCRIPTION_ID="${AZURE_SUBSCRIPTION_ID}" GPT_MIN_CAPACITY="${GPT_MIN_CAPACITY}" @@ -77,6 +102,7 @@ for REGION in "${REGIONS[@]}"; do if [ "$INSUFFICIENT_QUOTA" = false ]; then VALID_REGION="$REGION" + VALID_REGION_AVAILABLE_CAPACITY=$AVAILABLE break fi @@ -85,9 +111,11 @@ done if [ -z "$VALID_REGION" ]; then echo "❌ No region with sufficient quota found. Blocking deployment." echo "QUOTA_FAILED=true" >> "$GITHUB_ENV" - exit 0 + exit 1 else echo "✅ Suggested Region: $VALID_REGION" + echo "✅ Available Capacity: $VALID_REGION_AVAILABLE_CAPACITY" echo "VALID_REGION=$VALID_REGION" >> "$GITHUB_ENV" + echo "AVAILABLE_CAPACITY=$VALID_REGION_AVAILABLE_CAPACITY" >> "$GITHUB_ENV" exit 0 fi diff --git a/infra/scripts/post_deployment.ps1 b/infra/scripts/post_deployment.ps1 index 04104a50..c91b892e 100644 --- a/infra/scripts/post_deployment.ps1 +++ b/infra/scripts/post_deployment.ps1 @@ -1,5 +1,6 @@ -# Stop script on any error -$ErrorActionPreference = "Stop" +# Keep post-deployment best-effort so provisioning does not fail. +$ErrorActionPreference = "Continue" +$PSNativeCommandUseErrorActionPreference = $false Write-Host "[Search] Fetching container app info from azd environment..." @@ -76,13 +77,66 @@ if (-not $ApiReady) { Write-Host " API did not become ready after $MaxRetries attempts. Skipping schema registration." Write-Host " Run manually after the API is ready." } else { - # ---------- Schema registration (no Python dependency) ---------- + # ---------- Schema registration ---------- $SchemaInfoFile = Join-Path $FullPath "schema_info.json" $Manifest = Get-Content $SchemaInfoFile -Raw | ConvertFrom-Json $SchemaVaultUrl = "$ApiBaseUrl/schemavault/" $SchemaSetVaultUrl = "$ApiBaseUrl/schemasetvault/" + $PythonBin = $null + if (Get-Command python3 -ErrorAction SilentlyContinue) { + $PythonBin = "python3" + } elseif (Get-Command python -ErrorAction SilentlyContinue) { + $PythonBin = "python" + } + + function Convert-PythonSchemaToJson { + param( + [Parameter(Mandatory = $true)] [string]$PythonFile, + [Parameter(Mandatory = $true)] [string]$ClassName, + [Parameter(Mandatory = $true)] [string]$OutputFile, + [Parameter(Mandatory = $true)] [string]$PythonCmd + ) + + $script = @' +import importlib.util +import json +import inspect +import sys + +from pydantic import BaseModel + +py_path, class_name, out_path = sys.argv[1], sys.argv[2], sys.argv[3] +spec = importlib.util.spec_from_file_location("schema_module", py_path) +if spec is None or spec.loader is None: + raise RuntimeError(f"Unable to load schema module from {py_path}") +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) + +for obj in module.__dict__.values(): + if inspect.isclass(obj) and issubclass(obj, BaseModel): + try: + obj.model_rebuild(_types_namespace=module.__dict__) + except Exception: + pass + +cls = getattr(module, class_name, None) +if cls is None: + raise RuntimeError(f"Class '{class_name}' not found in {py_path}") +if not hasattr(cls, "model_json_schema"): + raise RuntimeError(f"Class '{class_name}' does not expose model_json_schema()") +schema = cls.model_json_schema() +with open(out_path, "w", encoding="utf-8") as f: + json.dump(schema, f, indent=2) +'@ + + $script | & $PythonCmd - $PythonFile $ClassName $OutputFile + if ($LASTEXITCODE -ne 0) { + throw "Failed generating JSON schema from '$PythonFile'." + } + } + # --- Step 1: Register schemas --- Write-Host "" Write-Host ("=" * 60) @@ -104,6 +158,7 @@ if (-not $ApiReady) { $ClassName = $entry.ClassName $Description = $entry.Description $SchemaFile = Join-Path $FullPath $entry.File + $SchemaFileOriginal = $SchemaFile Write-Host "" Write-Host "Processing schema: $ClassName" @@ -122,12 +177,34 @@ if (-not $ApiReady) { continue } + $UploadFile = $SchemaFile + $UploadFileName = [System.IO.Path]::GetFileName($SchemaFile) + $UploadContentType = "application/json" + $IsGeneratedJson = $false + + if ([System.IO.Path]::GetExtension($SchemaFile).ToLowerInvariant() -eq ".py") { + if (-not $PythonBin) { + Write-Host " Error: Python is required to convert '$UploadFileName' to JSON schema. Skipping..." + continue + } + + $GeneratedJsonPath = Join-Path $FullPath ("{0}.json" -f $ClassName) + try { + Convert-PythonSchemaToJson -PythonFile $SchemaFile -ClassName $ClassName -OutputFile $GeneratedJsonPath -PythonCmd $PythonBin + $UploadFile = $GeneratedJsonPath + $UploadFileName = [System.IO.Path]::GetFileNameWithoutExtension($SchemaFile) + ".json" + $IsGeneratedJson = $true + } catch { + Write-Host " Error: $_" + continue + } + } + Write-Host " Registering new schema '$ClassName'..." # Build multipart form data $dataPayload = @{ ClassName = $ClassName; Description = $Description } | ConvertTo-Json -Compress - $fileBytes = [System.IO.File]::ReadAllBytes($SchemaFile) - $fileName = [System.IO.Path]::GetFileName($SchemaFile) + $fileBytes = [System.IO.File]::ReadAllBytes($UploadFile) $boundary = [System.Guid]::NewGuid().ToString() $LF = "`r`n" @@ -136,8 +213,8 @@ if (-not $ApiReady) { "Content-Disposition: form-data; name=`"data`"$LF", $dataPayload, "--$boundary", - "Content-Disposition: form-data; name=`"file`"; filename=`"$fileName`"", - "Content-Type: text/x-python$LF", + "Content-Disposition: form-data; name=`"file`"; filename=`"$UploadFileName`"", + "Content-Type: $UploadContentType$LF", [System.Text.Encoding]::UTF8.GetString($fileBytes), "--$boundary--$LF" ) -join $LF @@ -150,7 +227,48 @@ if (-not $ApiReady) { Write-Host " Successfully registered: $Description's Schema Id - $schemaId" $Registered[$ClassName] = $schemaId } catch { - Write-Host " Failed to upload '$fileName'. Error: $_" + $statusCode = $null + $responseBody = "" + if ($_.Exception.Response) { + $statusCode = [int]$_.Exception.Response.StatusCode + try { + $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream()) + $responseBody = $reader.ReadToEnd() + $reader.Close() + } catch { + $responseBody = "" + } + } + + if ($IsGeneratedJson -and $statusCode -eq 415 -and $responseBody -match "Only \.py schema files are supported") { + Write-Host " API expects legacy .py schemas. Retrying with '$([System.IO.Path]::GetFileName($SchemaFileOriginal))'..." + try { + $legacyBytes = [System.IO.File]::ReadAllBytes($SchemaFileOriginal) + $legacyName = [System.IO.Path]::GetFileName($SchemaFileOriginal) + $legacyBoundary = [System.Guid]::NewGuid().ToString() + $legacyBody = ( + "--$legacyBoundary", + "Content-Disposition: form-data; name=`"data`"$LF", + $dataPayload, + "--$legacyBoundary", + "Content-Disposition: form-data; name=`"file`"; filename=`"$legacyName`"", + "Content-Type: text/x-python$LF", + [System.Text.Encoding]::UTF8.GetString($legacyBytes), + "--$legacyBoundary--$LF" + ) -join $LF + + $legacyResp = Invoke-RestMethod -Uri $SchemaVaultUrl -Method POST ` + -ContentType "multipart/form-data; boundary=$legacyBoundary" ` + -Body $legacyBody -TimeoutSec 60 -ErrorAction Stop + $schemaId = $legacyResp.Id + Write-Host " Successfully registered (legacy): $Description's Schema Id - $schemaId" + $Registered[$ClassName] = $schemaId + } catch { + Write-Host " Failed to upload '$legacyName'. Error: $_" + } + } else { + Write-Host " Failed to upload '$UploadFileName'. Error: $_" + } } } @@ -230,3 +348,5 @@ if (-not $ApiReady) { Write-Host " Schemas registered: $($Registered.Count)" Write-Host ("=" * 60) } + +exit 0 diff --git a/infra/scripts/post_deployment.sh b/infra/scripts/post_deployment.sh index 2b0ee0ad..6621f58b 100644 --- a/infra/scripts/post_deployment.sh +++ b/infra/scripts/post_deployment.sh @@ -1,7 +1,7 @@ #!/bin/bash -# Stop script on any error -set -e +# Keep post-deployment best-effort so provisioning does not fail. +set +e echo "🔍 Fetching container app info from azd environment..." @@ -80,11 +80,56 @@ if [ "$STATUS" != "200" ]; then echo " API did not become ready after $MAX_RETRIES attempts. Skipping schema registration." echo " Run manually after the API is ready." else - # ---------- Schema registration (no Python dependency) ---------- + # ---------- Schema registration ---------- SCHEMA_INFO_FILE="$DATA_SCRIPT_PATH/schema_info.json" SCHEMAVAULT_URL="$API_BASE_URL/schemavault/" SCHEMASETVAULT_URL="$API_BASE_URL/schemasetvault/" + PYTHON_BIN="" + if command -v python3 >/dev/null 2>&1; then + PYTHON_BIN="python3" + elif command -v python >/dev/null 2>&1; then + PYTHON_BIN="python" + fi + + generate_json_schema_from_python() { + local py_file="$1" + local class_name="$2" + local output_file="$3" + + "$PYTHON_BIN" - "$py_file" "$class_name" "$output_file" <<'PY' +import importlib.util +import json +import inspect +import sys + +from pydantic import BaseModel + +py_path, class_name, out_path = sys.argv[1], sys.argv[2], sys.argv[3] +spec = importlib.util.spec_from_file_location("schema_module", py_path) +if spec is None or spec.loader is None: + raise RuntimeError(f"Unable to load schema module from {py_path}") +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) + +for obj in module.__dict__.values(): + if inspect.isclass(obj) and issubclass(obj, BaseModel): + try: + obj.model_rebuild(_types_namespace=module.__dict__) + except Exception: + pass + +cls = getattr(module, class_name, None) +if cls is None: + raise RuntimeError(f"Class '{class_name}' not found in {py_path}") +if not hasattr(cls, "model_json_schema"): + raise RuntimeError(f"Class '{class_name}' does not expose model_json_schema()") +schema = cls.model_json_schema() +with open(out_path, "w", encoding="utf-8") as f: + json.dump(schema, f, indent=2) +PY + } + # --- Step 1: Register schemas --- echo "" echo "============================================================" @@ -101,6 +146,10 @@ else REGISTERED_IDS=() REGISTERED_NAMES=() + if [ "$SCHEMA_COUNT" -eq 0 ]; then + echo "No schemas found in manifest. Skipping schema registration." + fi + for idx in $(seq 0 $((SCHEMA_COUNT - 1))); do # Parse entry fields using grep/sed (no python needed) ENTRY=$(cat "$SCHEMA_INFO_FILE") @@ -109,6 +158,7 @@ else DESCRIPTION=$(echo "$ENTRY" | grep -o '"Description"[[:space:]]*:[[:space:]]*"[^"]*"' | sed -n "$((idx + 1))p" | sed 's/.*"Description"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') SCHEMA_FILE="$DATA_SCRIPT_PATH/$FILE_NAME" + SCHEMA_FILE_ORIGINAL="$SCHEMA_FILE" echo "" echo "Processing schema: $CLASS_NAME" @@ -133,18 +183,51 @@ else continue fi + UPLOAD_FILE="$SCHEMA_FILE" + UPLOAD_FILENAME="$FILE_NAME" + UPLOAD_CONTENT_TYPE="application/json" + IS_GENERATED_JSON=false + + if [[ "${SCHEMA_FILE,,}" == *.py ]]; then + if [ -z "$PYTHON_BIN" ]; then + echo " Error: Python is required to convert '$FILE_NAME' to JSON schema. Skipping..." + continue + fi + + GENERATED_JSON_FILE="$DATA_SCRIPT_PATH/${CLASS_NAME}.json" + if generate_json_schema_from_python "$SCHEMA_FILE" "$CLASS_NAME" "$GENERATED_JSON_FILE"; then + UPLOAD_FILE="$GENERATED_JSON_FILE" + UPLOAD_FILENAME="${FILE_NAME%.py}.json" + IS_GENERATED_JSON=true + else + echo " Error: Failed to generate JSON schema from '$FILE_NAME'. Skipping..." + continue + fi + fi + echo " Registering new schema '$CLASS_NAME'..." DATA_PAYLOAD="{\"ClassName\": \"$CLASS_NAME\", \"Description\": \"$DESCRIPTION\"}" RESPONSE=$(curl -s -w "\n%{http_code}" \ -X POST "$SCHEMAVAULT_URL" \ -F "data=$DATA_PAYLOAD" \ - -F "file=@$SCHEMA_FILE;type=text/x-python" \ + -F "file=@$UPLOAD_FILE;filename=$UPLOAD_FILENAME;type=$UPLOAD_CONTENT_TYPE" \ --connect-timeout 60) HTTP_CODE=$(echo "$RESPONSE" | tail -1) BODY=$(echo "$RESPONSE" | sed '$d') + if [ "$HTTP_CODE" = "415" ] && [ "$IS_GENERATED_JSON" = true ] && echo "$BODY" | grep -q "Only \.py schema files are supported"; then + echo " API expects legacy .py schemas. Retrying with '$FILE_NAME'..." + RESPONSE=$(curl -s -w "\n%{http_code}" \ + -X POST "$SCHEMAVAULT_URL" \ + -F "data=$DATA_PAYLOAD" \ + -F "file=@$SCHEMA_FILE_ORIGINAL;filename=$FILE_NAME;type=text/x-python" \ + --connect-timeout 60) + HTTP_CODE=$(echo "$RESPONSE" | tail -1) + BODY=$(echo "$RESPONSE" | sed '$d') + fi + if [ "$HTTP_CODE" = "200" ]; then SCHEMA_ID=$(echo "$BODY" | sed 's/.*"Id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') echo " Successfully registered: $DESCRIPTION's Schema Id - $SCHEMA_ID" @@ -250,14 +333,22 @@ CU_ACCOUNT_NAME=$(azd env get-value CONTENT_UNDERSTANDING_ACCOUNT_NAME 2>/dev/nu if [ -z "$CU_ACCOUNT_NAME" ]; then echo " ⚠️ CONTENT_UNDERSTANDING_ACCOUNT_NAME not found in azd env. Skipping refresh." else - echo " Refreshing account: $CU_ACCOUNT_NAME in resource group: $RESOURCE_GROUP" - if az cognitiveservices account update \ + echo " Checking account: $CU_ACCOUNT_NAME in resource group: $RESOURCE_GROUP" + + # Check if the resource group exists first + if ! az group show -n "$RESOURCE_GROUP" --output none 2>/dev/null; then + echo " ⚠️ Resource group '$RESOURCE_GROUP' does not exist yet. Skipping refresh (expected on fresh deployment)." + elif ! az cognitiveservices account show -g "$RESOURCE_GROUP" -n "$CU_ACCOUNT_NAME" --output none 2>/dev/null; then + echo " ⚠️ Cognitive Services account '$CU_ACCOUNT_NAME' not found. Skipping refresh (expected on fresh deployment)." + elif az cognitiveservices account update \ -g "$RESOURCE_GROUP" \ -n "$CU_ACCOUNT_NAME" \ --tags refresh=true \ - --output none; then + --output none 2>/dev/null; then echo " ✅ Successfully refreshed Cognitive Services account '$CU_ACCOUNT_NAME'." else - echo " ❌ Failed to refresh Cognitive Services account '$CU_ACCOUNT_NAME'." + echo " ⚠️ Failed to refresh Cognitive Services account '$CU_ACCOUNT_NAME' (non-critical)." fi fi + +exit 0