diff --git a/001-customer-support-agent/README.md b/001-customer-support-agent/README.md
index d5f5ed7..16b9260 100644
--- a/001-customer-support-agent/README.md
+++ b/001-customer-support-agent/README.md
@@ -14,19 +14,15 @@ This sample is a customer support agent application for the fictional **Contoso
Below is a high-level architecture diagram of the application:
-
-
-
+
At the completion of this walkthrough, the application and Radius configuration will look like:
-
-
-
+
## 📁 Repository structure
-```
+```text
├── knowledge-base/ # Contoso policy PDFs for RAG
├── scripts/setup-azure.sh # One-command Azure prerequisite setup
├── src/
@@ -63,7 +59,7 @@ Before you begin, you need:
>
> - WSL (recommended)
> - Git Bash
-> - Azure Cloud Shell
+> - [Azure Cloud Shell](https://shell.azure.com/)
>
> `jq` is required for parsing JSON output in Bash. Install it depending on the bash environment. PowerShell users can follow along with minor syntax adjustments (examples provided where needed).
@@ -91,10 +87,7 @@ Run the setup script to create an Azure resource group, AKS cluster, service pri
**PowerShell**
```powershell
-
-(Get-Content .\scripts\setup-azure.sh -Raw) -replace "`r`n","`n" | Set-Content .\scripts\setup-azure.sh -Encoding UTF8
-
-bash ./scripts/setup-azure.sh --location westus3 --resource-group customer-support-agent --cluster-name customer-support-agent-aks
+./scripts/setup-azure.ps1 -Location westus3 -ResourceGroupName customer-support-agent -ClusterName customer-support-agent-aks
```
> [!NOTE]
@@ -139,7 +132,7 @@ kubectl get pods -n radius-system
You should see all Radius pods running:
-```
+```text
NAME READY STATUS RESTARTS AGE
applications-rp 1/1 Running 0 1m
bicep-de 1/1 Running 0 1m
@@ -184,7 +177,7 @@ Verify the credential is registered:
rad credential list
```
-```
+```text
PROVIDER REGISTERED
azure true
```
@@ -215,7 +208,7 @@ You can verify the types were created:
rad resource-type list
```
-```
+```text
TYPE NAMESPACE APIVERSION
Applications.Core/applications Applications.Core ["2023-10-01-preview"]
...
@@ -236,6 +229,7 @@ rad bicep publish-extension -f radius/types/agent.yaml --target radius/extension
rad bicep publish-extension -f radius/types/postgreSqlDatabases.yaml --target radius/extensions/radiusdata.tgz
rad bicep publish-extension -f radius/types/blobStorages.yaml --target radius/extensions/radiusstorage.tgz
```
+
### Step 6: Create the Radius Environment
@@ -305,7 +299,7 @@ Now lets look at the recipes added to the environment. A Recipe defines *how* to
rad recipe list
```
-```
+```text
RECIPE TYPE TEMPLATE KIND TEMPLATE
default Radius.AI/agents bicep ghcr.io/radius-project/lab/recipes/agent:1.0
default Radius.Data/postgreSqlDatabases bicep ghcr.io/radius-project/lab/recipes/postgres:1.0
@@ -451,7 +445,7 @@ The deployment will take approximately 15-20 minutes to complete. In the meantim
When the deployment is complete, you should see output similar to:
-```
+```text
Deployment In Progress...
Completed contoso-support-agent Applications.Core/applications
@@ -482,7 +476,7 @@ Here are some things to try that demonstrate the agentic behavior:
**Simple order lookup** (single tool call):
-```
+```text
What's the status of ORD-10001?
```
@@ -490,7 +484,7 @@ The agent will call `lookup_order` and respond with the order details from the d
**Policy question** (knowledge base search):
-```
+```text
What's your return policy for electronics?
```
@@ -498,7 +492,7 @@ The agent will call `search_knowledge_base` and respond using information from t
**Multi-step tool chaining** (multiple tool calls in sequence):
-```
+```text
I want to return the headphones from order ORD-10001. Can you help?
```
@@ -511,7 +505,7 @@ The agent will:
**Escalation** (knowing when to hand off):
-```
+```text
This is unacceptable, I've been waiting 3 weeks and nobody can help me. I want to speak to a manager.
```
diff --git a/001-customer-support-agent/scripts/setup-azure.ps1 b/001-customer-support-agent/scripts/setup-azure.ps1
new file mode 100644
index 0000000..2ee95c3
--- /dev/null
+++ b/001-customer-support-agent/scripts/setup-azure.ps1
@@ -0,0 +1,142 @@
+#Requires -Modules Az.Accounts, Az.Resources, Az.Aks
+
+<#
+.SYNOPSIS
+ Setup Azure prerequisites for the Customer Support Agent.
+
+.DESCRIPTION
+ Creates: resource group, AKS cluster (if it doesn't exist),
+ service principal, and registers required resource providers.
+ Saves service principal credentials to .azure-sp.env for use by Radius.
+
+.PARAMETER Location
+ Azure region for resources. Default: westus3
+
+.PARAMETER ResourceGroupName
+ Name of the resource group. Default: customer-support-agent
+
+.PARAMETER ClusterName
+ Name of the AKS cluster. Default: customer-support-agent-aks
+
+.EXAMPLE
+ ./scripts/setup-azure.ps1
+ ./scripts/setup-azure.ps1 -Location eastus2
+ ./scripts/setup-azure.ps1 -ResourceGroupName my-rg -ClusterName my-aks
+#>
+
+[CmdletBinding()]
+param(
+ [string]$ResourceGroupName = 'customer-support-agent',
+ [string]$Location = 'westus3',
+ [string]$ClusterName = 'customer-support-agent-aks'
+)
+
+$ErrorActionPreference = 'Stop'
+
+$SpName = 'radius-sp'
+
+# ── 0. Ensure Azure PowerShell is logged in ──────────────────
+Write-Host '==> Checking Azure PowerShell login...'
+$context = Get-AzContext
+if (-not $context) {
+ Write-Host ' Not logged in. Opening browser for login...'
+ Connect-AzAccount
+ $context = Get-AzContext
+}
+
+$SubscriptionId = $context.Subscription.Id
+Write-Host "Subscription: $SubscriptionId"
+Write-Host "Resource Group: $ResourceGroupName"
+Write-Host "Location: $Location"
+Write-Host "AKS Cluster: $ClusterName"
+Write-Host ''
+
+# ── 1. Register resource providers ────────────────────────────
+Write-Host '==> Registering resource providers...'
+$providers = @(
+ 'Microsoft.Storage'
+ 'Microsoft.DBforPostgreSQL'
+ 'Microsoft.ContainerInstance'
+ 'Microsoft.OperationalInsights'
+ 'Microsoft.Search'
+ 'Microsoft.CognitiveServices'
+ 'Microsoft.ContainerService'
+)
+foreach ($provider in $providers) {
+ Register-AzResourceProvider -ProviderNamespace $provider | Out-Null
+ Write-Host " ✓ $provider"
+}
+
+# ── 2. Create resource group ─────────────────────────────────
+Write-Host ''
+Write-Host "==> Creating resource group '$ResourceGroupName' in '$Location'..."
+New-AzResourceGroup -Name $ResourceGroupName -Location $Location -Force | Out-Null
+
+# ── 3. Create or connect to AKS cluster ──────────────────────
+Write-Host ''
+$existingCluster = Get-AzAksCluster -ResourceGroupName $ResourceGroupName -Name $ClusterName -ErrorAction SilentlyContinue
+if ($existingCluster) {
+ Write-Host "==> AKS cluster '$ClusterName' already exists, getting credentials..."
+ Import-AzAksCredential `
+ -ResourceGroupName $ResourceGroupName `
+ -Name $ClusterName `
+ -Force
+}
+else {
+ Write-Host "==> Creating AKS cluster '$ClusterName' (this takes a few minutes)..."
+ $sshKey = Join-Path -Path $HOME -ChildPath '.ssh/id_rsa.pub'
+ $aksParams = @{
+ ResourceGroupName = $ResourceGroupName
+ Name = $ClusterName
+ Location = $Location
+ NodeCount = 1
+ NodeVmSize = 'Standard_D2s_v3'
+ }
+ if (Test-Path $sshKey) {
+ $aksParams['SshKeyValue'] = $sshKey
+ }
+ else {
+ $aksParams['GenerateSshKey'] = $true
+ }
+ New-AzAksCluster @aksParams
+
+ Write-Host '==> Getting AKS credentials...'
+ Import-AzAksCredential `
+ -ResourceGroupName $ResourceGroupName `
+ -Name $ClusterName `
+ -Force
+}
+
+# ── 4. Create service principal ──────────────────────────────
+Write-Host ''
+Write-Host "==> Creating service principal '$SpName' with Owner role..."
+$scope = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName"
+
+# Create the AD application and service principal with a password credential
+$sp = New-AzADServicePrincipal `
+ -DisplayName $SpName `
+ -Role 'Owner' `
+ -Scope $scope
+
+$ClientId = $sp.AppId
+$ClientSecret = $sp.PasswordCredentials.SecretText
+$TenantId = $context.Tenant.Id
+
+# ── 5. Save credentials to file ──────────────────────────────
+$envFile = '.azure-sp.env'
+@"
+AZURE_CLIENT_ID=$ClientId
+AZURE_CLIENT_SECRET=$ClientSecret
+AZURE_TENANT_ID=$TenantId
+AZURE_SUBSCRIPTION_ID=$SubscriptionId
+AZURE_RESOURCE_GROUP=$ResourceGroupName
+"@ | Set-Content -Path $envFile -NoNewline
+
+Write-Host ''
+Write-Host "==> Service principal credentials saved to $envFile"
+
+Write-Host ''
+Write-Host '================================================'
+Write-Host ' Azure setup complete!'
+Write-Host '================================================'
+Write-Host ''
diff --git a/001-customer-support-agent/scripts/setup-azure.sh b/001-customer-support-agent/scripts/setup-azure.sh
index 01dbc13..52313c2 100755
--- a/001-customer-support-agent/scripts/setup-azure.sh
+++ b/001-customer-support-agent/scripts/setup-azure.sh
@@ -1,4 +1,5 @@
#!/bin/bash
+
set -euo pipefail
# ── Setup Azure prerequisites for the Customer Support Agent ──
@@ -20,22 +21,22 @@ SP_NAME="radius-sp"
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
- --location)
- LOCATION="$2"
- shift 2
- ;;
- --resource-group)
- RESOURCE_GROUP="$2"
- shift 2
- ;;
- --cluster-name)
- CLUSTER_NAME="$2"
- shift 2
- ;;
- *)
- echo "Unknown option: $1"
- exit 1
- ;;
+ --location)
+ LOCATION="$2"
+ shift 2
+ ;;
+ --resource-group)
+ RESOURCE_GROUP="$2"
+ shift 2
+ ;;
+ --cluster-name)
+ CLUSTER_NAME="$2"
+ shift 2
+ ;;
+ *)
+ echo "Unknown option: $1"
+ exit 1
+ ;;
esac
done
@@ -45,23 +46,23 @@ sanitize() {
printf '%s' "$1" | tr -d '\r'
}
-RESOURCE_GROUP=$(sanitize "$RESOURCE_GROUP")
-LOCATION=$(sanitize "$LOCATION")
-CLUSTER_NAME=$(sanitize "$CLUSTER_NAME")
-SP_NAME=$(sanitize "$SP_NAME")
+RESOURCE_GROUP=$(sanitize "${RESOURCE_GROUP}")
+LOCATION=$(sanitize "${LOCATION}")
+CLUSTER_NAME=$(sanitize "${CLUSTER_NAME}")
+SP_NAME=$(sanitize "${SP_NAME}")
# ── 0. Ensure Azure CLI is logged in ─────────────────────
echo "==> Checking Azure CLI login..."
-if ! az account show &>/dev/null; then
+if ! az account show &> /dev/null; then
echo " Not logged in. Opening browser for login..."
az login
fi
SUBSCRIPTION_ID=$(sanitize "$(az account show --query id -o tsv)")
-echo "Subscription: $SUBSCRIPTION_ID"
-echo "Resource Group: $RESOURCE_GROUP"
-echo "Location: $LOCATION"
-echo "AKS Cluster: $CLUSTER_NAME"
+echo "Subscription: ${SUBSCRIPTION_ID}"
+echo "Resource Group: ${RESOURCE_GROUP}"
+echo "Location: ${LOCATION}"
+echo "AKS Cluster: ${CLUSTER_NAME}"
echo ""
# ── 1. Register resource providers ──────────────────────────
@@ -76,63 +77,63 @@ PROVIDERS=(
Microsoft.ContainerService
)
for provider in "${PROVIDERS[@]}"; do
- az provider register --namespace "$provider" --wait 2>/dev/null || true
- echo " ✓ $provider"
+ az provider register --namespace "${provider}" --wait 2> /dev/null || true
+ echo " ✓ ${provider}"
done
# ── 2. Create resource group ────────────────────────────────
echo ""
-echo "==> Creating resource group '$RESOURCE_GROUP' in '$LOCATION'..."
-az group create --name "$RESOURCE_GROUP" --location "$LOCATION" -o none
+echo "==> Creating resource group '${RESOURCE_GROUP}' in '${LOCATION}'..."
+az group create --name "${RESOURCE_GROUP}" --location "${LOCATION}" -o none
# ── 3. Create or connect to AKS cluster ─────────────────────
echo ""
-if az aks show --resource-group "$RESOURCE_GROUP" --name "$CLUSTER_NAME" &>/dev/null; then
- echo "==> AKS cluster '$CLUSTER_NAME' already exists, getting credentials..."
+if az aks show --resource-group "${RESOURCE_GROUP}" --name "${CLUSTER_NAME}" &> /dev/null; then
+ echo "==> AKS cluster '${CLUSTER_NAME}' already exists, getting credentials..."
az aks get-credentials \
- --resource-group "$RESOURCE_GROUP" \
- --name "$CLUSTER_NAME" \
+ --resource-group "${RESOURCE_GROUP}" \
+ --name "${CLUSTER_NAME}" \
--overwrite-existing
else
- echo "==> Creating AKS cluster '$CLUSTER_NAME' (this takes a few minutes)..."
+ echo "==> Creating AKS cluster '${CLUSTER_NAME}' (this takes a few minutes)..."
az aks create \
- --resource-group "$RESOURCE_GROUP" \
- --name "$CLUSTER_NAME" \
+ --resource-group "${RESOURCE_GROUP}" \
+ --name "${CLUSTER_NAME}" \
--generate-ssh-keys \
--node-count 1 \
--node-vm-size Standard_D2s_v3
echo "==> Getting AKS credentials..."
az aks get-credentials \
- --resource-group "$RESOURCE_GROUP" \
- --name "$CLUSTER_NAME" \
+ --resource-group "${RESOURCE_GROUP}" \
+ --name "${CLUSTER_NAME}" \
--overwrite-existing
fi
# ── 4. Create service principal ─────────────────────────────
echo ""
-echo "==> Creating service principal '$SP_NAME' with Owner role..."
+echo "==> Creating service principal '${SP_NAME}' with Owner role..."
SP_OUTPUT=$(az ad sp create-for-rbac \
- --name "$SP_NAME" \
+ --name "${SP_NAME}" \
--role Owner \
- --scopes /subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP \
+ --scopes "/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}" \
-o json)
-CLIENT_ID=$(echo "$SP_OUTPUT" | jq -r '.appId')
-CLIENT_SECRET=$(echo "$SP_OUTPUT" | jq -r '.password')
-TENANT_ID=$(echo "$SP_OUTPUT" | jq -r '.tenant')
+CLIENT_ID=$(echo "${SP_OUTPUT}" | jq -r '.appId')
+CLIENT_SECRET=$(echo "${SP_OUTPUT}" | jq -r '.password')
+TENANT_ID=$(echo "${SP_OUTPUT}" | jq -r '.tenant')
# ── 5. Save credentials to file ─────────────────────────────
ENV_FILE=".azure-sp.env"
-cat >"$ENV_FILE" < "${ENV_FILE}" << EOF
+AZURE_CLIENT_ID=${CLIENT_ID}
+AZURE_CLIENT_SECRET=${CLIENT_SECRET}
+AZURE_TENANT_ID=${TENANT_ID}
+AZURE_SUBSCRIPTION_ID=${SUBSCRIPTION_ID}
+AZURE_RESOURCE_GROUP=${RESOURCE_GROUP}
EOF
echo ""
-echo "==> Service principal credentials saved to $ENV_FILE"
+echo "==> Service principal credentials saved to ${ENV_FILE}"
echo ""
echo "================================================"