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
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@ The data lives in **Microsoft Fabric** (a governed Lakehouse with inventory, dem

![The notebook with InventoryLakehouse attached as the default Lakehouse and the Run all button highlighted](../images/challenge-00-run-all.png)

> [!IMPORTANT]
> **First run on a fresh capacity — the table-selection step may need a moment (or a manual tick).** The publish cell selects your 7 tables automatically, but on a **cold F2 capacity** the Lakehouse's **SQL analytics endpoint** can take a couple of minutes to expose the newly-written tables. Until that schema sync finishes you may see **either**:
> - the notebook raise **`No tables were selected (schema may still be syncing)`**, or
> - an **`Error updating data agent data source selection(s)`** toast if you tick tables in the portal too early.
>
> This is expected first-run timing, **not** a failure — your 7 tables always write successfully; only the *agent's selection* waits on the sync. To finish it:
> 1. Open **`InventoryLakehouse`** → switch the view (top-right) from **Lakehouse** to **SQL analytics endpoint** → expand **`dbo → Tables`** and confirm all **7 tables** are listed. If they aren't yet, wait ~1–2 min and **Refresh** (opening the endpoint also nudges the sync).
> 2. Then **either** re-run the notebook's publish cell (it's idempotent), **or** open the **`inventory-hack-agent`** data agent → tick **all 7 tables** in the **Explorer** (left pane) → **Publish**.

**d. Copy your two IDs.**
When it finishes, the **last cell prints your Workspace ID and Agent ID**. Note both — you paste them into the Fabric Data Agent tool in Challenge 2.

