From 5c2cadb2deefec393daf0c8aef83e825cd511d49 Mon Sep 17 00:00:00 2001 From: Jeff Omhover Date: Fri, 12 Dec 2025 09:26:15 -0800 Subject: [PATCH 1/8] move caphost creation in a postprovision hook due to idempotency pb --- azure.yaml | 42 ++++++++++++++++++++++++---------- infra/core/ai/ai-project.bicep | 10 -------- infra/main.bicep | 2 +- infra/main.parameters.json | 2 +- 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/azure.yaml b/azure.yaml index 3f7faf1..f68e495 100644 --- a/azure.yaml +++ b/azure.yaml @@ -1,12 +1,30 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json -name: ai-foundry-starter-basic - -infra: - provider: bicep - path: ./infra - -requiredVersions: - extensions: - # the azd ai agent extension is required for this template - "azure.ai.agents": ">=0.1.0-preview" - +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +# Azure Developer CLI (azd) configuration for the Forge project +# This file defines how azd provisions and deploys Azure infrastructure and services. +# Learn more: https://learn.microsoft.com/azure/developer/azure-developer-cli/ + +# Project identifier used across Azure resources +name: azd-ai-starter-basic + +# Infrastructure provisioning configuration +infra: + provider: bicep + path: ./infra + module: main + +# Hooks to run custom scripts after provisioning infrastructure +hooks: + postprovision: + # NOTE: because caphost creation is not idempotent, we cannot run this in bicep directly + windows: + shell: pwsh + run: infra/scripts/create_capability_host.ps1 + posix: + shell: sh + run: infra/scripts/create_capability_host.sh + +# Recommended extensions for this project +requiredVersions: + extensions: + azure.ai.agents: '>=0.1.3-preview' diff --git a/infra/core/ai/ai-project.bicep b/infra/core/ai/ai-project.bicep index d0e8753..d041168 100644 --- a/infra/core/ai/ai-project.bicep +++ b/infra/core/ai/ai-project.bicep @@ -121,16 +121,6 @@ resource aiAccount 'Microsoft.CognitiveServices/accounts@2025-06-01' = { seqDeployments ] } - - resource aiFoundryAccountCapabilityHost 'capabilityHosts@2025-10-01-preview' = if (enableHostedAgents) { - name: 'agents' - properties: { - capabilityHostKind: 'Agents' - // IMPORTANT: this is required to enable hosted agents deployment - // if no BYO Net is provided - enablePublicHostingEnvironment: true - } - } } diff --git a/infra/main.bicep b/infra/main.bicep index 3675047..7312683 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -79,7 +79,7 @@ var aiProjectConnections = json(aiProjectConnectionsJson) var aiProjectDependentResources = json(aiProjectDependentResourcesJson) @description('Enable hosted agent deployment') -param enableHostedAgents bool +param enableHostedAgents bool = true @description('Enable monitoring for the AI project') param enableMonitoring bool = true diff --git a/infra/main.parameters.json b/infra/main.parameters.json index 323829e..b1bcbc7 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -39,7 +39,7 @@ "value": "${ENABLE_MONITORING=true}" }, "enableHostedAgents": { - "value": "${ENABLE_HOSTED_AGENTS=false}" + "value": "${ENABLE_HOSTED_AGENTS=true}" } } } From 5f9d73477c0203a2f27fbe635a6bfbc305cb48b9 Mon Sep 17 00:00:00 2001 From: Jeff Omhover Date: Fri, 12 Dec 2025 09:29:16 -0800 Subject: [PATCH 2/8] do not enable by default --- infra/main.bicep | 2 +- infra/main.parameters.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index 7312683..9daa637 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -79,7 +79,7 @@ var aiProjectConnections = json(aiProjectConnectionsJson) var aiProjectDependentResources = json(aiProjectDependentResourcesJson) @description('Enable hosted agent deployment') -param enableHostedAgents bool = true +param enableHostedAgents bool = false @description('Enable monitoring for the AI project') param enableMonitoring bool = true diff --git a/infra/main.parameters.json b/infra/main.parameters.json index b1bcbc7..323829e 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -39,7 +39,7 @@ "value": "${ENABLE_MONITORING=true}" }, "enableHostedAgents": { - "value": "${ENABLE_HOSTED_AGENTS=true}" + "value": "${ENABLE_HOSTED_AGENTS=false}" } } } From c9e8bc1335135dd35de0bf8e62fddfc39ddebae9 Mon Sep 17 00:00:00 2001 From: Jeff Omhover Date: Fri, 12 Dec 2025 09:37:49 -0800 Subject: [PATCH 3/8] fix path --- azure.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure.yaml b/azure.yaml index f68e495..b399bde 100644 --- a/azure.yaml +++ b/azure.yaml @@ -19,10 +19,10 @@ hooks: # NOTE: because caphost creation is not idempotent, we cannot run this in bicep directly windows: shell: pwsh - run: infra/scripts/create_capability_host.ps1 + run: scripts/create_capability_host.ps1 posix: shell: sh - run: infra/scripts/create_capability_host.sh + run: scripts/create_capability_host.sh # Recommended extensions for this project requiredVersions: From 40916fc199dccff08b96f3cac95a86303eaed1fa Mon Sep 17 00:00:00 2001 From: Jeff Omhover Date: Fri, 12 Dec 2025 09:48:33 -0800 Subject: [PATCH 4/8] Add scripts --- scripts/create_capability_host.ps1 | 112 +++++++++++++++++++++++++++++ scripts/create_capability_host.sh | 108 ++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 scripts/create_capability_host.ps1 create mode 100644 scripts/create_capability_host.sh diff --git a/scripts/create_capability_host.ps1 b/scripts/create_capability_host.ps1 new file mode 100644 index 0000000..b6d2cb5 --- /dev/null +++ b/scripts/create_capability_host.ps1 @@ -0,0 +1,112 @@ +#!/usr/bin/env pwsh + +# Script to create an Azure AI Foundry capability host using the REST API +# This script checks if the capability host exists before attempting to create it + +$ErrorActionPreference = "Stop" + +# Load environment variables from azd +if (-not $env:AZURE_SUBSCRIPTION_ID) { + Write-Error "AZURE_SUBSCRIPTION_ID not set. Please run 'azd env refresh' first." + exit 1 +} + +if (-not $env:AZURE_RESOURCE_GROUP) { + Write-Error "AZURE_RESOURCE_GROUP not set. Please run 'azd env refresh' first." + exit 1 +} + +if (-not $env:AZURE_AI_ACCOUNT_NAME) { + Write-Error "AZURE_AI_ACCOUNT_NAME not set. Please run 'azd env refresh' first." + exit 1 +} + +$subscriptionId = $env:AZURE_SUBSCRIPTION_ID +$resourceGroup = $env:AZURE_RESOURCE_GROUP +$accountName = $env:AZURE_AI_ACCOUNT_NAME +$capabilityHostName = "agents" +$apiVersion = "2025-10-01-preview" + +# Get Azure access token +Write-Host "Getting Azure access token..." -ForegroundColor Cyan +try { + $tokenResponse = azd auth token --output json --scope https://management.azure.com/.default | ConvertFrom-Json + $accessToken = $tokenResponse.token + if (-not $accessToken) { + throw "Failed to get access token" + } +} +catch { + Write-Error "Failed to get access token. Please run 'azd auth login' first." + exit 1 +} + +# Check if capability host already exists +Write-Host "Checking if capability host '$capabilityHostName' already exists..." -ForegroundColor Cyan +$checkUrl = "https://management.azure.com/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.CognitiveServices/accounts/${accountName}/capabilityHosts/${capabilityHostName}?api-version=${apiVersion}" + +Write-Host "Debug - Check URL: $checkUrl" -ForegroundColor Gray + +$headers = @{ + "Authorization" = "Bearer $accessToken" + "Content-Type" = "application/json" +} + +try { + $existingHost = Invoke-RestMethod -Uri $checkUrl -Method Get -Headers $headers + if ($existingHost) { + Write-Host "✓ Capability host '$capabilityHostName' already exists. Skipping creation." -ForegroundColor Green + exit 0 + } +} +catch { + $statusCode = $_.Exception.Response.StatusCode.value__ + if ($statusCode -eq 404) { + # 404 means it doesn't exist, continue with creation + Write-Host "Capability host does not exist. Creating..." -ForegroundColor Cyan + } + else { + Write-Error "Error checking for existing capability host (HTTP $statusCode): $($_.ErrorDetails.Message)" + exit 1 + } +} + +# Construct the REST API URL +$url = "https://management.azure.com/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.CognitiveServices/accounts/${accountName}/capabilityHosts/${capabilityHostName}?api-version=${apiVersion}" + +# Construct the request body +# For hosted agents without BYONET, enablePublicHostingEnvironment is required +$requestBody = @{ + properties = @{ + capabilityHostKind = "Agents" + enablePublicHostingEnvironment = $true + } +} | ConvertTo-Json -Depth 10 + +Write-Host "Creating capability host '$capabilityHostName'..." -ForegroundColor Cyan +Write-Host "URL: $url" -ForegroundColor Gray + +# Make the REST API call +try { + $response = Invoke-RestMethod -Uri $url -Method Put -Headers $headers -Body $requestBody + + Write-Host "✓ Successfully created capability host '$capabilityHostName'" -ForegroundColor Green + Write-Host "Response:" -ForegroundColor Gray + $response | ConvertTo-Json -Depth 10 | Write-Host + + # Check provisioning state + $provisioningState = $response.properties.provisioningState + if ($provisioningState -in @("Creating", "Updating")) { + Write-Host "" + Write-Warning "Capability host is being provisioned (state: $provisioningState)" + Write-Host "This may take a few minutes. Check the Azure portal for status." -ForegroundColor Yellow + } +} +catch { + Write-Host "✗ Failed to create capability host" -ForegroundColor Red + Write-Host "Error: $_" -ForegroundColor Red + if ($_.ErrorDetails.Message) { + Write-Host "Details: $($_.ErrorDetails.Message)" -ForegroundColor Red + } + exit 1 +} diff --git a/scripts/create_capability_host.sh b/scripts/create_capability_host.sh new file mode 100644 index 0000000..7f3fe03 --- /dev/null +++ b/scripts/create_capability_host.sh @@ -0,0 +1,108 @@ +#!/bin/bash + +# Script to create an Azure AI Foundry capability host using the REST API +# This script checks if the capability host exists before attempting to create it + +set -e + +# Load environment variables from azd +if [ -z "$AZURE_SUBSCRIPTION_ID" ]; then + echo "Error: AZURE_SUBSCRIPTION_ID not set. Please run 'azd env refresh' first." + exit 1 +fi + +if [ -z "$AZURE_RESOURCE_GROUP" ]; then + echo "Error: AZURE_RESOURCE_GROUP not set. Please run 'azd env refresh' first." + exit 1 +fi + +if [ -z "$AZURE_AI_ACCOUNT_NAME" ]; then + echo "Error: AZURE_AI_ACCOUNT_NAME not set. Please run 'azd env refresh' first." + exit 1 +fi + +SUBSCRIPTION_ID="${AZURE_SUBSCRIPTION_ID}" +RESOURCE_GROUP="${AZURE_RESOURCE_GROUP}" +ACCOUNT_NAME="${AZURE_AI_ACCOUNT_NAME}" +CAPABILITY_HOST_NAME="agents" +API_VERSION="2025-10-01-preview" + +# Get Azure access token +echo "Getting Azure access token..." +TOKEN_JSON=$(azd auth token --output json --scope https://management.azure.com/.default) +ACCESS_TOKEN=$(echo "$TOKEN_JSON" | jq -r '.token') + +if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then + echo "Error: Failed to get access token. Please run 'azd auth login' first." + exit 1 +fi + +# Check if capability host already exists +echo "Checking if capability host '$CAPABILITY_HOST_NAME' already exists..." +CHECK_URL="https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.CognitiveServices/accounts/${ACCOUNT_NAME}/capabilityHosts/${CAPABILITY_HOST_NAME}?api-version=${API_VERSION}" + +echo "Debug - Check URL: $CHECK_URL" + +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ + -X GET \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json" \ + "${CHECK_URL}") + +if [ "$HTTP_CODE" = "200" ]; then + echo "✓ Capability host '$CAPABILITY_HOST_NAME' already exists. Skipping creation." + exit 0 +fi + +echo "Capability host does not exist. Creating..." + +# Construct the REST API URL +URL="https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.CognitiveServices/accounts/${ACCOUNT_NAME}/capabilityHosts/${CAPABILITY_HOST_NAME}?api-version=${API_VERSION}" + +# Construct the request body +# For hosted agents without BYONET, enablePublicHostingEnvironment is required +REQUEST_BODY=$(cat </dev/null || echo "$RESPONSE_BODY" + + # Check provisioning state + PROVISIONING_STATE=$(echo "$RESPONSE_BODY" | jq -r '.properties.provisioningState' 2>/dev/null) + if [ "$PROVISIONING_STATE" = "Creating" ] || [ "$PROVISIONING_STATE" = "Updating" ]; then + echo "" + echo "⚠ Capability host is being provisioned (state: $PROVISIONING_STATE)" + echo "This may take a few minutes. Check the Azure portal for status." + fi +else + echo "✗ Failed to create capability host" + echo "Response:" + echo "$RESPONSE_BODY" | jq '.' 2>/dev/null || echo "$RESPONSE_BODY" + exit 1 +fi From 226aefc123665a77432d07493a5bc4787f77b5a8 Mon Sep 17 00:00:00 2001 From: Jeff Omhover Date: Fri, 12 Dec 2025 09:58:24 -0800 Subject: [PATCH 5/8] fix path --- azure.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure.yaml b/azure.yaml index b399bde..b1d1c8e 100644 --- a/azure.yaml +++ b/azure.yaml @@ -19,10 +19,10 @@ hooks: # NOTE: because caphost creation is not idempotent, we cannot run this in bicep directly windows: shell: pwsh - run: scripts/create_capability_host.ps1 + run: ./scripts/create_capability_host.ps1 posix: shell: sh - run: scripts/create_capability_host.sh + run: ./scripts/create_capability_host.sh # Recommended extensions for this project requiredVersions: From 7f18fe708594f3fa841c37cf94f0cb2f806b1b81 Mon Sep 17 00:00:00 2001 From: Jeff Omhover Date: Fri, 12 Dec 2025 10:15:46 -0800 Subject: [PATCH 6/8] add interactive mode --- azure.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/azure.yaml b/azure.yaml index b1d1c8e..561ae47 100644 --- a/azure.yaml +++ b/azure.yaml @@ -20,9 +20,11 @@ hooks: windows: shell: pwsh run: ./scripts/create_capability_host.ps1 + interactive: true posix: shell: sh run: ./scripts/create_capability_host.sh + interactive: true # Recommended extensions for this project requiredVersions: From 7c02ff1b78b58646b71cb99fe36c2c4b36b3cfbb Mon Sep 17 00:00:00 2001 From: Jeff Omhover Date: Fri, 12 Dec 2025 10:24:32 -0800 Subject: [PATCH 7/8] add sh to resolve pperms denied --- azure.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure.yaml b/azure.yaml index 561ae47..4f14378 100644 --- a/azure.yaml +++ b/azure.yaml @@ -23,7 +23,7 @@ hooks: interactive: true posix: shell: sh - run: ./scripts/create_capability_host.sh + run: sh ./scripts/create_capability_host.sh interactive: true # Recommended extensions for this project From 27fadbc3a607424b4b8c92dfa6096619131d66b6 Mon Sep 17 00:00:00 2001 From: Jeff Omhover Date: Fri, 12 Dec 2025 10:59:07 -0800 Subject: [PATCH 8/8] wait for caphost success --- scripts/create_capability_host.ps1 | 44 ++++++++++++++++++++----- scripts/create_capability_host.sh | 52 +++++++++++++++++++++++++----- 2 files changed, 80 insertions(+), 16 deletions(-) diff --git a/scripts/create_capability_host.ps1 b/scripts/create_capability_host.ps1 index b6d2cb5..13e5790 100644 --- a/scripts/create_capability_host.ps1 +++ b/scripts/create_capability_host.ps1 @@ -90,16 +90,44 @@ Write-Host "URL: $url" -ForegroundColor Gray try { $response = Invoke-RestMethod -Uri $url -Method Put -Headers $headers -Body $requestBody - Write-Host "✓ Successfully created capability host '$capabilityHostName'" -ForegroundColor Green - Write-Host "Response:" -ForegroundColor Gray - $response | ConvertTo-Json -Depth 10 | Write-Host + Write-Host "✓ Capability host creation request submitted" -ForegroundColor Green - # Check provisioning state + # Poll for provisioning completion $provisioningState = $response.properties.provisioningState - if ($provisioningState -in @("Creating", "Updating")) { - Write-Host "" - Write-Warning "Capability host is being provisioned (state: $provisioningState)" - Write-Host "This may take a few minutes. Check the Azure portal for status." -ForegroundColor Yellow + $maxAttempts = 60 # 5 minutes max (5 second intervals) + $attempt = 0 + + while ($provisioningState -in @("Creating", "Updating") -and $attempt -lt $maxAttempts) { + $attempt++ + Write-Host "Waiting for provisioning to complete (state: $provisioningState, attempt $attempt/$maxAttempts)..." -ForegroundColor Yellow + Start-Sleep -Seconds 5 + + try { + $statusResponse = Invoke-RestMethod -Uri $url -Method Get -Headers $headers + $provisioningState = $statusResponse.properties.provisioningState + } + catch { + Write-Warning "Failed to check status: $_" + break + } + } + + # Check final state + if ($provisioningState -eq "Succeeded") { + Write-Host "✓ Successfully created capability host '$capabilityHostName'" -ForegroundColor Green + Write-Host "Provisioning state: $provisioningState" -ForegroundColor Green + } + elseif ($provisioningState -eq "Failed") { + Write-Error "Capability host provisioning failed" + exit 1 + } + elseif ($provisioningState -eq "Canceled") { + Write-Error "Capability host provisioning was canceled" + exit 1 + } + else { + Write-Warning "Capability host is still provisioning (state: $provisioningState)" + Write-Host "Check the Azure portal for status." -ForegroundColor Yellow } } catch { diff --git a/scripts/create_capability_host.sh b/scripts/create_capability_host.sh index 7f3fe03..30ad206 100644 --- a/scripts/create_capability_host.sh +++ b/scripts/create_capability_host.sh @@ -89,16 +89,52 @@ RESPONSE_BODY=$(echo "$RESPONSE" | sed '$d') echo "HTTP Status: $HTTP_STATUS" if [ "$HTTP_STATUS" = "200" ] || [ "$HTTP_STATUS" = "201" ]; then - echo "✓ Successfully created capability host '$CAPABILITY_HOST_NAME'" - echo "Response:" - echo "$RESPONSE_BODY" | jq '.' 2>/dev/null || echo "$RESPONSE_BODY" + echo "✓ Capability host creation request submitted" - # Check provisioning state + # Poll for provisioning completion PROVISIONING_STATE=$(echo "$RESPONSE_BODY" | jq -r '.properties.provisioningState' 2>/dev/null) - if [ "$PROVISIONING_STATE" = "Creating" ] || [ "$PROVISIONING_STATE" = "Updating" ]; then - echo "" - echo "⚠ Capability host is being provisioned (state: $PROVISIONING_STATE)" - echo "This may take a few minutes. Check the Azure portal for status." + MAX_ATTEMPTS=60 # 5 minutes max (5 second intervals) + ATTEMPT=0 + + while [ "$PROVISIONING_STATE" = "Creating" ] || [ "$PROVISIONING_STATE" = "Updating" ]; do + if [ $ATTEMPT -ge $MAX_ATTEMPTS ]; then + echo "⚠ Timeout waiting for provisioning to complete (state: $PROVISIONING_STATE)" + echo "Check the Azure portal for status." + break + fi + + ATTEMPT=$((ATTEMPT + 1)) + echo "Waiting for provisioning to complete (state: $PROVISIONING_STATE, attempt $ATTEMPT/$MAX_ATTEMPTS)..." + sleep 5 + + # Check current status + STATUS_RESPONSE=$(curl -s \ + -X GET \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json" \ + "${URL}") + + PROVISIONING_STATE=$(echo "$STATUS_RESPONSE" | jq -r '.properties.provisioningState' 2>/dev/null) + + if [ -z "$PROVISIONING_STATE" ] || [ "$PROVISIONING_STATE" = "null" ]; then + echo "⚠ Failed to check provisioning status" + break + fi + done + + # Check final state + if [ "$PROVISIONING_STATE" = "Succeeded" ]; then + echo "✓ Successfully created capability host '$CAPABILITY_HOST_NAME'" + echo "Provisioning state: $PROVISIONING_STATE" + elif [ "$PROVISIONING_STATE" = "Failed" ]; then + echo "✗ Capability host provisioning failed" + exit 1 + elif [ "$PROVISIONING_STATE" = "Canceled" ]; then + echo "✗ Capability host provisioning was canceled" + exit 1 + else + echo "⚠ Capability host is still provisioning (state: $PROVISIONING_STATE)" + echo "Check the Azure portal for status." fi else echo "✗ Failed to create capability host"