diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ac1085a..a0cd8cc 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -12,7 +12,8 @@ "extensions": [ "ms-python.python", "ms-azuretools.vscode-docker", - "ms-windows-ai-studio.windows-ai-studio" + "ms-windows-ai-studio.windows-ai-studio", + "marp-team.marp-vscode" ], "settings": { "python.defaultInterpreterPath": "/usr/local/bin/python", diff --git a/AGENTS.md b/AGENTS.md index d717b08..f037155 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,6 +63,39 @@ casually. The coach must always: Use `` in markdown and `# TODO(nitya):` in code for maintainer follow-ups (screenshots, exact commands, model pinning, etc.). +## Known gotchas + +- **Foundry hosted-agent 409 on `azd deploy`.** Reusing the same `azd env` name + after a torn-down deploy can hit stale server-side state on the Foundry agents + data plane, producing `RESPONSE 409: 409 Conflict … modified concurrently` + even though `azd ai agent doctor` sees no agent. Recovery: `azd down --purge + --force` → `azd env new ` → `azd provision` → `azd deploy`. + Captured in the Lab 05 gotcha callout — keep those two in sync. +- **Portal cannot *create* hosted agents.** The Foundry portal can view and + manage hosted agents but has no create flow — `azd deploy` is the only + supported creation path today. Do not add "portal path" instructions for + hosted agents unless the portal ships that flow. +- **`azd provision` fails with `invalid character 'n' after object key:value + pair`.** The four `*Json` params in `infra/main.parameters.json` are + quoted-string substitutions (e.g. `"value": "${AI_PROJECT_DEPLOYMENTS=[]}"`) + and azd 1.30 does not JSON-escape embedded `"` on substitution. Any non-empty + override (e.g. a user-supplied `AI_PROJECT_DEPLOYMENTS` array) breaks the + parameters file. Workaround: `azd env set AI_PROJECT_DEPLOYMENTS "[]"` and + re-provision — the defaults in `infra/main.bicep` still deploy + `gpt-5.4-mini` + `gpt-5.4-judge`. Structural fix (`TODO(nitya)` in + `infra/main.bicep`): change the four params to `array`/`object` types, drop + the `json()` calls, and substitute without surrounding quotes. Kept in sync + with Lab 03 + Lab 05 gotcha callouts. +- **`azd deploy` fails with `RESPONSE 404: ResourceNotFound — Subdomain does + not map to a resource`.** The Foundry agents data plane returns 404 (not + 401) when the caller's bearer token is expired or revoked — typically + `AADSTS50173` after a password change, credential rotation, or Conditional + Access policy update. The account is fine; auth is stale. `azd` and `az` + cache tokens independently, so both must be refreshed: `az logout && az + login --tenant ` then `azd auth logout && azd auth login --tenant-id + `, then re-run `azd deploy`. Kept in sync with the Lab 05 gotcha + callout. + ## Tests Run before opening a PR: diff --git a/README.md b/README.md index b2b0e1b..ae27873 100644 --- a/README.md +++ b/README.md @@ -95,9 +95,24 @@ gh --version > Prefer local setup? Create a virtual environment and install the tools manually. You will need Python 3.13+, `az`, `azd`, `copilot` and `gh` on your PATH. -You are ready to get started on the first lab. -- **Open [`labs/fundamentals/00-overview.md`](./labs/fundamentals/00-overview.md)** - and follow the trail from there. + +
+ +## Fundamentals + +The Fundamentals track gets your Foundry substrate, models, and both agents (Prompt + Hosted) provisioned and verified — so you have a **green baseline** before starting the Core Labs. Complete them in order; Labs 01 and 02 are alternative paths (pick one). + +| # | Lab | Loop node | +|---|-----|-----------| +| 0 | [Course overview & the Agent DevOps loop](./labs/fundamentals/00-overview.md) | Plan | +| 1 | [Provision Foundry with `azd`](./labs/fundamentals/01-provision-azd.md) _(CLI path)_ | Build | +| 2 | [Provision Foundry with the Portal](./labs/fundamentals/02-provision-portal.md) _(UI path)_ | Build | +| 3 | [Deploy the required models](./labs/fundamentals/03-deploy-models.md) | Build | +| 4 | [Create the Prompt Agent](./labs/fundamentals/04-create-prompt-agent.md) | Build | +| 5 | [Deploy the Hosted Agent](./labs/fundamentals/05-deploy-hosted-agent.md) | Deploy | +| 6 | [End-to-end verification](./labs/fundamentals/06-verify.md) | Evaluate | + +Start here → **[`labs/fundamentals/00-overview.md`](./labs/fundamentals/00-overview.md)**.
diff --git a/artifacts/prompts/reference/prompt-agent-baseline-v1.md b/artifacts/prompts/reference/prompt-agent-baseline-v1.md index 8eb41e4..67a3a39 100644 --- a/artifacts/prompts/reference/prompt-agent-baseline-v1.md +++ b/artifacts/prompts/reference/prompt-agent-baseline-v1.md @@ -10,7 +10,7 @@ ## Role -You are the **Contoso Travel Concierge** at Contoso Travel, a fictitious premium +You are the **Contoso Travel Concierge** at Contoso Travel, a premium travel agency that books flights, hotels, and car rentals across Paris, London, Tokyo, Rome, and Cancún. Be warm, professional, and concise. diff --git a/infra/main.bicep b/infra/main.bicep index e49b7e8..79683bf 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -40,6 +40,14 @@ param aiFoundryResourceName string = '' param aiFoundryProjectName string = 'ai-project-${environmentName}' @description('List of model deployments') +// TODO(nitya): The four *Json params below are strings that hold JSON, because +// main.parameters.json substitutes `"${AI_PROJECT_DEPLOYMENTS=[]}"` (quoted). +// azd 1.30 does not escape embedded `"` on substitution, so any non-empty +// override crashes provisioning with `invalid character 'n' after object +// key:value pair`. Structural fix: change these to `array`/`object` params, +// drop the `json()` calls below, and update main.parameters.json to substitute +// without surrounding quotes (`"value": ${AI_PROJECT_DEPLOYMENTS=[]}`). Until +// then, users must keep the override empty — see Lab 03/05 gotcha callouts. param aiProjectDeploymentsJson string = '[]' @description('List of connections') diff --git a/labs/core/05-capstone-hosted.md b/labs/core/05-capstone-hosted.md index 03ce7cf..25af5ba 100644 --- a/labs/core/05-capstone-hosted.md +++ b/labs/core/05-capstone-hosted.md @@ -81,7 +81,7 @@ Do each step yourself. Refer back to the linked lab if you get stuck. cd . # repo root azd down --purge --force ``` - This tears down everything `azd up` created so you stop paying for it. + This tears down everything you provisioned earlier so you stop paying for it. ## ✅ Verify diff --git a/labs/fundamentals/00-overview.md b/labs/fundamentals/00-overview.md index 21c71b7..4c3a5f2 100644 --- a/labs/fundamentals/00-overview.md +++ b/labs/fundamentals/00-overview.md @@ -29,17 +29,17 @@ flowchart LR ## 📋 Steps -1. **Read the [scenario](#scenario) below.** - You should now be able to name the four data sources the Concierge queries. -2. **Skim the [Agent DevOps loop](#agent-devops-loop) diagram.** - You should now recognize the seven nodes and how they connect. -3. **Pick your provisioning path** for the next lab: `azd up` (self-guided) or +1. **Know the [scenario](#scenario).** + You should be able to name the sources for the Concierge queries. +2. **Know the [Agent DevOps loop](#agent-devops-loop).** + You should recognize the nodes and how they connect. +3. **Pick your provisioning path** for the next lab: `azd` (self-guided) or Foundry portal (UI-first). ### Scenario **Contoso Travel** is a fictitious travel agency. Their AI-powered **Contoso -Travel Concierge** orchestrates specialist sub-agents that read three CSVs: +Travel Concierge** orchestrates specialist sub-agents that read three CSVs (below) - also available in JSON format. | Data source | What it holds | |---|---| @@ -69,9 +69,9 @@ cycle you'll run whenever the agent underperforms. Answer these to yourself (no tool required): -- Which three CSVs does the Concierge use? +- Which are the three CSVs used by the Travel Concierge? - What are the seven nodes of the Agent DevOps loop? -- Which path (`azd up` or portal) will you take in Lab 01? +- Which path (`azd` or portal) will you take in Lab 01? If you can answer all three, you're ready to provision. @@ -85,5 +85,5 @@ If you can answer all three, you're ready to provision. Choose one: -- Self-guided → **[Lab 01 — Provision with `azd up`](./01-provision-azd.md)** +- Code-first → **[Lab 01 — Provision with `azd`](./01-provision-azd.md)** - UI-first → **[Lab 02 — Provision with the portal](./02-provision-portal.md)** diff --git a/labs/fundamentals/01-provision-azd.md b/labs/fundamentals/01-provision-azd.md index b17cfa3..e3b72bd 100644 --- a/labs/fundamentals/01-provision-azd.md +++ b/labs/fundamentals/01-provision-azd.md @@ -1,9 +1,10 @@ -# Lab 01 — Provision Foundry with `azd up` +# Lab 01 — Provision Foundry with `azd` > **What you'll do:** Stand up a Foundry project, deploy the `gpt-5.4-mini` and > `gpt-5.4-judge` models, and wire up Application Insights and Log Analytics in -> your Azure subscription with a single command. The container registry and -> hosted agent come later in [Lab 05](./05-deploy-hosted-agent.md). +> your Azure subscription with a single `azd provision` command. The container +> registry and hosted agent come later in +> [Lab 05](./05-deploy-hosted-agent.md). > **Time:** ~15 min · **Prerequisites:** [Lab 00](./00-overview.md) > > ⏩ **Taking the portal path instead?** Skip to [Lab 02](./02-provision-portal.md). @@ -87,16 +88,21 @@ Confirm you have: > project, run 'azd init'`, you're not in the repo root. `cd` to the folder > that contains `azure.yaml` and rerun the command. -4. **Deploy.** +4. **Provision.** ```bash - azd up + azd provision ``` You'll be prompted twice: - **Select an Azure Subscription** — pick the subscription to deploy into. - **`aiDeploymentsLocation` infrastructure parameter** — choose **`(US) East US 2 (eastus2)`** (or one of the alternates above). - `azd up` provisions the Bicep in [`../../infra/`](../../infra/): + > 💡 **Why `azd provision` and not `azd up`?** `azd up` = `azd provision + + > azd deploy`. This lab only needs the Foundry substrate — the hosted-agent + > **deploy** is a deliberate step in [Lab 05](./05-deploy-hosted-agent.md). + > Splitting them keeps failures easy to diagnose and retries cheap. + + `azd provision` runs the Bicep in [`../../infra/`](../../infra/): - resource group (`rg-contoso-travel`) - Foundry account + project - **`gpt-5.4-mini`** (concierge) + **`gpt-5.4-judge`** model deployments @@ -117,31 +123,37 @@ Confirm you have: (✓) Done: Application Insights: appi-xxxxxxxxxxxxx (✓) Done: Foundry project connection: .../appi-xxxxxxxxxxxxx - SUCCESS: Your application was provisioned and deployed to Azure in 1 minute. + SUCCESS: Your application was provisioned in Azure in 1 minute. ``` > 💡 **Want different models?** The golden path deploys `gpt-5.4-mini` and a > `gpt-5.4-judge`. To swap them, set the `AI_PROJECT_DEPLOYMENTS` env var - > before `azd up` — see [Lab 03](./03-deploy-models.md). + > before `azd provision` — see [Lab 03](./03-deploy-models.md). > 💡 **What's *not* here yet:** the Container Registry and the hosted agent - > are **not** provisioned by `azd up` — you create them when you deploy the - > hosted agent in [Lab 05](./05-deploy-hosted-agent.md). + > are **not** provisioned in this lab — you enable and create them when you + > deploy the hosted agent in [Lab 05](./05-deploy-hosted-agent.md). - + 5. **Read the outputs.** - The tail of `azd up` prints the **Foundry project endpoint** and related - environment values. Copy the endpoint into your notes — you'll use it in - every later lab. (The agent name and playground link appear once you deploy - the hosted agent in [Lab 05](./05-deploy-hosted-agent.md).) + The tail of `azd provision` prints the link to the deployed resource group in the Azure Portal. Click to visit the portal - you should see a resource group, with provisioned Foundry project and other resources as shown. -> 💡 **Tip:** `azd env get-values` prints everything `azd` knows about your -> environment. Later labs use this to pick up endpoints automatically. -> -> ⚠️ **Gotcha:** if `azd up` fails on quota, retry in a different region from -> the list above using `azd env set AZURE_LOCATION swedencentral` then -> `azd provision` again. + ![Provisioned `rg-contoso-travel` resource group in the Azure Portal](./images/01-provision-infra-01.png) + +6. **Visit the Foundry portal.** + From the resource group, click into the **Foundry** (`ai-account-xxx`) resource — the overview blade has a **Go to Foundry portal** button in the top toolbar that jumps you straight into your project in the Foundry portal, no separate URL required. + + ![Go to Foundry portal button on the Foundry resource overview](./images/01-provision-infra-02.png) + +7. **Explore the Foundry project.** + You'll land in the **new Microsoft Foundry portal** — confirm the **New Foundry** toggle in the top-right is **on** (the classic view hides most of what we use later). The top nav reflects the developer journey from **Discover** and **Build**, to **Operate** and **Manage**; we'll spend most of the workshop in the **Build** tab (agents, models, prompts). Note that the project Homepage has handy links to the Foundry **endpoint** and **API key** values that you may need later. You can also get these directly using `azd env get-values` - when you provision with azd. Later labs use this to pick up endpoints automatically. + + ![New Microsoft Foundry portal — New Foundry toggle on, Build tab highlighted, endpoint and API key visible on the overview](./images/01-provision-infra-03.png) + + > ⚠️ **Gotcha:** if `azd provision` fails on quota, retry in a different region + > from the list above using `azd env set AZURE_LOCATION swedencentral` then + > `azd provision` again. ## ✅ Verify @@ -163,26 +175,34 @@ AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-5.4-mini" Then open , go to **Resource groups**, and confirm the `rg-contoso-travel` group contains a Foundry account, a Foundry project, -the `gpt-5.4-mini` and `gpt-5.4-judge` model deployments, Application Insights, -and a Log Analytics workspace. +Application Insights, and a Log Analytics workspace. + +> 💡 **Where are the models?** Model deployments (`gpt-5.4-mini`, +> `gpt-5.4-judge`) are sub-resources of the Foundry account, so they don't show +> as separate rows in the Azure Portal resource-group view. To see them, open +> the **Foundry portal**, enable the **new Foundry** toggle (top-right), then +> go to **Build → Models**. Lab 03 walks through this. -> 💡 The Container Registry, AI Search, and Storage show up **later** — after -> [Lab 05](./05-deploy-hosted-agent.md) (hosted agent + registry). +> 💡 The Container Registry, AI Search, and Storage show up **after** you enable +> hosted-agent hosting in [Lab 05](./05-deploy-hosted-agent.md) — the +> `azd env set ENABLE_HOSTED_AGENTS true && azd provision` step there adds them +> to this same resource group, before you ever run `azd deploy`. ## 🧠 Recap -- `azd up` provisioned the Foundry substrate — account, project, the +- `azd provision` stood up the Foundry substrate — account, project, the `gpt-5.4-mini` and `gpt-5.4-judge` models, and observability (App Insights + Log Analytics) — in one shot. +- Splitting **provision** (this lab) from **deploy** ([Lab 05](./05-deploy-hosted-agent.md)) + mirrors the Agent DevOps loop and keeps retries scoped. - Environment values are stored per-`azd env` and reused by later labs. - Next you'll **confirm the model deployments** and learn how to change them. ## ➡️ Next -**[Lab 03 — Deploy required models](./03-deploy-models.md)** to confirm the -model deployment came up, or jump ahead to +**[Lab 03 — Deploy required models](./03-deploy-models.md)** to verify model deployments succeeded, or jump ahead to **[Lab 05 — Deploy the hosted agent](./05-deploy-hosted-agent.md)** to ship the container. diff --git a/labs/fundamentals/03-deploy-models.md b/labs/fundamentals/03-deploy-models.md index 89dc4bd..dd4f23f 100644 --- a/labs/fundamentals/03-deploy-models.md +++ b/labs/fundamentals/03-deploy-models.md @@ -6,7 +6,7 @@ ## 🎯 Goal Deploy the language model(s) your agents and evaluators will call. On the -`azd` path this already happened — `azd up` deployed `gpt-5.4-mini` and +`azd` path this already happened — `azd provision` deployed `gpt-5.4-mini` and `gpt-5.4-judge` for you — so this lab is mostly about **confirming** the deployments and knowing how to add or swap models. On the portal path you deploy the model by hand here. @@ -37,7 +37,7 @@ flowchart LR ## 📋 Steps -**If you provisioned with `azd up` (Lab 01):** +**If you provisioned with `azd` (Lab 01):** 1. Confirm the deployments already exist: ```bash @@ -56,13 +56,20 @@ flowchart LR > 💡 **Want different models?** The golden path deploys `gpt-5.4-mini` and > `gpt-5.4-judge`. To change them, set the `AI_PROJECT_DEPLOYMENTS` env var to a -> JSON array **before** `azd up` (or set it and rerun `azd provision`). Setting -> it replaces the default entirely, so include **every** model you want: +> JSON array **before** `azd provision` (or set it and rerun `azd provision`). +> Setting it replaces the default entirely, so include **every** model you want: > > ```bash > azd env set AI_PROJECT_DEPLOYMENTS '[{"name":"gpt-5.4-mini","model":{"name":"gpt-5.4-mini","format":"OpenAI","version":"2026-03-17"},"sku":{"name":"GlobalStandard","capacity":100}},{"name":"gpt-5.4-judge","model":{"name":"gpt-5.4","format":"OpenAI","version":"2026-03-05"},"sku":{"name":"GlobalStandard","capacity":100}}]' > azd provision > ``` +> +> ⚠️ **Known limitation:** once this override is set, later re-provisions +> (e.g. enabling hosted agents in [Lab 05](./05-deploy-hosted-agent.md)) fail +> with `invalid character 'n' after object key:value pair` — azd 1.30 doesn't +> escape the JSON when it substitutes into the Bicep parameters file. Clear it +> before re-provisioning (`azd env set AI_PROJECT_DEPLOYMENTS "[]"`). Tracked +> as `TODO(nitya)` in [`infra/main.bicep`](../../infra/main.bicep). **If you provisioned with the portal (Lab 02):** diff --git a/labs/fundamentals/05-deploy-hosted-agent.md b/labs/fundamentals/05-deploy-hosted-agent.md index 50b5d19..9943a1a 100644 --- a/labs/fundamentals/05-deploy-hosted-agent.md +++ b/labs/fundamentals/05-deploy-hosted-agent.md @@ -22,9 +22,9 @@ flowchart LR ## Before you start -The hosted agent is **not** deployed by `azd up` in Lab 01 — that step only -provisioned the Foundry substrate and models. Deploying the container is its own -deliberate step, which is what you do here. +The hosted agent is **not** deployed by `azd provision` in Lab 01 — that step +only stands up the Foundry substrate and models. Deploying the container is its +own deliberate step, which is what you do here. The hosted agent is declared as a **service** in [`azure.yaml`](../../azure.yaml) (`host: azure.ai.agent`). Deploying it needs two things that Lab 01 didn't set @@ -32,7 +32,7 @@ up: the `azure.ai.agents` **azd extension** (the CLI commands) and the **hosted-agent hosting** infrastructure (a container registry + agent capability host). You'll enable both below. -## 📋 Steps — deploy with `azd` +## 📋 Steps 1. **Make sure the hosted-agents extension is installed.** The `azd ai agent` commands and hosted `azd deploy` come from the @@ -46,13 +46,28 @@ host). You'll enable both below. 2. **Enable hosted-agent hosting and provision it.** The container registry and agent capability host aren't created by the - default `azd up`. Turn them on, then provision: + default `azd provision` in Lab 01. Turn them on, then provision: ```bash azd env set ENABLE_HOSTED_AGENTS true azd provision ``` This adds the registry + capability host to your existing resource group. + > ⚠️ **Gotcha — `invalid character 'n' after object key:value pair`.** + > `azd provision` fails when your env has a custom `AI_PROJECT_DEPLOYMENTS` + > (or `AI_PROJECT_CONNECTIONS` / `_CREDENTIALS` / `_DEPENDENT_RESOURCES`) + > JSON value set — azd 1.30 doesn't escape the quotes when it substitutes + > it into the Bicep parameters file. Clear it and re-run; the defaults in + > `infra/main.bicep` still deploy `gpt-5.4-mini` + `gpt-5.4-judge`: + > + > ```bash + > azd env set AI_PROJECT_DEPLOYMENTS "[]" + > azd provision + > ``` + > + > Need a custom deployment list? See the workaround in + > [Lab 03](./03-deploy-models.md). + 3. **Deploy the hosted agent.** ```bash azd deploy contoso-travel-concierge @@ -95,20 +110,45 @@ host). You'll enable both below. In the Foundry portal → **Build → Agents → contoso-travel-concierge → Try in playground**. Ask the same question. Same answer, richer trace. -## 📋 Steps — deploying via the portal (portal path only) - -1. **Open the portal → Build → Agents → + New agent → Hosted agent**. -2. Provide: - - **Agent name:** `contoso-travel-concierge` - - **Container image:** upload or point to a registry image built from `src/` - - **Model deployment:** `gpt-5.4-mini` (from Lab 03) -3. Click **Deploy** and wait for status **Ready**. - - - -> ⚠️ **Gotcha (portal path):** the container image must expose the Responses -> protocol on port 8088 — the shipped `Dockerfile` already does this. Build -> from `src/` unmodified for your first deploy. +> 🧭 **No portal path?** Correct — the current Foundry portal can **view and +> manage** hosted agents but does not have a *create* flow for them. `azd +> deploy` is the one supported way to publish a hosted agent today. + +> ⚠️ **Gotcha — 409 Conflict on `azd deploy`.** If deploy fails with +> `RESPONSE 409: 409 Conflict … The resource already exists or was modified +> concurrently. Please retry.` and a plain retry doesn't clear it (portal may +> also show *"Project not found"* on the Agents blade), the Foundry data plane +> is holding a stale registration keyed on the current project/agent name. +> Recovery: +> +> ```bash +> azd down --purge --force # tear down + purge soft-delete +> azd env new contoso-travel-v2 # any new name → new account+project hash +> azd env set ENABLE_HOSTED_AGENTS true +> azd provision +> azd deploy contoso-travel-concierge +> ``` +> +> The new env name changes the `uniqueString()` hash used by the infra, which +> gives you a genuinely fresh Foundry project and clears the cache. + +> ⚠️ **Gotcha — 404 "Subdomain does not map to a resource" on `azd deploy`.** +> The Foundry agents data plane returns `RESPONSE 404: ResourceNotFound — +> Subdomain does not map to a resource` when the caller's bearer token is +> expired or revoked (typical trigger: `AADSTS50173` — a password change, +> credential rotation, or Conditional Access policy moved `TokensValidFrom` +> past your issued-at time). The account is fine; auth is stale. `azd deploy` +> uses its own token cache separate from `az`, so **both** must be refreshed: +> +> ```bash +> az logout && az login --tenant +> azd auth logout && azd auth login --tenant-id +> azd deploy contoso-travel-concierge +> ``` +> +> Quick sanity check that the resource itself is healthy (should return `HTTP +> 200` even unauthenticated): +> `curl -sS -o /dev/null -w "%{http_code}\n" https://$(azd env get-value AZURE_AI_ACCOUNT_NAME).services.ai.azure.com/` ## Redeploying after a code change diff --git a/labs/fundamentals/images/01-provision-infra-01.png b/labs/fundamentals/images/01-provision-infra-01.png new file mode 100644 index 0000000..6f6319f Binary files /dev/null and b/labs/fundamentals/images/01-provision-infra-01.png differ diff --git a/labs/fundamentals/images/01-provision-infra-02.png b/labs/fundamentals/images/01-provision-infra-02.png new file mode 100644 index 0000000..62b4c88 Binary files /dev/null and b/labs/fundamentals/images/01-provision-infra-02.png differ diff --git a/labs/fundamentals/images/01-provision-infra-03.png b/labs/fundamentals/images/01-provision-infra-03.png new file mode 100644 index 0000000..5bb6f10 Binary files /dev/null and b/labs/fundamentals/images/01-provision-infra-03.png differ