Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ Legend: ✅ complete · 🟡 stub/partial · ❌ not started · – not applicab
| Model deployment | ✅ | ✅ | ✅ | `fundamentals/03-deploy-models.md` — full body (azd + portal paths) |
| Prompt Agent creation | ✅ | – | ✅ | `fundamentals/04-create-prompt-agent.md` — references baseline artifact |
| Hosted Agent source (`src/` + `src.original/`) | – | ✅ | ✅ | main.py, agent.yaml, Dockerfile, instructions/, scripts/ ported + rebranded |
| Hosted Agent deploy lab | – | ✅ | ✅ | `fundamentals/05-deploy-hosted-agent.md` — full body |
| Hosted Agent deploy lab | – | ✅ | ✅ | `fundamentals/05-deploy-hosted-agent.md` — portal linker normalizes project name and persists ID + endpoint metadata before deploy |
| Bicep infra (`infra/`) | – | ✅ | ✅ | main.bicep, params, core/{ai,host,monitor,search,storage} ported + rebranded |
| Fundamentals verify lab | – | – | ✅ | `fundamentals/06-verify.md` — canonical question, both agents |
| Core: observability in portal | ✅ | partial | ✅ | `core/01-observe-portal.md` — playground + live evaluators + traces |
Expand Down
14 changes: 13 additions & 1 deletion labs/fundamentals/05-deploy-hosted-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,19 @@ host). You'll enable both below.
> ```
>
> The script auto-discovers the RG, Foundry account, and project, then binds
> them into an `azd env` named `contoso-travel`. Idempotent — safe to re-run.
> them into an `azd env` named `contoso-travel`. Verify the project metadata
> before provisioning:
>
> ```bash
> azd env get-values | grep -E \
> "AZURE_AI_PROJECT_(NAME|ID|ENDPOINT)|FOUNDRY_PROJECT_ENDPOINT"
> azd ai project show
> ```
>
> `AZURE_AI_PROJECT_NAME` must contain only the child name (for example,
> `contoso-travel-project`), not `account/project`. The project ID and both
> endpoint values must also be present, and `azd ai project show` must resolve
> the same endpoint. The linker is idempotent and safe to re-run.
> **Skip this if you did Lab 01 (`azd`) already.**

1. **Make sure the hosted-agents extension is installed.**
Expand Down
27 changes: 21 additions & 6 deletions scripts/link-portal-rg.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ az account show >/dev/null 2>&1 || {
echo "❌ 'az' is not logged in. Run: az login --use-device-code"; exit 1; }

# --- discover the RG + Foundry resources -----------------------------------
LOC=$(az group show -n "$RG" --query location -o tsv 2>/dev/null) || {
RG_LOC=$(az group show -n "$RG" --query location -o tsv 2>/dev/null) || {
echo "❌ Resource group '$RG' not found in the current subscription."
echo " Pass a different name: ./scripts/link-portal-rg.sh <rg-name>"
exit 1
Expand All @@ -44,10 +44,17 @@ if [[ -z "$ACCT" || "$ACCT" == "null" ]]; then
exit 1
fi

# Project name — best-effort. Older API versions omit the 'project list' verb;
# in that case the workshop uses the default pattern 'ai-project-<env>'.
PROJ=$(az cognitiveservices account project list \
-g "$RG" --name "$ACCT" --query "[0].name" -o tsv 2>/dev/null || true)
# Project metadata — best-effort. Azure returns the name as 'account/project',
# while Bicep expects only the child project name.
PROJ_JSON=$(az cognitiveservices account project list \
-g "$RG" --name "$ACCT" --query "[0]" -o json 2>/dev/null || true)
[[ -n "$PROJ_JSON" ]] || PROJ_JSON='{}'
PROJ_QUALIFIED=$(jq -r '.name // empty' <<<"$PROJ_JSON")
PROJ="${PROJ_QUALIFIED##*/}"
PROJ_ID=$(jq -r '.id // empty' <<<"$PROJ_JSON")
PROJ_ENDPOINT=$(jq -r '.properties.endpoints["AI Foundry API"] // empty' <<<"$PROJ_JSON")
PROJ_LOC=$(jq -r '.location // empty' <<<"$PROJ_JSON")
LOC="${PROJ_LOC:-$RG_LOC}"

# --- bind the azd env ------------------------------------------------------
if azd env list -o json 2>/dev/null | jq -e ".[] | select(.Name==\"$ENV\")" >/dev/null; then
Expand All @@ -61,6 +68,14 @@ azd env set AZURE_RESOURCE_GROUP "$RG"
azd env set AZURE_LOCATION "$LOC"
azd env set AZURE_AI_ACCOUNT_NAME "$ACCT"
[[ -n "$PROJ" && "$PROJ" != "null" ]] && azd env set AZURE_AI_PROJECT_NAME "$PROJ"
if [[ -n "$PROJ_ID" && "$PROJ_ID" != "null" ]]; then
azd env set AZURE_AI_PROJECT_ID "$PROJ_ID"
azd env set AZURE_AI_FOUNDRY_PROJECT_ID "$PROJ_ID"
fi
if [[ -n "$PROJ_ENDPOINT" && "$PROJ_ENDPOINT" != "null" ]]; then
azd env set AZURE_AI_PROJECT_ENDPOINT "$PROJ_ENDPOINT"
azd env set FOUNDRY_PROJECT_ENDPOINT "$PROJ_ENDPOINT"
fi
azd env set USE_EXISTING_AI_PROJECT true
azd env set AI_PROJECT_DEPLOYMENTS "[]" # sidestep the JSON-escape gotcha in Lab 05

Expand All @@ -71,7 +86,7 @@ echo " Foundry account: $ACCT"
[[ -n "$PROJ" && "$PROJ" != "null" ]] && echo " Foundry project: $PROJ"
echo ""
echo "Values now set (subscription id truncated for readability):"
azd env get-values | grep -E "^(AZURE_(SUBSCRIPTION_ID|RESOURCE_GROUP|LOCATION|AI_ACCOUNT_NAME|AI_PROJECT_NAME))|^USE_EXISTING_AI_PROJECT|^AI_PROJECT_DEPLOYMENTS" \
azd env get-values | grep -E "^(AZURE_(SUBSCRIPTION_ID|RESOURCE_GROUP|LOCATION|AI_ACCOUNT_NAME|AI_PROJECT_NAME|AI_PROJECT_ID|AI_FOUNDRY_PROJECT_ID|AI_PROJECT_ENDPOINT)|FOUNDRY_PROJECT_ENDPOINT)|^USE_EXISTING_AI_PROJECT|^AI_PROJECT_DEPLOYMENTS" \
| sed -E 's|(AZURE_SUBSCRIPTION_ID=")([^"]{4})[^"]*(")|\1\2…\3|'
echo ""
echo "Next: run Lab 05 Step 2 (azd provision) — it will reuse '$RG' instead of"
Expand Down
4 changes: 3 additions & 1 deletion specs/course.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ phases:
loop_node: deploy
time_min: 15
prereqs: ["fundamentals/03-deploy-models"]
produces: [{type: config, name: hosted-agent-endpoint}]
produces:
- {type: config, name: portal-azd-project-link}
- {type: config, name: hosted-agent-endpoint}
consumes: []
- id: "06-verify"
title: Smoke-test both agents
Expand Down
91 changes: 91 additions & 0 deletions tests/test_link_portal_rg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""Portal-first linker writes canonical Foundry project metadata."""
from __future__ import annotations

import os
import subprocess
from pathlib import Path

from conftest import REPO_ROOT


LINKER = REPO_ROOT / "scripts" / "link-portal-rg.sh"


def _write_executable(path: Path, content: str) -> None:
path.write_text(content)
path.chmod(0o755)


def test_linker_normalizes_project_name_and_sets_project_metadata(tmp_path):
bin_dir = tmp_path / "bin"
bin_dir.mkdir()
azd_log = tmp_path / "azd.log"

_write_executable(
bin_dir / "az",
"""#!/usr/bin/env bash
set -euo pipefail
case "$*" in
"account show") echo '{}' ;;
"account show --query id -o tsv") echo '00000000-0000-0000-0000-000000000000' ;;
"group show -n rg-test --query location -o tsv") echo 'eastus' ;;
"cognitiveservices account list -g rg-test --query "*) echo 'foundry-test' ;;
"cognitiveservices account project list -g rg-test --name foundry-test --query [0] -o json")
cat <<'JSON'
{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.CognitiveServices/accounts/foundry-test/projects/travel-project","location":"eastus2","name":"foundry-test/travel-project","properties":{"endpoints":{"AI Foundry API":"https://foundry-test.services.ai.azure.com/api/projects/travel-project"}}}
JSON
;;
*) echo "unexpected az command: $*" >&2; exit 1 ;;
esac
""",
)
_write_executable(
bin_dir / "azd",
"""#!/usr/bin/env bash
set -euo pipefail
case "$*" in
"env list -o json") echo '[]' ;;
"env new test-env --no-prompt") ;;
"env set "*) printf '%s\n' "$*" >> "$AZD_LOG" ;;
"env get-values")
cat <<'VALUES'
AZURE_SUBSCRIPTION_ID="00000000-0000-0000-0000-000000000000"
AZURE_RESOURCE_GROUP="rg-test"
AZURE_LOCATION="eastus2"
AZURE_AI_ACCOUNT_NAME="foundry-test"
AZURE_AI_PROJECT_NAME="travel-project"
AZURE_AI_PROJECT_ID="project-id"
AZURE_AI_FOUNDRY_PROJECT_ID="project-id"
AZURE_AI_PROJECT_ENDPOINT="project-endpoint"
FOUNDRY_PROJECT_ENDPOINT="project-endpoint"
USE_EXISTING_AI_PROJECT="true"
AI_PROJECT_DEPLOYMENTS="[]"
VALUES
;;
*) echo "unexpected azd command: $*" >&2; exit 1 ;;
esac
""",
)

env = os.environ.copy()
env["PATH"] = f"{bin_dir}:{env['PATH']}"
env["AZD_LOG"] = str(azd_log)
result = subprocess.run(
[str(LINKER), "rg-test", "test-env"],
cwd=REPO_ROOT,
env=env,
capture_output=True,
text=True,
)

assert result.returncode == 0, result.stderr
writes = azd_log.read_text().splitlines()
assert "env set AZURE_LOCATION eastus2" in writes
assert "env set AZURE_AI_PROJECT_NAME travel-project" in writes
assert not any("foundry-test/travel-project" in write for write in writes)
project_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.CognitiveServices/accounts/foundry-test/projects/travel-project"
assert f"env set AZURE_AI_PROJECT_ID {project_id}" in writes
assert f"env set AZURE_AI_FOUNDRY_PROJECT_ID {project_id}" in writes
endpoint = "https://foundry-test.services.ai.azure.com/api/projects/travel-project"
assert f"env set AZURE_AI_PROJECT_ENDPOINT {endpoint}" in writes
assert f"env set FOUNDRY_PROJECT_ENDPOINT {endpoint}" in writes
Loading