Expand Down Expand Up @@ -233,6 +242,8 @@ The data lives in **Microsoft Fabric** (a governed Lakehouse with inventory, dem
| The setup notebook errors on `%pip install` or the publish cell | Preview SDK drift — re-run the failed cell; if a method name differs, check the [SDK reference](https://learn.microsoft.com/fabric/data-science/fabric-data-agent-sdk). Make sure your F2 capacity is **running**. |
| **Fabric Data Agent** isn't in the tool catalogue (previewing ahead) | Your F2 capacity must be running, and the setup notebook must have **published** the agent. |
| The agent replies *"I'm unable to access the data source"* (or *"No tables selected yet"*) | Its tables aren't selected. The setup notebook selects all seven automatically, but if it didn't: open the data agent → **Data** tab → tick **all** tables under `InventoryLakehouse → dbo` → **Publish**, then re-test. |
| The publish cell raises **`No tables were selected (schema may still be syncing)`** | Expected on a **cold F2** — the 7 tables wrote fine, but the Lakehouse **SQL analytics endpoint** hasn't exposed them yet. Open `InventoryLakehouse` → **SQL analytics endpoint** → confirm all 7 tables under `dbo`, then **re-run the publish cell** (idempotent) or tick the tables in the data agent's **Explorer** and **Publish**. |
| Ticking tables in the data agent shows **`Error updating data agent data source selection(s)`** | Same root cause — the SQL endpoint schema isn't fully synced. Wait until all 7 tables show under the Lakehouse's **SQL analytics endpoint**, **refresh** the data agent page (F5), then re-tick and **Publish**. If it persists, remove and re-add the `InventoryLakehouse` data source (**+ Add data source → OneLake catalog**), tick the 7 tables, and **Publish**. |

![The data agent's Data tab showing InventoryLakehouse tables unselected and the agent replying that it cannot access the data source](../images/challenge-00-no-tables.png)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ Everything for this hack is provisioned per attendee by [`deploy-lab.ps1`](deplo
| Resource | Type | Notes |
|----------|------|-------|
| Azure AI Foundry account | `Microsoft.CognitiveServices/accounts` | One per attendee (AIServices kind) |
| Foundry project | `…/projects/inventory-hack` | Within the Foundry account |
| Foundry project | `…/projects/inventory-hack` | System-assigned managed identity; provisioning is verified before the lab continues |
| gpt-5.4-mini model deployment | GlobalStandard, 100K TPM | Used by all three hack agents |
| Fabric **F2 capacity** | `Microsoft.Fabric/capacities` | One per attendee; attendee set as **capacity admin** |

The script sets each attendee as **admin of their own F2 capacity** (resolving their Entra object ID to a UPN via the platform `Get-MhhLabUser` helper), so they can create a workspace, assign it to the capacity, and publish their own Data Agent — no shared state, no cross-attendee contention.
The script grants the **Foundry User** role to each attendee and to the Foundry project's managed identity at the Foundry account scope. It also sets each attendee as **admin of their own F2 capacity** (resolving their Entra object ID to a UPN via the platform `Get-MhhLabUser` helper), so they can create a workspace, assign it to the capacity, and publish their own Data Agent — no shared state, no cross-attendee contention.

## What the attendee does themselves (Challenge 1)

Expand All @@ -24,16 +24,18 @@ The script sets each attendee as **admin of their own F2 capacity** (resolving t

| Key | Value |
|-----|-------|
| `FoundryProjectEndpoint` | `https://<account>.services.ai.azure.com/api/projects/inventory-hack` |
| `FoundryProjectUrl` | Clickable deep-link to the attendee's `inventory-hack` project in the Foundry portal (`ai.azure.com`) — where they build the agents in Challenges 2-5 |
| `FoundryProjectEndpoint` | `https://<account>.services.ai.azure.com/api/projects/inventory-hack` (SDK/API endpoint; optional — the hack is done in the portal) |
| `ModelDeploymentName` | `gpt-5.4-mini` |
| `FabricCapacityName` | The attendee's own F2 capacity (assign your workspace to it in Challenge 1) |
| `FabricPortalUrl` | Link to the Fabric portal (`app.fabric.microsoft.com`) to create the workspace on `FabricCapacityName` — the workspace URL itself only exists once the attendee creates it in Challenge 1 |
| `ResourceGroupName` | The attendee's resource group |

Attendees use these in the Foundry and Fabric portals — no SDK or code required.

## Prerequisites for facilitators / the platform

- The deploying identity (the platform **service principal**) needs **Owner** on each attendee resource group (creates the Foundry account, model deployment, and Fabric capacity via ARM) — the platform grants this automatically for `resourcegroup` deployments.
- The deploying identity (the platform **service principal**) needs **Owner** on each attendee resource group so it can create resources and assign the **Foundry User** role to the attendee and project managed identity — the platform grants this automatically for `resourcegroup` deployments.
- **`groups: ["M365-E5-Users"]`** in [`lab-defaults.json`](lab-defaults.json) — each attendee's lab Entra user gets a **Microsoft 365 E5** license (includes **Power BI Pro**), which is what lets them sign into the Fabric portal ([app.fabric.microsoft.com](https://app.fabric.microsoft.com)) and create a workspace. Without a Fabric/Power BI license the attendee cannot open the portal.
- **The lab tenant must have Fabric enabled** — a Fabric **tenant-admin** setting (*“Users can create Fabric items”* / workspaces). This is **not** settable via `lab-defaults.json`; the tenant administrator for the lab tenant must turn it on, or attendees cannot create a workspace even with a license.
- **Fabric F-SKU quota** in a preferred region (F2 = 2 CU; a 512-CU subscription supports ~256 attendees). `swedencentral` and `norwayeast` are good defaults.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ $ErrorActionPreference = "Stop"
$candidateRegions = if ($PreferredLocation.Count -gt 0) { $PreferredLocation } else { @("swedencentral", "westeurope", "norwayeast") }
$effectiveLocation = $candidateRegions[0]
$effectiveRG = $ResourceGroupName
$stableHash = (Get-MhhStableHash $AllowedEntraUserIds -Length 12).ToLower()

if ($AllowedEntraUserIds.Count -eq 0) {
throw "At least one attendee object ID must be supplied in AllowedEntraUserIds."
}
$stableHash = (Get-MhhStableHash $AllowedEntraUserIds -Length 12).ToLower()

# For 'subscription' deployments the platform does NOT pre-create a resource group
# ($ResourceGroupName arrives empty). Create one ourselves with a deterministic,
Expand Down Expand Up @@ -170,29 +174,102 @@ if ($existingAccount) {
# ---------------------------------------------------------------------------
# Foundry project
# ---------------------------------------------------------------------------
$accountScope = "/subscriptions/$SubscriptionId/resourceGroups/$effectiveRG" +
"/providers/Microsoft.CognitiveServices/accounts/$foundryAccountName"
$projectUri = "/subscriptions/$SubscriptionId/resourceGroups/$effectiveRG" +
"/providers/Microsoft.CognitiveServices/accounts/$foundryAccountName" +
"/projects/${foundryProjectName}?api-version=2025-04-01-preview"

$existingProject = Invoke-AzRestMethod -Method GET -Path $projectUri -ErrorAction SilentlyContinue
if ($existingProject.StatusCode -ne 200) {
$projectBody = @{
location = $effectiveLocation
properties = @{ description = "Agentic Inventory Planning MicroHack" }
} | ConvertTo-Json -Depth 3
$projectBody = @{
location = $effectiveLocation
identity = @{ type = "SystemAssigned" }
properties = @{
description = "Agentic Inventory Planning MicroHack"
displayName = "Inventory Planning MicroHack"
}
} | ConvertTo-Json -Depth 4

$projectResponse = Invoke-AzRestMethod -Method PUT -Path $projectUri -Payload $projectBody
if ($projectResponse.StatusCode -ge 400) {
throw "Foundry project create or update failed (HTTP $($projectResponse.StatusCode)): $($projectResponse.Content)"
}

Invoke-AzRestMethod -Method PUT -Path $projectUri -Payload $projectBody | Out-Null
$projectResource = $null
$elapsed = 0
do {
Start-Sleep -Seconds 8
$elapsed += 8
$projectResponse = Invoke-AzRestMethod -Method GET -Path $projectUri -ErrorAction SilentlyContinue
if ($projectResponse.StatusCode -eq 200) {
$projectResource = $projectResponse.Content | ConvertFrom-Json
if ($projectResource.properties.provisioningState -eq "Failed") {
throw "Foundry project provisioning failed: $($projectResponse.Content)"
}
}
} while (
($projectResponse.StatusCode -ne 200 -or
$projectResource.properties.provisioningState -ne "Succeeded" -or
-not $projectResource.identity.principalId) -and
($elapsed -lt 180)
)

$elapsed = 0
do { Start-Sleep -Seconds 8; $elapsed += 8
$r = Invoke-AzRestMethod -Method GET -Path $projectUri -ErrorAction SilentlyContinue
} while (($r.StatusCode -ne 200) -and ($elapsed -lt 90))
if ($projectResponse.StatusCode -ne 200 -or
$projectResource.properties.provisioningState -ne "Succeeded" -or
-not $projectResource.identity.principalId) {
throw "Foundry project did not reach Succeeded with a managed identity within 180 seconds. Last response: $($projectResponse.Content)"
}
Write-Host "[OK] Foundry project '$foundryProjectName' is ready."

# Foundry User (formerly Azure AI User) enables project access and agent operations.
$foundryUserRoleId = "53ca6127-db72-4b80-b1b0-d745d6d5456d"

function Grant-FoundryUserRole {
param(
[Parameter(Mandatory=$true)]
[string]$PrincipalId,

[Parameter(Mandatory=$true)]
[string]$PrincipalDescription
)

$existingAssignment = Get-AzRoleAssignment `
-ObjectId $PrincipalId `
-Scope $accountScope `
-RoleDefinitionId $foundryUserRoleId `
-ErrorAction SilentlyContinue
if ($existingAssignment) {
Write-Host "[OK] Foundry User already assigned to $PrincipalDescription."
return
}

Write-Host "[OK] Foundry project '$foundryProjectName' created."
} else {
Write-Host "[OK] Foundry project already exists — skipping."
$lastError = $null
for ($attempt = 1; $attempt -le 6; $attempt++) {
try {
New-AzRoleAssignment `
-ObjectId $PrincipalId `
-RoleDefinitionId $foundryUserRoleId `
-Scope $accountScope `
-ErrorAction Stop | Out-Null
Write-Host "[OK] Granted Foundry User to $PrincipalDescription."
return
} catch {
$lastError = $_
if ($attempt -lt 6) {
Start-Sleep -Seconds 10
}
}
}

throw "Could not grant Foundry User to $PrincipalDescription after 6 attempts: $lastError"
}

foreach ($userId in $AllowedEntraUserIds) {
Grant-FoundryUserRole -PrincipalId $userId -PrincipalDescription "attendee '$userId'"
}
Grant-FoundryUserRole `
-PrincipalId $projectResource.identity.principalId `
-PrincipalDescription "project managed identity '$($projectResource.identity.principalId)'"

# ---------------------------------------------------------------------------
# gpt-5.4-mini model deployment (ACCOUNT-scoped, GlobalStandard capacity 100).
# Matches the pro-code inventory hack: a low-cost GPT-5 model whose Foundry model
Expand Down Expand Up @@ -308,14 +385,31 @@ if ($adminUpns.Count -eq 0) {
# ---------------------------------------------------------------------------
# Return credentials to the attendee dashboard
# ---------------------------------------------------------------------------
$tenantId = (Get-AzContext).Tenant.Id
$projectEndpoint = "https://$foundryAccountName.services.ai.azure.com/api/projects/$foundryProjectName"

# Clickable Foundry portal deep-link to THIS project. The ai.azure.com portal encodes the
# subscription GUID as a base64url token (in string/hex byte order — NOT Guid.ToByteArray()),
# followed by <rg>,,<account>,<project>. Giving attendees this link means they don't have to
# hunt for their project in the portal.
$subHex = $SubscriptionId -replace '-', ''
$subBytes = for ($i = 0; $i -lt $subHex.Length; $i += 2) { [Convert]::ToByte($subHex.Substring($i, 2), 16) }
$subToken = [Convert]::ToBase64String([byte[]]$subBytes).TrimEnd('=').Replace('+', '-').Replace('/', '_')
$projectPortalUrl = "https://ai.azure.com/nextgen/r/$subToken,$effectiveRG,,$foundryAccountName,$foundryProjectName/home?tid=$tenantId"
$fabricPortalUrl = "https://app.fabric.microsoft.com/home?tid=$tenantId"

Write-Host "[OK] Lab provisioning complete."

@{ HackboxCredential = @{
name = "FoundryProjectUrl"
value = $projectPortalUrl
note = "Open your Foundry project directly (sign in with your lab user). This is where you build the agents in Challenges 2-5."
} }

@{ HackboxCredential = @{
name = "FoundryProjectEndpoint"
value = $projectEndpoint
note = "Open ai.azure.com → select this project OR use as SDK endpoint"
note = "SDK / API endpoint for your project (optional - the hack is done in the portal via FoundryProjectUrl)"
} }

@{ HackboxCredential = @{
Expand All @@ -330,6 +424,12 @@ Write-Host "[OK] Lab provisioning complete."
note = "Your own Fabric F2 capacity. In Challenge 1 you create a workspace and assign it to this capacity, then Run All the setup notebook to publish your Data Agent."
} }

@{ HackboxCredential = @{
name = "FabricPortalUrl"
value = $fabricPortalUrl
note = "Open the Fabric portal (sign in with your lab user) to create your workspace on your FabricCapacityName in Challenge 1. Your workspace URL doesn't exist until you create it there."
} }

@{ HackboxCredential = @{
name = "ResourceGroupName"
value = $effectiveRG
Expand Down