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: -

- -

+![Application](./images/application.png) At the completion of this walkthrough, the application and Radius configuration will look like: -

- -

+![Config](./images/radius-config.png) ## 📁 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 "================================================"