diff --git a/azure.yaml b/azure.yaml index 3f7faf1..4f14378 100644 --- a/azure.yaml +++ b/azure.yaml @@ -1,12 +1,32 @@ -# 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: ./scripts/create_capability_host.ps1 + interactive: true + posix: + shell: sh + run: sh ./scripts/create_capability_host.sh + interactive: true + +# 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..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 +param enableHostedAgents bool = false @description('Enable monitoring for the AI project') param enableMonitoring bool = true diff --git a/scripts/create_capability_host.ps1 b/scripts/create_capability_host.ps1 new file mode 100644 index 0000000..13e5790 --- /dev/null +++ b/scripts/create_capability_host.ps1 @@ -0,0 +1,140 @@ +#!/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 "✓ Capability host creation request submitted" -ForegroundColor Green + + # Poll for provisioning completion + $provisioningState = $response.properties.provisioningState + $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 { + 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..30ad206 --- /dev/null +++ b/scripts/create_capability_host.sh @@ -0,0 +1,144 @@ +#!/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) + 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" + echo "Response:" + echo "$RESPONSE_BODY" | jq '.' 2>/dev/null || echo "$RESPONSE_BODY" + exit 1 +fi