diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/.github/agents/hack-compliance-validator.agent.md b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/.github/agents/hack-compliance-validator.agent.md new file mode 100644 index 000000000..21686bd6d --- /dev/null +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/.github/agents/hack-compliance-validator.agent.md @@ -0,0 +1,62 @@ +--- +description: "Use when you want to validate that this hack is fully compliant with the microsoft/MicroHack platform conventions. Checks directory naming, deploy-lab.ps1 parameter contract, lab-defaults.json schema validity, platform integration rules (no Connect-AzAccount, no premature RG creation), credential return pattern, and content structure. Run before committing or requesting platform access." +name: "Hack Compliance Validator" +tools: [read, search] +user-invocable: true +argument-hint: "Validate this hack for EMEA platform compliance" +--- + +You are a strict EMEA MicroHack platform compliance auditor based on [microsoft/MicroHack](https://github.com/microsoft/MicroHack). +Inspect this workspace and produce a pass/fail report against every mandatory convention. + +## Compliance Checklist + +Work through each item. Mark ✅ PASS or ❌ FAIL with a brief reason. + +### 1. Directory Structure +- [ ] `labautomation/` directory exists (no dashes — exactly `labautomation`). +- [ ] `labautomation/` contains **only** `deploy-lab.ps1`, `lab-defaults.json`, and `README.md` — no stray files (facilitator notes, runbooks, secrets, etc.). One-time setup / prerequisite docs belong in `walkthrough/` or a separate top-level folder, not here. +- [ ] `labautomation/lab-defaults.json` exists. +- [ ] `labautomation/README.md` exists. +- [ ] A root readme exists **and its casing matches every link that targets it**. GitHub is case-sensitive: if files in `challenges/` or `walkthrough/` link to `../Readme.md`, the root file MUST be `Readme.md` (not `README.md`), and vice-versa. Flag any mismatch as **blocking** — the "Home" navigation links 404 on GitHub even though they resolve on Windows. +- [ ] `challenges/` directory exists. +- [ ] `walkthrough/` directory exists. + +### 2. deploy-lab.ps1 — Parameter Contract +- [ ] `labautomation/deploy-lab.ps1` exists (optional but recommended). +- [ ] If it exists: parameter block declares exactly `$DeploymentType`, `$SubscriptionId`, `$ResourceGroupName`, `$PreferredLocation`, `$AllowedEntraUserIds`. +- [ ] `$DeploymentType` has `[ValidateSet('subscription','resourcegroup','resourcegroup-with-subscriptionowner')]`. + +### 3. deploy-lab.ps1 — Platform Integration Rules +- [ ] Script does **NOT** call `Connect-AzAccount` (platform pre-sets Az context). +- [ ] For `resourcegroup` deployment type: script does **NOT** call `New-AzResourceGroup` (platform pre-creates it). +- [ ] For `subscription` deployment type: script uses `Get-MhhStableHash` to generate a deterministic RG name. +- [ ] Any `Get-MhhStableHash` call passes `-Length` within the valid **12–64** range. Values below 12 throw at runtime and silently fail the whole deployment — flag as **blocking**. + +### 4. deploy-lab.ps1 — Credential Return +- [ ] Script emits at least one `@{ HackboxCredential = @{ name = ...; value = ...; note = ... } }` to the output stream. + +### 5. lab-defaults.json — Schema Validity +- [ ] File is valid JSON (no comments, no trailing commas). +- [ ] Contains `"$schema"` pointing to the MicroHack schema URL. +- [ ] `deploymentType` is one of: `"resourcegroup"`, `"resourcegroup-with-subscriptionowner"`, `"subscription"`. +- [ ] `preferredLocation` is a comma-separated string of Azure region names. +- [ ] `estimatedDailyCostsUsd` is a non-negative number. + +### 6. Platform Compatibility +- [ ] No hard-coded corporate network, specific DNS, or environment-specific blocks. +- [ ] Script works for on-site, online, and hybrid event formats. + +--- + +## How to Run + +1. Use `#tool:search` and `#tool:read` to locate and read each required file. +2. For each checklist item, read the relevant section and determine pass/fail. +3. Output the full checklist with ✅/❌ status. +4. Output a **Summary** section: + - Total: X/6 categories passing + - Blocking issues (❌ on categories 1–5 are blocking) + - Recommendations (non-blocking improvements) + +Be strict. A missing `$schema` in lab-defaults.json or a wrong parameter name in deploy-lab.ps1 causes the platform to skip the script silently — flag these as blocking. A root-readme casing mismatch and an out-of-range `Get-MhhStableHash -Length` are also blocking (broken GitHub links / runtime failure respectively). diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/.github/copilot-instructions.md b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/.github/copilot-instructions.md new file mode 100644 index 000000000..3f306934a --- /dev/null +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/.github/copilot-instructions.md @@ -0,0 +1,80 @@ +# MicroHack Lab — EMEA Platform Conventions + +This workspace is a microhack lab for the **EMEA MicroHack platform** ([microsoft/MicroHack](https://github.com/microsoft/MicroHack)). +The platform renders a front-end UI, but **all resource provisioning comes from `labautomation/deploy-lab.ps1`**. +Without a working deployment script, no resources appear in the Azure portal — the platform is just a shiny front end. + +## Mandatory Structure + +``` +labautomation/ ← exactly this name, no dashes + deploy-lab.ps1 ← platform entry point (optional but recommended) + lab-defaults.json ← platform configuration (required if folder exists) + README.md ← usage notes +challenges/ ← challenge instructions (Markdown) +walkthrough/ ← solutions +README.md ← hack intro, objectives, prerequisites +``` + +## deploy-lab.ps1 — Parameter Contract + +The platform **skips the script** if the parameter block does not exactly match: + +```powershell +param( + [Parameter(Mandatory=$true)] + [ValidateSet('subscription','resourcegroup','resourcegroup-with-subscriptionowner')] + [string]$DeploymentType, + + [Parameter(Mandatory=$true)] + [string]$SubscriptionId, + + [string]$ResourceGroupName = "", + + [string[]]$PreferredLocation = @(), + + [string[]]$AllowedEntraUserIds = @() +) +``` + +## Platform Guarantees (before your script runs) + +- Az context is already set to `$SubscriptionId` — **do NOT call `Connect-AzAccount`** +- For `resourcegroup`/`resourcegroup-with-subscriptionowner`: the RG exists and your script has `Owner` — **do NOT call `New-AzResourceGroup`** +- For `subscription`: your script has `Owner` on the subscription; **you must create the RG yourself** using `Get-MhhStableHash` for deterministic naming +- `Az.Accounts` and `Az.Resources` modules are already imported + +## lab-defaults.json — Required Shape + +```json +{ + "$schema": "https://raw.githubusercontent.com/microsoft/MicroHack/refs/heads/main/lab-defaults-schema.json", + "groups": [], + "deploymentType": "resourcegroup", + "labsPerSubscription": 4, + "preferredLocation": "westeurope, swedencentral, norwayeast", + "estimatedDailyCostsUsd": 5.0 +} +``` + +- `deploymentType`: `"resourcegroup"` | `"resourcegroup-with-subscriptionowner"` | `"subscription"` +- `groups`: `[]` (Azure only), `["GHCPUsers"]` (GitHub Copilot seat), `["M365-E5-Users"]` (M365 E5) +- `preferredLocation`: comma-separated regions, priority order +- `estimatedDailyCostsUsd`: per-user per-day cost for the lifecycle wizard + +## Returning Credentials to Users + +Write a hashtable to the output stream — the platform captures it and shows it on the user's dashboard: + +```powershell +@{ HackboxCredential = @{ name = "AdminPassword"; value = "TopSecret!"; note = "VM admin password" } } +``` + +## Delivery Deadline + +Labs must have at least a skeleton committed by **end of July** — September events are already scheduled. + +## Platform Compatibility + +The platform supports **on-site**, **online**, and **hybrid** delivery. +Do not hard-code corporate network or specific DNS assumptions. diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/.github/instructions/labautomation.instructions.md b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/.github/instructions/labautomation.instructions.md new file mode 100644 index 000000000..699ded2dd --- /dev/null +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/.github/instructions/labautomation.instructions.md @@ -0,0 +1,101 @@ +--- +description: "Use when creating, editing, or reviewing labautomation/deploy-lab.ps1, lab-defaults.json, or any PowerShell helper inside labautomation/. Enforces microsoft/MicroHack platform deployment conventions." +applyTo: "labautomation/**" +--- + +# Lab Automation Conventions (microsoft/MicroHack) + +## deploy-lab.ps1 — Required Parameter Contract + +The platform **silently skips** the script if the parameter block does not match exactly: + +```powershell +param( + [Parameter(Mandatory=$true)] + [ValidateSet('subscription','resourcegroup','resourcegroup-with-subscriptionowner')] + [string]$DeploymentType, + + [Parameter(Mandatory=$true)] + [string]$SubscriptionId, + + [string]$ResourceGroupName = "", + + [string[]]$PreferredLocation = @(), + + [string[]]$AllowedEntraUserIds = @() +) +``` + +## Critical Platform Rules + +**NEVER do these — they break platform integration:** + +```powershell +# WRONG — platform already sets Az context +Connect-AzAccount + +# WRONG — platform pre-creates the RG for resourcegroup deployments +New-AzResourceGroup -Name $ResourceGroupName ... +``` + +**For `subscription` deployments only** — you must create your own RG using a deterministic name: + +```powershell +$stableHash = Get-MhhStableHash $AllowedEntraUserIds -Length 24 +$effectiveResourceGroup = "lab-$stableHash" +New-AzResourceGroup -Name $effectiveResourceGroup -Location $effectiveLocation +``` + +## Resolving Effective Location + +```powershell +$effectiveLocation = if ($PreferredLocation.Count -gt 0) { $PreferredLocation[0] } else { "westeurope" } +``` + +## Deploying Resources + +Reference templates relative to `$scriptPath`: + +```powershell +$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +$template = Join-Path $scriptPath "template.bicep" +New-AzResourceGroupDeployment -ResourceGroupName $effectiveResourceGroup -TemplateFile $template -Verbose +``` + +## Returning Credentials to the User Dashboard + +Write a hashtable to the output stream — the platform captures every one: + +```powershell +@{ HackboxCredential = @{ name = "AdminPassword"; value = "TopSecret!"; note = "VM admin password" } } +``` + +Always return at minimum the resource group name so users can find their resources. + +## lab-defaults.json — Required Shape + +```json +{ + "$schema": "https://raw.githubusercontent.com/microsoft/MicroHack/refs/heads/main/lab-defaults-schema.json", + "groups": [], + "deploymentType": "resourcegroup", + "labsPerSubscription": 4, + "preferredLocation": "westeurope, swedencentral, norwayeast", + "estimatedDailyCostsUsd": 5.0 +} +``` + +- `$schema` is **required** — the platform validates against it +- `deploymentType`: `"resourcegroup"` | `"resourcegroup-with-subscriptionowner"` | `"subscription"` +- `groups`: `[]` for Azure-only; add `"GHCPUsers"` or `"M365-E5-Users"` as needed +- `preferredLocation`: comma-separated regions in priority order — list multiple for regional fallback +- `estimatedDailyCostsUsd`: cost per user per day; used in the lifecycle cost wizard + +## Available Platform Helper Cmdlets + +| Cmdlet | Use | +|--------|-----| +| `Get-MhhStableHash` | Deterministic per-user hash for resource naming in `subscription` mode | +| `Get-MhhLabUser` | Get Entra user details for `$AllowedEntraUserIds` | +| `Invoke-MhhDeploymentWithRegionFallback` | Deploy with automatic region fallback | +| `Test-MhhDeploymentFailureRetryable` | Check if a deployment error is transient | diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/.github/prompts/scaffold-lab-automation.prompt.md b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/.github/prompts/scaffold-lab-automation.prompt.md new file mode 100644 index 000000000..6582a2ef9 --- /dev/null +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/.github/prompts/scaffold-lab-automation.prompt.md @@ -0,0 +1,86 @@ +--- +description: "Scaffold a complete labautomation/ directory for a new EMEA MicroHack. Generates deploy-lab.ps1, lab-defaults.json, and README.md that are 100% compliant with the microsoft/MicroHack platform contract." +name: "Scaffold Lab Automation" +agent: "agent" +argument-hint: "Describe the Azure resources this hack needs to deploy (e.g. 'a Storage Account and a Function App for an inventory management scenario')" +tools: [edit, read, search] +--- + +You are scaffolding the `labautomation/` directory for an EMEA MicroHack. +The user has described what Azure resources the hack needs. + +**Do not ask clarifying questions** — infer reasonable defaults and note assumptions in the README. + +## Step 1 — Read existing context + +Check whether `labautomation/` already exists. If it does, read existing files before overwriting. + +## Step 2 — Create lab-defaults.json + +Create `labautomation/lab-defaults.json`. Always include the `$schema` field: + +```json +{ + "$schema": "https://raw.githubusercontent.com/microsoft/MicroHack/refs/heads/main/lab-defaults-schema.json", + "groups": [], + "deploymentType": "resourcegroup", + "labsPerSubscription": 4, + "preferredLocation": "westeurope, swedencentral, norwayeast", + "estimatedDailyCostsUsd": 5.0 +} +``` + +Adjust `deploymentType`, `preferredLocation`, and `estimatedDailyCostsUsd` to match the hack's needs. +Add `"GHCPUsers"` or `"M365-E5-Users"` to `groups` only if the hack requires those licenses. + +## Step 3 — Create deploy-lab.ps1 + +Create `labautomation/deploy-lab.ps1` following the **exact** platform parameter contract: + +```powershell +param( + [Parameter(Mandatory=$true)] + [ValidateSet('subscription','resourcegroup','resourcegroup-with-subscriptionowner')] + [string]$DeploymentType, + + [Parameter(Mandatory=$true)] + [string]$SubscriptionId, + + [string]$ResourceGroupName = "", + + [string[]]$PreferredLocation = @(), + + [string[]]$AllowedEntraUserIds = @() +) +``` + +**Critical rules — the platform silently skips the script if these are violated:** +- Do NOT call `Connect-AzAccount` — Az context is pre-set by the platform +- Do NOT call `New-AzResourceGroup` for `resourcegroup` deployments — the RG is pre-created +- For `subscription` deployments: use `Get-MhhStableHash $AllowedEntraUserIds -Length 24` for deterministic RG naming, then call `New-AzResourceGroup` +- Always emit at least one `@{ HackboxCredential = @{ name = ...; value = ...; note = ... } }` to the output stream + +Add the actual resource deployments (Bicep, ARM, or Az cmdlets) in the body. +Reference the Bicep/ARM template file relative to `$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition`. + +## Step 4 — Create README.md + +Create `labautomation/README.md` covering: +- What the script deploys (bullet list of resources) +- `deploymentType` choice and reason +- `preferredLocation` choice and reason +- `estimatedDailyCostsUsd` basis +- Any assumptions made during scaffolding + +## Step 5 — Self-validate + +After creating all files, verify against the compliance checklist: +- Directory named exactly `labautomation` (no dashes) ✅ +- `lab-defaults.json` has `$schema` field ✅ +- `deploy-lab.ps1` parameter block matches contract exactly ✅ +- No `Connect-AzAccount` in the script ✅ +- No `New-AzResourceGroup` for resourcegroup deployment type ✅ +- At least one `HackboxCredential` returned ✅ +- No hard-coded environment assumptions ✅ + +Report any item you could not satisfy and why. diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/.gitignore b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/.gitignore new file mode 100644 index 000000000..1c404058b --- /dev/null +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/.gitignore @@ -0,0 +1,19 @@ +# Lab-provisioned credentials — never commit +labautomation/lab-defaults.local.json + +# Azure session artifacts +.azure/ +*.log + +# VS Code local settings +.vscode/settings.json +.vscode/launch.json + +# OS +Thumbs.db +desktop.ini +.DS_Store + +# Diagram build artifacts — only the rendered PNG (images/architecture.png) is committed +images/render/ +images/architecture.excalidraw \ No newline at end of file diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/README.md b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/README.md new file mode 100644 index 000000000..aa4a963f3 --- /dev/null +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/README.md @@ -0,0 +1,135 @@ +# Agentic Inventory Planning — Sense, Reason, Act + +- [**MicroHack introduction**](#microhack-introduction) +- [**What you'll build**](#what-youll-build) +- [**MicroHack context**](#microhack-context) +- [**Objectives**](#objectives) +- [**MicroHack challenges**](#microhack-challenges) +- [**Contributors**](#contributors) + +## MicroHack introduction + +This MicroHack teaches you to build **AI agents that reason over governed enterprise data and take human-approved actions**. You will use **Microsoft Foundry Agent Service** to create three agents that form a closed planning loop for inventory management: sensing real-world demand signals, optimising stock positions, and submitting replenishment orders with a human-in-the-loop approval gate. + +No coding background in AI is required. All agents in this MicroHack are built as **prompt agents** directly in the Foundry portal — no application code to write or maintain. You will spend your time on the *reasoning* layer, not the infrastructure layer. + +The star of the hack is the **Fabric Data Agent**: a published Microsoft Fabric agent that turns plain-English questions into governed queries (NL2SQL) over an inventory Lakehouse. Every Foundry agent you build calls it as a tool — so you get trustworthy, governed answers without writing a line of SQL. + +> [!TIP] +> **The pattern you take home works for any industry.** Agents that combine governed internal data with live external signals to drive human-approved actions apply equally to manufacturing, finance, healthcare, and retail. + +## What you'll build + +Three Foundry prompt agents that form one closed planning loop — every one of them calling **your** **Fabric Data Agent** for governed answers: + +| # | Agent | Tools it uses | What it produces | +|---|-------|---------------|------------------| +| 1 | **Demand Sensing** | Fabric Data Agent *(+ optional Web Search)* | A demand-exposure assessment (adequate / at risk / critically exposed) | +| 2 | **Inventory Optimisation** | Fabric Data Agent | A reorder recommendation table with CRITICAL items flagged | +| 3 | **Replenishment Action** | Fabric Data Agent + human approval gate | A **human-approved** purchase order (simulated submit) | +| ⭐ *(stretch)* | **Workflow** | The three agents chained in a Foundry Workflow | The whole loop, driven from a single prompt | + +![Agentic Inventory Planning — the sense → reason → act loop across Foundry Agents, a human-approved action, and the governed Fabric data layer](images/architecture.png) + +> [!NOTE] +> You build **your own** Fabric Data Agent on **your own** Inventory Lakehouse in **Challenge 1** (one notebook, Run All) — there is no shared backend. You then build the three Foundry agents on top of it; **Challenge 5** wraps them under a single orchestrator. + +## MicroHack context + +Enterprise planning cycles have a fundamental problem: decisions are made on historical data, but the world changes in real time. A sporting event is announced, a supplier delays, a competitor launches — and the plan is already wrong. + +This MicroHack demonstrates how AI agents close that gap: + +1. A **Demand Sensing Agent** monitors real-world signals (via Web Search) and reconciles them against the governed inventory position (via the Fabric Data Agent) to produce an adjusted demand forecast. +2. An **Inventory Optimisation Agent** reasons over the adjusted forecast and current stock levels to recommend the right reorder quantities and locations. +3. A **Replenishment Action Agent** closes the loop by proposing a purchase order, waiting for human approval, and recording the outcome — completing the sense → plan → act cycle. + +The architecture uses two key Foundry tools: + +| Tool | What it does in this hack | +|------|--------------------------| +| **Fabric Data Agent** ⭐ | Queries the governed inventory **Lakehouse** in plain English (NL2SQL) — no SQL or Fabric knowledge needed. This is the centrepiece of the hack. | +| **Web Search** *(optional)* | Grounds the demand signal in live external information (news, market trends, competitor activity) | + +> [!NOTE] +> The Fabric Data Agent and Web Search are in Public Preview. They are stable enough for a hack environment and are the tools underlying the production-bound architecture described in the slide deck. + +## Objectives + +After completing this MicroHack you will be able to: + +- Create and configure **Foundry prompt agents** in the portal with specific tools and instructions. +- Ground an agent's reasoning in **live web signals** using the Web Search tool. +- Connect a Foundry agent to a **governed Fabric data source** via the Fabric Data Agent tool, without writing any SQL or understanding Fabric internals. +- Implement a **human-in-the-loop** approval pattern inside an agent conversation. +- Use **agent tracing** in Foundry to inspect every model call and tool invocation — understanding *why* the agent made a decision. +- Articulate the **sense → plan → approve → act** agentic pattern and identify where it applies in your own organisation. + +## MicroHack challenges + +### Prerequisites + +Complete the following before the hack session: + +- A modern browser (Chrome, Edge, or Firefox). +- Access to the **Microsoft Foundry portal** at [ai.azure.com](https://ai.azure.com) — your facilitator will share credentials on the day. +- No local software installation required — all participant work is browser-based. + +> [!NOTE] +> Your Foundry project, gpt-5.4-mini model deployment, and your **own Fabric F2 capacity** are provisioned for you by the lab automation. In **Challenge 1** you stand up your own Fabric workspace and publish your **Fabric Data Agent** by running one notebook (Run All). You'll be handed a project endpoint and your capacity name at the start of the hack. Web Search is optional and may need project-level enablement. + +> [!TIP] +> **You're ready to start when**, in the Foundry portal, you can: (1) see `gpt-5.4-mini` under **Models + endpoints** with status *Succeeded*, and (2) add the **Fabric Data Agent** tool to an agent and create the `inventory-hack-agent` connection (you do this in Challenge 2). If `gpt-5.4-mini` is missing, flag your facilitator before starting. + +> [!IMPORTANT] +> **Facilitators:** there is **no shared Fabric backend** — [`labautomation/deploy-lab.ps1`](labautomation/deploy-lab.ps1) provisions each attendee's Foundry project, model, and their **own Fabric F2 capacity**, and each attendee builds their workspace + Data Agent in Challenge 1. Ensure: the platform service principal has **Owner** on the attendee resource groups; [`lab-defaults.json`](labautomation/lab-defaults.json) sets **`groups: ["M365-E5-Users"]`** so attendees get a Power BI Pro license for the Fabric portal; the **lab tenant has Fabric enabled** (tenant-admin setting *“Users can create Fabric items / workspaces”* — **not** settable via `lab-defaults.json`); and the subscription has **Fabric F-SKU quota** (F2 = 2 CU; ~256 attendees per 512-CU subscription). See [`labautomation/README.md`](labautomation/README.md). + +### Challenge overview + +| Challenge | Title | Duration | Key learning | +|-----------|-------|----------|--------------| +| [Challenge 1](challenges/challenge-01.md) | Get Grounded | 45 min | Understand the inventory scenario and the Foundry + Fabric architecture | +| [Challenge 2](challenges/challenge-02.md) | Demand Sensing Agent | 60 min | Build an agent that combines web signals with governed data | +| [Challenge 3](challenges/challenge-03.md) | Inventory Optimisation Agent | 60 min | Reasoning over stock positions + reading agent traces | +| [Challenge 4](challenges/challenge-04.md) | Replenishment + Human-in-the-Loop | 75 min | Close the loop with an approval gate and end-to-end trace | +| [Challenge 5](challenges/challenge-05.md) *(stretch)* | Orchestrate the Loop | 45 min | Multi-agent orchestration with a Foundry Workflow — optional, for fast finishers | + +### Agenda + +``` +09:00 – 10:00 Tech Talk: the agentic dev model, the three agents, the tools (60 min) +10:00 – 11:45 Challenges 1 and 2 (with a short break) +11:45 – 12:45 Lunch +12:45 – 15:00 Challenges 3 and 4 (fast finishers: Challenge 5 stretch) +15:00 – 15:30 Wrap-up and discussion +``` + +## Contributors + +| Name | Role | +|------|------| +| Digvijay Chauhan | Author & maintainer | + + + +## Contributing + +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit [https://cla.opensource.microsoft.com](https://cla.opensource.microsoft.com). + +When you submit a pull request, a CLA bot will automatically determine whether you need to provide a +CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## Trademarks + +This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft +trademarks or logos is subject to and must follow +[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). +Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply +Microsoft sponsorship. Any use of third-party trademarks or logos is subject to those third-party's policies. diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/challenges/challenge-01.md b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/challenges/challenge-01.md new file mode 100644 index 000000000..a1513e175 --- /dev/null +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/challenges/challenge-01.md @@ -0,0 +1,172 @@ +# Challenge 1 — Get Grounded + +**[Home](../README.md)** - [Next Challenge →](challenge-02.md) + +## 🎯 Objective + +Understand the inventory scenario, provision your own Fabric workspace + Data Agent, and learn the pattern you will build across the next three challenges. No Foundry agent-building yet — just reading, exploring, standing up your data backend, and forming a clear mental model. + +## 🧭 Context + +You are a developer at a retail company. The planning team complains that their inventory decisions are always based on last month's data. By the time a demand spike or supplier disruption becomes visible in the ERP, it is too late. Your job is to build three agents that close this gap: + +1. **Demand Sensing** — react to real-world changes before they hit the system +2. **Inventory Optimisation** — decide the right stock level given the updated demand picture +3. **Replenishment Action** — submit the order with a human review step + +The data lives in **Microsoft Fabric** (a governed Lakehouse with inventory, demand, product, and market-signal data). In this challenge you'll stand up **your own** Fabric workspace and publish a **Fabric Data Agent** on top of it (one notebook, Run All) — then connect your Foundry agents to it in the next challenges. + +> [!IMPORTANT] +> Understanding the architecture saves you time in the next challenges. Spend the full 45 minutes here; do not rush ahead. + +## ✅ Tasks + +1. **Read the Zava data model.** + The Fabric **Lakehouse** you'll create in Task 3 has the following tables. You do not need to know SQL or Fabric to use them — the agents query in plain English. + + | Table | What it contains | Used by | + |-------|-----------------|------| + | `Inventory` | Current stock (`onHand`), `reorderPoint`, and `safetyStock` per product per store/warehouse | Challenges 2, 3 | + | `DemandHistory` | Weekly sales units per product per location | Challenge 2 | + | `ExternalSignals` | Pre-loaded market signals: competitor pricing, search trends, weather, news (sourced from Web IQ) | Challenge 2 | + | `Products` | Product catalogue: `name`, `category`, `unitCost`, `leadTimeDays`, `supplierId` | Challenges 3, 4 | + | `Stores` | Store and warehouse locations with region and type | Challenges 2, 3 | + | `ReplenishmentOrders` | Purchase orders and transfers — status, approver, items JSON | Challenge 4 | + | `Suppliers` | Supplier details | Challenge 4 | + + **Real product categories:** `garden_and_lawn`, `outdoor_power_tools`, `paint_and_supplies`, `smart_home` + **Real locations:** 4 retail stores (Boston, Brooklyn, Portland, Seattle) + 2 distribution warehouses (Chicago, Dallas) + +2. **Explore the Foundry portal.** + Navigate to [ai.azure.com](https://ai.azure.com) and open **your** Foundry project — its `FoundryProjectEndpoint` is on your lab dashboard. + - Locate the **model deployments** — confirm `gpt-5.4-mini` is deployed. + - Open **Agents → Playground** and send a test message: *"What can you do?"* + - Open **Tracing** in the left navigation — this is where you will inspect agent decisions in Challenges 3 and 4. + + ![Models and endpoints view in the Foundry portal showing gpt-5.4-mini deployed with status Succeeded](../images/challenge-00-model-deployments.png) + + ![Agents playground with the test message "What can you do?" and the agent's response](../images/challenge-00-playground-what-can-you-do.png) + + > [!NOTE] + > **The model name in the screenshots is only for illustration.** Some captures may show `gpt-4o-mini` (or another model) selected — that's just what was deployed when the screenshot was taken. Use **whatever model is deployed in your project** (check **Models + endpoints**, e.g. `gpt-5.4-mini`). The model is swappable; every step in this hack works the same regardless of which chat model is deployed. + +3. **Provision your own Fabric workspace and Data Agent.** *(~15 min)* + + Your lab dashboard gives you a **`FabricCapacityName`** — your own Fabric **F2 capacity**. You'll stand up a workspace on it and publish your own Data Agent by running one notebook. Follow each step carefully — this is the data backend every later challenge depends on. + + **a. Create your workspace on your F2 capacity.** + Go to [app.fabric.microsoft.com](https://app.fabric.microsoft.com) → **Workspaces** (left rail) → **+ New workspace**. + + ![Fabric left navigation with Workspaces open and the New workspace button highlighted](../images/challenge-00-new-workspace-button.png) + + Name it `inventory-hack`, expand **Advanced**, and under **Workspace type** select **Fabric**. A **Details** dropdown appears — select your **`FabricCapacityName`** (the `invcap…` value from your dashboard). Click **Apply**. + + ![Create a workspace dialog with Workspace type set to Fabric and the attendee's F2 capacity selected under Details](../images/challenge-00-create-workspace.png) + + > If your capacity isn't in the dropdown: it must be **running** (Azure portal → your Fabric capacity → **Resume**), and you must be signed in as the lab user it was assigned to. Ask your facilitator if it still doesn't appear. + + **b. Create the Lakehouse.** + Inside the workspace, click **+ New item**, choose **Lakehouse** (type `lake` in the search box to find it fast), and name it exactly **`InventoryLakehouse`** — the notebook writes its tables here. + + ![The New item panel filtered to "lake" with the Lakehouse tile selected in the inventory-hack workspace](../images/challenge-00-new-lakehouse.png) + + ![The New Lakehouse dialog with the name InventoryLakehouse and Location inventory-hack](../images/challenge-00-lakehouse-name.png) + + **c. Import the setup notebook.** + Download **`Setup-InventoryDataAgent.ipynb`** from the hack repo's **`setup/`** folder (or the link your facilitator shares). In the workspace toolbar, click **Import → Notebook → From this computer**, then **Upload** and choose the file. You'll see an **Imported successfully** confirmation — open the notebook. + + ![The workspace Import menu open on Notebook then From this computer](../images/challenge-00-import-notebook.png) + + ![The Import status panel with the Upload button to choose the notebook file](../images/challenge-00-import-upload.png) + + ![The imported notebook open with an Imported successfully confirmation popover](../images/challenge-00-notebook-imported.png) + + **d. Attach your Lakehouse to the notebook.** + Open the imported notebook. In the left **Explorer** pane click **Add** (Lakehouses) → **Existing Lakehouse**. In the **OneLake catalog** dialog, tick **`InventoryLakehouse`** and click **Add** — if more than one appears, pick the row whose **Location** is **`inventory-hack`** (your workspace). It becomes the notebook's **default** Lakehouse. **Without this, the table-write cells fail.** + + ![OneLake catalog dialog selecting InventoryLakehouse in the inventory-hack workspace as the notebook's lakehouse](../images/challenge-00-attach-lakehouse.png) + + **e. Run all.** + Click **Run all**. The notebook installs the preview SDKs (the kernel restarts once — that's expected), loads its embedded seed data, writes **7 tables**, and **publishes** your `inventory-hack-agent` Data Agent (~5–10 min). + + ![The notebook with InventoryLakehouse attached as the default Lakehouse and the Run all button highlighted](../images/challenge-00-run-all.png) + + **f. 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. + + ![Final notebook cell output printing the Workspace ID and Data Agent ID](../images/challenge-00-notebook-ids.png) + + **g. Verify your agent answers.** *(1 min)* + Open the data agent's **Test data agent** pane and ask: *"How many Leaf Blower X2 units are on hand at the Portland and Seattle stores, and are they below safety stock?"* You should get exact numbers with **CRITICAL** flags — proof that your tables are selected and the agent is published. + + ![The published data agent answering the Leaf Blower X2 test question with exact CRITICAL stock levels for Portland and Seattle](../images/challenge-00-agent-test.png) + + | Value | Where it comes from | + |-------|---------------------| + | Workspace ID | printed by the notebook's last cell (also in the workspace URL) | + | Artifact / Data Agent ID | printed by the notebook's last cell | + | Connection name | `inventory-hack-agent` | + + > [!NOTE] + > You attach the **Fabric Data Agent** as a *tool* on each Foundry agent, and the connection is created the **first time** you add it (Challenge 2). After that you simply **select** it. It points your Foundry agents at the data agent on top of **your** inventory Lakehouse. + + > [!TIP] + > **No "Fabric Data Agent" in the Foundry tool catalogue later?** Your F2 capacity must be **running** (Azure portal → your Fabric capacity → **Resume**) and the setup notebook must have finished publishing the agent. + +4. **Understand the three-agent pattern.** + Draw (on paper or a whiteboard) the flow: + ``` + Real-world event + ↓ + [Demand Sensing Agent] — Web Search + Fabric Data Agent + ↓ adjusted demand signal + [Inventory Optimisation Agent] — Fabric Data Agent + ↓ reorder recommendation + [Replenishment Action Agent] — Fabric Data Agent + human approval + ↓ submitted purchase order + ``` + Be able to answer: *What tool does each agent use and why?* + +5. **Answer the grounding questions** (discuss with your team): + - What would go wrong if the Demand Sensing Agent only looked at web signals and ignored the governed inventory data? + - Why does the Replenishment Action Agent need a human approval step? + - What data does the Fabric Data Agent abstract away for you? + +## 🏁 Success criteria + +- [ ] You can name the key inventory tables and explain what each one is used for. +- [ ] You have confirmed the `gpt-5.4-mini` model is deployed in your Foundry project. +- [ ] You have created your own Fabric workspace on your F2 capacity, run the setup notebook, and published your `inventory-hack-agent` Data Agent — and noted its **Workspace ID** and **Agent ID** (used in Challenge 2). +- [ ] You can describe, in one sentence each, what each of the three agents does. +- [ ] You can explain why combining web signals with governed data produces better decisions than either source alone. + +## 🛠️ Troubleshooting + +| Symptom | Fix | +|---------|-----| +| `gpt-5.4-mini` is missing under **Models + endpoints** | Flag your facilitator before starting — the model deployment is part of your provisioned lab. | +| Your **`FabricCapacityName`** isn't selectable when creating the workspace | The F2 capacity must be **running** — resume it (Azure portal → your Fabric capacity → **Resume**). | +| 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 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) + +## 🚀 Go further + +- Sketch the three-agent flow from memory and label which tool each agent uses and why. +- In the playground, ask the agent *“What data can you see?”* and compare its answer to the table list above. +- Predict which tables Challenge 3 and Challenge 4 will lean on before you get there. + +## 🧠 Reflection + +- What would go wrong if the Demand Sensing Agent only looked at web signals and ignored the governed inventory data? +- Why does the Replenishment Action Agent need a human approval step? +- What does the Fabric Data Agent abstract away for you, and why does that matter for a non-SQL audience? + +## 📚 Learning resources + +- [What is Microsoft Foundry Agent Service?](https://learn.microsoft.com/azure/foundry/agents/overview) +- [Fabric Data Agent with Foundry agents](https://learn.microsoft.com/fabric/data-science/data-agent-foundry) +- [Foundry agents playground](https://learn.microsoft.com/azure/foundry/concepts/concept-playgrounds) +- [Web Search tool in Foundry](https://learn.microsoft.com/azure/foundry/agents/how-to/tools/web-search) diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/challenges/challenge-02.md b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/challenges/challenge-02.md new file mode 100644 index 000000000..6bcd1a469 --- /dev/null +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/challenges/challenge-02.md @@ -0,0 +1,136 @@ +# Challenge 2 — Demand Sensing Agent + +**[← Previous](challenge-01.md)** - [Home](../README.md) - [Next Challenge →](challenge-03.md) + +## 🎯 Objective + +Build your first Foundry prompt agent. Configure it with two tools — **Web Search** and the **Fabric Data Agent** — and use it to sense a real-world demand change and reconcile it against the current inventory position. No code required; everything is done in the Foundry portal. + +## 🧭 Context + +Your facilitator will announce a scenario at the start of this challenge — for example: + +> *"A prolonged heatwave and early spring across the Pacific Northwest is driving a surge in demand for garden and outdoor power equipment. Retailer search trends and social media show spikes for leaf blowers and lawn tools. Our planning team needs to know if current stock levels can absorb this demand or if we are already exposed."* + +Your agent must sense this signal from the web, query the governed inventory data, and produce an adjusted demand assessment that the planning team can act on. + +## ✅ Tasks + +### Part A — Create the agent (15 min) + +1. In the Foundry portal, navigate to **Agents** and click **+ New agent**. +2. Name it `demand-sensing-agent`. +3. Select the `gpt-5.4-mini` model deployment. +4. Paste the following **system instructions** into the Instructions field: + ``` + You are a Demand Sensing Agent for a retail inventory planning team. + + Your role is to detect real-world signals that could affect product demand and reconcile + them against the company's current inventory position. + + IMPORTANT - tool use: You have a Fabric Data Agent tool connected to the governed Zava + inventory data (Inventory, Products, Stores, DemandHistory, ExternalSignals, Suppliers, + ReplenishmentOrders). For ANY question about stock, on-hand units, reorder points, safety + stock, sales velocity, or any inventory number, you MUST call the Fabric Data Agent and + answer from its result. Never answer inventory questions from memory, and never use Web + Search for internal inventory data (Web Search is only for external market signals). + + When given a scenario or event: + 1. Use Web Search to find relevant market signals, news, and trend data about the affected + product categories. Cite your sources. + 2. Use the Fabric Data Agent to query current stock levels and recent sales velocity for + the relevant SKUs and warehouses. + 3. Synthesise both sources into a demand assessment: state whether current stock is + adequate, at risk, or critically exposed — with a clear reason for each conclusion. + 4. Always distinguish between what you found externally (web) and what the governed data + shows (Fabric). Never blend them without attribution. + + Be concise. Planners need a signal they can act on, not an essay. + ``` + +5. Click **Save**. + +![New agent editor showing the name, gpt-5.4-mini model selection, and instructions field](../images/challenge-01-new-agent.png) + +### Part B — Add the Web Search tool (optional, 10 min) + +> [!NOTE] +> Web Search is **optional**. It enriches the demand signal with live external context, but if it is not enabled in your project, skip this part — the Fabric Data Agent alone still completes the challenge (the `ExternalSignals` table carries pre-loaded market signals). + +1. In the agent editor, click **+ Add tool**. +2. Select **Web Search** from the tool catalogue. +3. Leave the default configuration — no API key required for the built-in preview tool. +4. Click **Save**. + +![Tool catalogue in the agent editor showing Web Search and Fabric Data Agent available to add](../images/challenge-01-add-tool.png) + +> [!TIP] +> **Web Search not appearing in the catalogue?** It is in Public Preview and may need project-level enablement — ask your facilitator, or simply skip it and continue with the Fabric Data Agent. + +### Part C — Add the Fabric Data Agent tool (10 min) + +This is the first agent where you attach the Fabric Data Agent, so you'll **create** the `inventory-hack-agent` connection here using the two IDs from Challenge 1. Every later challenge just selects it. + +1. In the agent editor, expand **Tools** and select **Add**. +2. Choose **Fabric Data Agent** from the tool catalogue. +3. Create the connection with the two IDs your **setup notebook printed in Challenge 1**: + - **Workspace ID** → your Fabric workspace ID + - **Artifact / Data Agent ID** → your `inventory-hack-agent` Agent ID + - **Connection name** → `inventory-hack-agent` +4. Select **Add tool**, then **Save**. + +![The Add tool → Fabric Data Agent connection dialog with the Workspace ID and Artifact ID fields, named inventory-hack-agent](../images/challenge-01-new-connection.png) + +> [!TIP] +> **No "Fabric Data Agent" in the catalogue?** Your F2 capacity must be running (Azure portal → your Fabric capacity → **Resume**) and the setup notebook must have published the agent. + +> [!NOTE] +> **Why two tools?** Web Search gives the agent access to what is happening *outside* the business. The Fabric Data Agent gives access to what is happening *inside* — including the `ExternalSignals` table of pre-loaded market signals. Combining live web context with governed internal data is the core pattern of this hack. + +### Part D — Test the agent (20 min) + +1. Open the **Agents playground** (click **Test in playground**). +2. Send the facilitator's scenario as your first message. +3. Observe the agent's response — look for: + - At least one web source cited with a URL. + - At least one inventory query result from Fabric (stock level or sales velocity). + - A clear demand assessment: adequate / at risk / critically exposed. + + ![Playground response citing a web source and a Fabric inventory query result with a demand assessment](../images/challenge-01-playground.png) +4. Ask a follow-up question: *"Which store or warehouse has the lowest stock of outdoor power tools relative to its reorder point?"* +5. Ask: *"What external signals in the last 30 days could affect demand for outdoor power tools in the Pacific Northwest?"* + +## 🏁 Success criteria + +- [ ] The `demand-sensing-agent` prompt agent exists in your Foundry project with the Fabric Data Agent tool attached (and Web Search too, if it is enabled). +- [ ] A test run produces a response backed by at least one governed data point from the Fabric Data Agent (and an external web source, if Web Search is enabled). +- [ ] The agent produces a clear demand assessment (adequate / at risk / critically exposed) with reasoning. +- [ ] You can explain in your own words what each tool contributed to the response. + +## 🛠️ Troubleshooting + +| Symptom | Fix | +|---------|-----| +| **Web Search** isn't in the tool catalogue | It's in Public Preview and may need project-level enablement — ask your facilitator, or skip it and rely on the `ExternalSignals` table via the Fabric Data Agent. | +| **Fabric Data Agent** isn't in the catalogue | The integration needs **your** F2 Fabric capacity to be running — resume it (Azure portal → your Fabric capacity → **Resume**) and confirm the setup notebook published the agent. | +| The agent answers inventory questions from memory | Strengthen the *IMPORTANT – tool use* line in the instructions; it must call the Fabric Data Agent for any stock number. | +| The connection dialog asks for IDs you don't have | Copy the **Workspace ID** and **Agent ID** your setup notebook printed in its last cell (Challenge 1). | + +## 🚀 Go further + +- Ask the agent to rank **all** warehouses by demand exposure, not just the most exposed one. +- Add a second scenario (e.g. a supplier delay) and see whether the agent changes its assessment. +- Have the agent state its confidence and list exactly which data points drove the conclusion. + +## 🧠 Reflection + +- What did **Web Search** contribute that the governed data could not — and vice versa? +- How did the instructions force the agent to separate external signals from governed facts, and why does that matter for trust? +- Where in your own organisation is a decision made on stale data that this *sense → reconcile* pattern could improve? + +## 📚 Learning resources + +- [Create a prompt agent in Foundry](https://learn.microsoft.com/azure/foundry/agents/quickstarts/prompt-agent) +- [Web Search tool — Foundry Agent Service](https://learn.microsoft.com/azure/foundry/agents/how-to/tools/web-search) +- [Fabric Data Agent with Foundry agents](https://learn.microsoft.com/fabric/data-science/data-agent-foundry) +- [Tool best practices — Foundry](https://learn.microsoft.com/azure/foundry/agents/concepts/tool-best-practice) diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/challenges/challenge-03.md b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/challenges/challenge-03.md new file mode 100644 index 000000000..61230666e --- /dev/null +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/challenges/challenge-03.md @@ -0,0 +1,124 @@ +# Challenge 3 — Inventory Optimisation Agent + +**[← Previous](challenge-02.md)** - [Home](../README.md) - [Next Challenge →](challenge-04.md) + +## 🎯 Objective + +Build a second Foundry prompt agent that takes the demand assessment from Challenge 2 and reasons over the governed stock data to produce a concrete reorder recommendation. Then use **agent tracing** to inspect every decision the agent made — the most important skill for building trustworthy AI systems. + +## 🧭 Context + +The demand signal from your Demand Sensing Agent shows exposure. Now the planning team needs to know: + +- *Which SKUs need to be reordered?* +- *How many units, and to which warehouse?* +- *What is the reasoning behind the recommendation?* + +Your Inventory Optimisation Agent answers these questions by querying the Fabric data and applying simple planning logic. Crucially, after each run you will **open the trace** and verify the agent's reasoning step by step. + +## ✅ Tasks + +### Part A — Create the Inventory Optimisation Agent (20 min) + +1. In the Foundry portal, navigate to **Agents** and click **+ New agent**. +2. Name it `inventory-optimisation-agent`. +3. Select the `gpt-5.4-mini` model deployment. +4. Paste the following **system instructions**: + + ``` + You are an Inventory Optimisation Agent for a retail planning team. + + You receive a demand assessment (adequate / at risk / critically exposed) and a + description of the affected product categories or SKUs. Your job is to produce a + concrete reorder recommendation. + + IMPORTANT - tool use: You have a Fabric Data Agent tool connected to the governed Zava + inventory data (Inventory, Products, Stores, DemandHistory, Suppliers, ReplenishmentOrders). + For ANY inventory number - stock level, reorder point, safety stock, average daily sales - + you MUST call the Fabric Data Agent and answer from its result. Never answer from memory + and never invent numbers. + + For each at-risk or critically exposed SKU: + 1. Use the Fabric Data Agent to query current stock level, reorder point, and + average daily sales for that SKU across all warehouses. + 2. Calculate the suggested reorder quantity using this rule: + reorder_qty = max(0, (30-day_demand - current_stock)) + where 30-day_demand = average_daily_sales × 30. + 3. Identify the warehouse with the lowest stock relative to demand — that is the + priority replenishment location. + 4. Return a structured recommendation table: + | SKU | Warehouse | Current Stock | Suggested Reorder Qty | Priority | + 5. Flag any SKU where current stock is already below the reorder point as CRITICAL. + + If you cannot find a SKU in the data, say so clearly — do not invent numbers. + ``` + +5. Add the **Fabric Data Agent** tool and select the existing **`inventory-hack-agent`** connection (created in Challenge 2). Do **not** add Web Search — this agent works only with internal data. +6. Click **Save**. + +### Part B — Run the agent (15 min) + +1. Open the **Agents playground**. +2. Paste the demand assessment you received in Challenge 2 as your first message. Copy it directly from your Challenge 2 playground chat, for example: + + > *"Outdoor power tools are critically exposed. Leaf blowers and chainsaws show demand uplift signals but stock at both Chicago and Dallas warehouses is near reorder point. The Portland and Seattle stores are already below safety stock on leaf blowers."* + +3. The agent should query Fabric and return a recommendation table. +4. Ask a follow-up: *"Show me only the CRITICAL items."* +5. Ask: *"Can the Chicago warehouse cover Portland's shortfall with a transfer instead of a new purchase order?"* + +![Playground showing a structured reorder recommendation table with a CRITICAL item flagged](../images/challenge-02-recommendation.png) + +### Part C — Inspect the agent trace (25 min) + +> [!IMPORTANT] +> This is the most important part of the challenge. Understanding what an agent did — and why — is essential for building trust with stakeholders and catching errors before they reach production. + +1. In the left navigation, click **Tracing**. +2. Find the most recent run of `inventory-optimisation-agent`. +3. Open the trace and locate: + - The **model call** — what instructions and context were sent to gpt-5.4-mini? + - The **tool call** — what exact NL query was sent to the Fabric Data Agent? + - The **tool response** — what data did Fabric return? + - The **final generation** — how did the model synthesise the data into the recommendation? + + ![Expanded trace view showing the model call, Fabric tool call, tool response, and final generation spans](../images/challenge-02-trace.png) +4. Answer these questions by reading the trace: + - Did the agent query Fabric once or multiple times? Why? + - Was the reorder quantity calculation visible in the trace? + - Did the agent correctly identify the CRITICAL items? + +## 🏁 Success criteria + +- [ ] The `inventory-optimisation-agent` prompt agent exists with only the Fabric Data Agent tool (no Web Search). +- [ ] A test run returns a recommendation table with at least one CRITICAL flagged item. +- [ ] You have opened the trace for your run and can identify the model call, tool call, tool response, and final generation steps. +- [ ] You can explain, by pointing to the trace, why the agent made the recommendation it did. + +## 🛠️ Troubleshooting + +| Symptom | Fix | +|---------|-----| +| The run doesn't appear under **Tracing** | Give it a few seconds and refresh; make sure you ran the agent from the playground, not just saved it. | +| The agent invents stock numbers | Reinforce the *IMPORTANT – tool use* instruction — every inventory number must come from a Fabric Data Agent call. | +| No item is flagged **CRITICAL** | Use a scenario/SKU that is genuinely below reorder point (e.g. leaf blowers at Portland/Seattle), or ask the agent to list items below their reorder point. | +| The reorder quantity looks wrong | Check the trace — confirm the agent used `average_daily_sales × 30 − current_stock` and pulled real numbers from Fabric. | + +## 🚀 Go further + +- Ask whether a **transfer** from another warehouse could cover a shortfall instead of a new purchase order. +- Extend the reorder rule to respect `safetyStock` as well as `reorderPoint`. +- Have the agent rank all recommendations by estimated spend using `unitCost` from the `Products` table. + +## 🧠 Reflection + +- By pointing at the trace, can you prove *why* the agent recommended what it did? +- Did the agent query Fabric once or several times — and what does that tell you about its reasoning? +- What would you show a sceptical stakeholder from this trace to earn their trust? + +## 📚 Learning resources + +- [Agent tracing — Foundry](https://learn.microsoft.com/azure/foundry/observability/concepts/trace-agent-concept) +- [Fabric Data Agent with Foundry agents](https://learn.microsoft.com/fabric/data-science/data-agent-foundry) +- [Responsible AI for agents — Foundry](https://learn.microsoft.com/azure/foundry/responsible-use-of-ai-overview) +- [Tool best practices — when to use one tool vs many](https://learn.microsoft.com/azure/foundry/agents/concepts/tool-best-practice) diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/challenges/challenge-04.md b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/challenges/challenge-04.md new file mode 100644 index 000000000..7bdfdf24d --- /dev/null +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/challenges/challenge-04.md @@ -0,0 +1,163 @@ +# Challenge 4 — Replenishment Action Agent + Human-in-the-Loop + +**[← Previous](challenge-03.md)** - [Home](../README.md) - [Next Challenge →](challenge-05.md) + +## 🎯 Objective + +Build the third and final agent: one that proposes a purchase order, waits for a human to approve or reject it, and records the outcome. Then trace the **complete sense → plan → approve → act cycle** end-to-end — the pattern that turns an AI assistant into an AI agent that genuinely acts in the world. + +## 🧭 Context + +The optimisation recommendation is ready. Now someone has to actually place the order. But purchase orders above a threshold require a human sign-off before anything is submitted. Your Replenishment Action Agent handles this: + +1. It presents a clear, structured purchase order proposal. +2. It pauses and asks: *"Do you approve this order? (yes / no / modify)"* +3. If approved, it records the outcome and returns a submission confirmation (simulated — see Part B). +4. If rejected, it records the reason and suggests an alternative. + +This human-in-the-loop pattern is critical for **agentic AI in enterprise settings**: the agent proposes, the human decides, the agent acts. + +## ✅ Tasks + +### Part A — Create the Replenishment Action Agent (25 min) + +1. In the Foundry portal, navigate to **Agents** and click **+ New agent**. +2. Name it `replenishment-action-agent`. +3. Select the `gpt-5.4-mini` model deployment. +4. Paste the following **system instructions**: + + ``` + You are a Replenishment Action Agent for a retail planning team. + + You receive an inventory optimisation recommendation (a table of SKUs, warehouses, and + suggested reorder quantities). Your job is to present a formal purchase order proposal + and obtain human approval before taking any action. + + IMPORTANT - tool use: When you need a unit cost, you MUST look it up from the Products + table via the Fabric Data Agent - do not answer from memory. Use $20 per unit only as a + fallback if the Fabric Data Agent has no cost for that SKU. + + Workflow: + 1. Format the recommendation as a purchase order proposal: + PURCHASE ORDER PROPOSAL + ───────────────────────────────────────────── + Date: + Requested by: Inventory Optimisation Agent + ───────────────────────────────────────────── + | Line | SKU | Warehouse | Qty | Unit Cost (est.) | Line Total | + ───────────────────────────────────────────── + TOTAL ESTIMATED VALUE: $X,XXX + ───────────────────────────────────────────── + Look up each item's unit cost from the Products table via the Fabric Data Agent. + Use $20 per unit as a fallback only if a cost is unavailable. + + 2. Ask clearly: "Do you approve this purchase order? Reply YES to submit, NO to cancel, + or MODIFY to adjust a specific line." + + 3. If the response is YES: generate a simulated submission confirmation - do NOT call any + external system (there is no live ERP in this lab). Reply with: + "✅ Purchase order PO--001 submitted to ERP." Build the PO number yourself from + today's date; never claim to have written to a real system. + + 4. If the response is NO: record the reason (ask for one if not given) and reply: + "❌ Order cancelled. Reason recorded: . No action has been taken." + + 5. If the response is MODIFY: update the specified line, recalculate the total, re-present + the updated proposal, and ask for approval again. + + Never submit an order without explicit YES approval. Only generate the simulated PO number + (from today's date) after a YES; never fabricate a real ERP order ID. + ``` + +5. Click **Save** — you'll attach the Fabric Data Agent tool next. + +### Part B — Add the Fabric Data Agent tool (10 min) + +The "act" step — submitting the purchase order — is **simulated by the agent itself**. In the current **New Foundry** portal, prompt agents don't expose a no-code custom-function tool: the tool catalogue's **Custom** tab offers only **OpenAPI**, **MCP**, and **Agent2Agent (A2A)** connectors, which all require a hosted endpoint. Because the Fabric Data Agent is **read-only**, the agent closes the loop by *generating* a submission confirmation on approval — which delivers the exact human-in-the-loop teaching point with zero infrastructure. + +You only need one tool here: + +1. In the agent editor, expand **Tools** and select **Add**. +2. On the **Configured** tab, choose **Fabric Data Agent** and connect the existing **`inventory-hack-agent`** connection — the agent needs it to look up unit costs from the `Products` table when building the PO. +3. Select **Add tool**, then **Save**. + +> [!NOTE] +> **Want a *real* submit action?** In production you'd add an **OpenAPI tool** (New Foundry) or a custom function that writes to the `ReplenishmentOrders` table / your ERP. That needs a hosted endpoint, so it's out of scope for this no-code lab — see **Part E — Stretch** at the end of this challenge. + +### Part C — Run the complete planning loop (30 min) + +> [!IMPORTANT] +> This is the payoff: running the full sense → plan → approve → act cycle in one session. + +1. Open a **new chat** in the Agents playground with `replenishment-action-agent`. +2. Paste the recommendation table from Challenge 3 as your first message. +3. The agent should present a formatted purchase order proposal. +4. **Test the approval flow:** + - First, reply `MODIFY 1 50` — adjust the first line quantity. Verify the agent updates and re-presents the order. + - Then reply `YES` — verify the agent responds with the simulated submission confirmation (`✅ Purchase order PO-... submitted to ERP.`). + + ![Playground showing the MODIFY 1 50 adjustment followed by a YES approval and the submission confirmation](../images/challenge-03-approval-flow.png) +5. Open **Tracing** and find this run. Trace the complete sequence: + - Model call (format PO from the recommendation) → **Fabric tool call** (unit-cost lookup from `Products`) → model call (present proposal) → model call (apply MODIFY, re-present) → model call (confirm submission after YES). +6. **Discuss with your team:** At which step did the human add value that an automated pipeline could not? + +![Trace view showing the model calls and the Fabric unit-cost lookup across the approval cycle](../images/challenge-03-trace.png) + +### Part D — Reflect on the full loop (optional, 10 min) + +If time allows, open a single trace view and describe the complete three-agent journey: + +``` +Challenge 2: [web signal] + [Fabric stock query] → demand assessment +Challenge 3: [Fabric stock query] + [calculation] → reorder recommendation +Challenge 4: [proposal formatting] + [human YES] → PO-xxxx submitted +``` + +- What would break if the Fabric Data Agent returned stale or incorrect data? +- Where would you add monitoring in a production deployment of this pattern? + +### Part E — Stretch: wire a real action tool (optional) + +The lab simulates submission so it stays 100% no-code. To make the "act" step *real*, you have two production-grade options — both require a small hosted endpoint, so treat this as homework: + +- **OpenAPI tool (New Foundry).** Deploy a tiny API with a `POST /purchase-orders` operation, then in the agent editor go to **Add → Custom → OpenAPI tool** and paste its OpenAPI 3.0 schema. The agent calls your endpoint on approval. +- **Adaptive Card approval (channel surface).** The Foundry *playground* renders Markdown only — it does **not** render Adaptive Cards. To show a true Adaptive Card with an approval button, surface the agent through a channel that supports cards (Teams, or a custom Web Chat using the Foundry SDK + the Adaptive Cards JS renderer) and return a card payload instead of the Markdown proposal. This is a front-end exercise, not a portal one. + +Either path turns the simulated confirmation into a genuine side effect while keeping the same sense → plan → approve → act gate. + +## 🏁 Success criteria + +- [ ] The `replenishment-action-agent` prompt agent exists on `gpt-5.4-mini` with the Fabric Data Agent tool attached. +- [ ] A test run successfully presents a formatted purchase order proposal (per-line cost table with a total). +- [ ] The MODIFY flow works correctly — the agent updates the specified line, recalculates the total, and re-presents. +- [ ] A YES reply produces the simulated submission confirmation (`✅ Purchase order PO-... submitted`). +- [ ] You have opened the trace and can identify the human approval step and the Fabric unit-cost lookup. +- [ ] You can explain the sense → plan → approve → act pattern and where the human decision point sits. + +## 🛠️ Troubleshooting + +| Symptom | Fix | +|---------|-----| +| The agent submits without waiting for approval | Reinforce the *Never submit an order without explicit YES approval* rule; the confirmation must only appear after a `YES`. | +| The agent claims it wrote to a real ERP | The submit is **simulated** — the instructions must build the `PO--001` number itself and never fabricate a real order ID. | +| **MODIFY** doesn't recalculate the total | Confirm the instruction to update the line, recompute the total, and re-present before asking for approval again. | +| Unit costs all show the $20 fallback | The `Products` table lookup via the Fabric Data Agent may have failed — check the trace; the fallback is only for genuinely missing costs. | + +## 🚀 Go further + +- Make the *act* step real: follow **Part E** to wire an **OpenAPI tool** that writes to `ReplenishmentOrders`, or surface the proposal as an **Adaptive Card** approval. +- Add a spend cap: have the agent refuse to auto-format any PO above a threshold and escalate instead. +- Capture the approver's name and reason and echo them back in the confirmation for an audit trail. + +## 🧠 Reflection + +- At which step did the human add value that a fully automated pipeline could not? +- What would break if the Fabric Data Agent returned stale or incorrect unit costs? +- Where would you add monitoring and guardrails before letting this run against a real ERP? + +## 📚 Learning resources + +- [Agent tools overview (tool catalog) — Foundry](https://learn.microsoft.com/azure/foundry/agents/concepts/tool-catalog) +- [Human-in-the-loop tool approvals for AI agents](https://learn.microsoft.com/agent-framework/agents/tools/tool-approval) +- [Agent tracing — Foundry](https://learn.microsoft.com/azure/foundry/observability/concepts/trace-agent-concept) +- [Agent development lifecycle](https://learn.microsoft.com/azure/foundry/agents/concepts/development-lifecycle) diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/challenges/challenge-05.md b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/challenges/challenge-05.md new file mode 100644 index 000000000..8081cee4f --- /dev/null +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/challenges/challenge-05.md @@ -0,0 +1,133 @@ +# Challenge 5 — Orchestrate the Loop (Stretch) + +**[← Previous](challenge-04.md)** - [Home](../README.md) + +> [!NOTE] +> This is an **optional stretch challenge** for teams that finish early. It builds directly on the three agents you created in Challenges 2–4. Skip it without penalty — Challenges 1–4 are the core of the hack. + +## 🎯 Objective + +Wire your three agents together in a Foundry **Workflow** so the entire sense → plan → approve → act loop runs from one prompt. You'll use the visual workflow designer to chain the agents in sequence with a human-approval gate, then read the trace to see each node execute. This is Microsoft's sanctioned replacement for the older "connected agents" tool and the pattern behind real multi-agent systems. + +## 🧭 Context + +The planning team doesn't want to talk to three separate agents. They want to type one sentence — *"A heatwave is hitting the Pacific Northwest, check our outdoor power tool exposure and prepare a replenishment order for approval"* — and have the system run the whole loop, pausing only for the human approval gate. + +## ✅ Tasks + +### Part A — Create the workflow (15 min) + +> [!NOTE] +> The classic **connected agents** tool (adding one agent as another's tool) is **not available** in the New Foundry portal — Microsoft replaced it with **Workflows** for multi-agent orchestration. The visual workflow designer is in preview and is supported in-portal until **1 December 2026**, after which you run the same logic by exporting the workflow **YAML** and deploying it as a hosted agent (see Learning resources). For this hack's timeframe the visual designer is the fastest no-code path. + +1. In the Foundry portal, go to **Agents → Workflows → + New workflow** (or **Create**). +2. Choose the **Sequential** template (it pre-wires a Start node you can extend). If you start from **Blank**, you'll add the Start node yourself. +3. Name it `inventory-planning-workflow` and open the **Build** canvas (Visualizer view). + +### Part B — Add and wire the three agent nodes (20 min) + +You'll chain the three specialist agents you built in Challenges 2–4. **You do not need a separate orchestrator agent — the workflow *is* the orchestrator.** + +1. On the canvas, click **+ New node** (or the **+** on the Start node) and add an **Agent** node. In **Select an agent**, pick **`demand-sensing-agent`**. +2. Add a second Agent node for **`inventory-optimisation-agent`**, connected after the first. +3. Add a third Agent node for **`replenishment-action-agent`**, connected after the second. +4. For each node, open **Node settings** and wire the data flow so each agent receives the previous agent's output: + - Use **Save agent output message as** on each node to name its output (e.g. `demandAssessment`, `reorderTable`). + - Set the next node's **Input message** to that upstream variable. + - Leave **Conversation context** as `System.ConversationId` so the run shares one conversation. +5. On the **`replenishment-action-agent`** node, enable **Allow multi-turn conversation** — this lets the workflow pause at the purchase-order proposal and wait for the human's `YES` / `MODIFY` reply (the human-in-the-loop gate). In the **Loop condition** box, paste this PowerFx so the node keeps re-prompting until the agent submits or cancels the order: + + ```powerfx + And( + IsBlank(Find("submitted to ERP", Last(Local.LatestMessage).Text)), + IsBlank(Find("Order cancelled", Last(Local.LatestMessage).Text)) + ) + ``` + + The loop runs **while the condition is true**: `MODIFY` re-presents the proposal (neither phrase present → keep looping), while `YES` (`✅ … submitted to ERP.`) or `NO` (`❌ Order cancelled…`) makes it false and ends the node. `Find` is **case-sensitive**, so keep the Challenge 4 confirmation wording exactly. +6. Click **Save**. + + ![Workflow canvas showing Start then demand-sensing-agent then inventory-optimisation-agent then replenishment-action-agent wired in sequence](../images/challenge-04-orchestrator.png) + +> [!TIP] +> Foundry also ships a **Human in the loop** template and a **Group chat** template. The **Sequential** pattern used here matches this hack's deterministic sense → plan → approve → act chain; **Group chat** is for dynamic hand-off scenarios. + +### Part C — Run the full loop from one prompt (20 min) + +1. Click **Preview** (or **Run**) to open the workflow playground. +2. Send a single request, e.g.: + + > *"A prolonged heatwave is hitting the Pacific Northwest. Check our exposure on outdoor power tools and prepare a replenishment order for approval."* + +3. Watch the nodes execute in order: demand sensing → optimisation → replenishment. When the replenishment node presents the PO proposal, reply `YES` (or `MODIFY `). +4. Open **Traces** and find the run. Confirm you can see the **node-by-node execution** — each agent node's model call with its **Fabric Data Agent** tool call underneath, and the human approval turn on the replenishment node. + +## 🏁 Success criteria + +- [ ] `inventory-planning-workflow` exists with the three agents chained Start → demand → optimisation → replenishment. +- [ ] A single prompt drives the full sense → plan → approve → act loop, pausing only at the human approval gate on the replenishment node. +- [ ] The trace shows node-by-node execution (each agent node → its Fabric Data Agent tool call), plus the human approval turn. +- [ ] You can explain the trade-offs of a deterministic workflow vs. three agents a human drives manually — and when you'd choose the Group chat pattern instead. + +## Part D — Alternative / bonus: an evaluation pass (optional) + +If you want a second stretch (or the visual designer is unavailable), evaluate one agent's quality instead: + +1. Open **Evaluation** in the portal (preview). +2. Create a small evaluation dataset of 3–5 questions with expected answers about the inventory data (e.g. *"Which store has the lowest leaf blower stock?"* → *"Portland"*). +3. Run an evaluation against `inventory-optimisation-agent` and review the **groundedness** and **relevance** scores. +4. Discuss: what would you add to the agent's instructions to improve the weakest score? + +## Part E — What's next: publish to Teams & Microsoft 365 Copilot (showcase, optional) + +> [!NOTE] +> This part is a **look-ahead showcase**, not a required lab step. You don't need to publish anything to finish the hack — it's here to show where a working workflow goes next and what a business rollout looks like. + +Everything so far lives in the Foundry portal — great for builders, but the **planners who actually place orders live in Teams**. Publishing the workflow to **Microsoft 365 Copilot & Teams** would let a non-technical user run the whole sense → plan → approve → act loop by typing one sentence in a Teams chat, and approve the PO right there. + +> [!NOTE] +> Publishing to Microsoft 365 Copilot and Teams is an **Early Access Preview**. Org-wide publishing needs a **Microsoft 365 admin** to approve the agent — so if you *do* try it, use the **Just you** scope, which is available immediately with no approval. + +1. Open your published `inventory-planning-workflow` (or any of the agents), select **Publish → Publish to Teams and Microsoft 365 Copilot**. Foundry provisions an **Azure Bot Service** resource behind the scenes. + + ![The inventory-planning-workflow with the Publish menu open, showing the Teams & Microsoft 365 Copilot publish target](../images/challenge-04-teams.png) +2. Fill in the metadata: **Name** (e.g. *Inventory Planning Assistant*), **Publish version** `1.0.0`, a one-line **Short description**, a longer **Description**, and your **Developer** name. + > Don't put secrets/keys in any field — this metadata is user-visible. +3. Select **Next: Publish options → Direct publish**, choose **Just you** under *Choose who can use this agent*, then **Publish**. (Org-wide = *People in your organization* → M365 admin approval → appears under **Built by your org** in the agent store.) +4. In **Microsoft 365 Copilot** or **Teams**, open the **agent store → Your agents**, pick your agent, and chat like a business user: + + > *"A heatwave is hitting the Pacific Northwest — check our outdoor power tool exposure and prepare a replenishment order for approval."* + +5. Watch the loop run inside the Teams chat and reply `YES` at the approval gate — no portal, no code. + +**Discuss:** what governance would you add before letting hundreds of planners trigger real purchase orders from Teams? (Think: the approval gate, a spend cap, `BotServiceRbac` vs `BotServiceTenant` calling scope, and audit via the trace.) + +## 🛠️ Troubleshooting + +| Symptom | Fix | +|---------|-----| +| There's no **connected agents** tool | Correct — the New Foundry portal replaced it with **Workflows**. Use the visual workflow designer as described above. | +| The replenishment node never stops looping | The **Loop condition** `Find` is **case-sensitive** — keep the Challenge 4 confirmation wording (`submitted to ERP`, `Order cancelled`) exactly. | +| A node doesn't receive the previous agent's output | Set **Save agent output message as** on the upstream node and reference that variable in the next node's **Input message**. | +| The visual designer isn't available | Do the **Part D** evaluation bonus instead, or export the workflow **YAML** and run it as a hosted agent (see Learning resources). | + +## 🚀 Go further + +- Do the **Part D** evaluation pass to score `inventory-optimisation-agent` on groundedness and relevance. +- Follow the **Part E** showcase to publish the workflow to **Teams & Microsoft 365 Copilot** (use the *Just you* scope). +- Swap the **Sequential** template for **Group chat** and observe how dynamic hand-off changes the run. + +## 🧠 Reflection + +- When would you choose a deterministic workflow over three agents a human drives manually — and when would **Group chat** fit better? +- What governance would you add before hundreds of planners could trigger real purchase orders from Teams? +- Where does the human-approval gate belong once the loop runs end-to-end from a single prompt? + +## 📚 Learning resources + +- [Build a workflow in Microsoft Foundry (Preview)](https://learn.microsoft.com/azure/foundry/agents/concepts/workflow) +- [Publish agents to Microsoft 365 Copilot and Microsoft Teams](https://learn.microsoft.com/azure/foundry/agents/how-to/publish-copilot) +- [Microsoft Agent Framework workflows (code-first / YAML)](https://learn.microsoft.com/agent-framework/workflows/orchestrations/sequential) +- [Agent tracing — Foundry](https://learn.microsoft.com/azure/foundry/observability/concepts/trace-agent-concept) +- [Evaluate agents — Foundry](https://learn.microsoft.com/azure/foundry/observability/how-to/evaluate-agent) +- [Orchestration patterns for AI agents](https://learn.microsoft.com/azure/architecture/ai-ml/guide/ai-agent-design-patterns) diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/dev/run-local.ps1 b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/dev/run-local.ps1 new file mode 100644 index 000000000..11fe3c3e9 --- /dev/null +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/dev/run-local.ps1 @@ -0,0 +1,81 @@ +# Local-only wrapper to run deploy-lab.ps1 outside the MicroHack platform. +# Supplies the platform-provided helper (Get-MhhStableHash), pre-creates the +# resource group, and passes the current signed-in user's Entra object ID. +# NOT part of the hack deliverable — for local end-to-end verification only. +# +# Prereqs: Connect-AzAccount (Az) AND az login (CLI) to the SAME subscription. +# deploy-lab.ps1 provisions the signed-in user's Foundry project + their own Fabric +# F2 capacity; you then build your workspace + Data Agent by running +# setup/Setup-InventoryDataAgent.ipynb (see Challenge 1). + +param( + # Defaults to the current Az context subscription; pass to override. + [string]$SubscriptionId = ((Get-AzContext -ErrorAction SilentlyContinue).Subscription.Id), + [string]$ResourceGroupName = "rg-inventory-hack-local", + [string]$Location = "swedencentral", + # Defaults to the current signed-in user's Entra object ID (resolved below). + [string[]]$AllowedEntraUserIds = @() +) + +$ErrorActionPreference = "Stop" + +if (-not $SubscriptionId) { + throw "No subscription in context. Run Connect-AzAccount first, or pass -SubscriptionId." +} + +# The platform passes the lab's attendee object IDs here. Run locally, this defaults +# to the signed-in user so the RBAC grants target you. +if ($AllowedEntraUserIds.Count -eq 0) { + $signedInId = (Get-AzADUser -SignedIn -ErrorAction SilentlyContinue).Id + if (-not $signedInId) { + throw "Could not resolve your Entra object ID. Pass -AllowedEntraUserIds explicitly." + } + $AllowedEntraUserIds = @($signedInId) +} + +# Platform-provided helper shim: deterministic, DNS-safe, lowercase hash. +function Get-MhhStableHash { + param([Parameter(ValueFromRemainingArguments = $true)][object[]]$InputValues, [int]$Length = 12) + $strings = @() + foreach ($v in $InputValues) { if ($v -is [array]) { $strings += $v } else { $strings += $v } } + $joined = ($strings -join '|') + if ([string]::IsNullOrEmpty($joined)) { $joined = "default" } + $sha = [System.Security.Cryptography.SHA256]::Create() + $bytes = $sha.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($joined)) + $hex = -join ($bytes | ForEach-Object { $_.ToString('x2') }) + return $hex.Substring(0, $Length) +} + +# Platform-provided helper shim: resolve an Entra object ID to a lab-user record +# (Id / UserPrincipalName / ShortName). The real platform serves this from a cache; +# locally we resolve via Get-AzADUser. +function Get-MhhLabUser { + param([Parameter(Mandatory = $true, ValueFromPipeline = $true)][Alias('ObjectId', 'Id')][string[]]$UserId) + process { + foreach ($id in $UserId) { + $u = Get-AzADUser -ObjectId $id -ErrorAction SilentlyContinue + $upn = $u.UserPrincipalName + [PSCustomObject]@{ + Id = $id + UserPrincipalName = $upn + ShortName = if ($upn) { ($upn -split '@')[0].ToLower() } else { $null } + } + } + } +} + +# Pre-create the resource group (the platform does this for 'resourcegroup' type). +if (-not (Get-AzResourceGroup -Name $ResourceGroupName -ErrorAction SilentlyContinue)) { + Write-Host "[INFO] Creating resource group '$ResourceGroupName' in '$Location'..." + New-AzResourceGroup -Name $ResourceGroupName -Location $Location | Out-Null + Write-Host "[OK] Resource group created." +} else { + Write-Host "[OK] Resource group already exists." +} + +& "$PSScriptRoot\..\labautomation\deploy-lab.ps1" ` + -DeploymentType 'resourcegroup' ` + -SubscriptionId $SubscriptionId ` + -ResourceGroupName $ResourceGroupName ` + -PreferredLocation @($Location, "westeurope", "norwayeast") ` + -AllowedEntraUserIds $AllowedEntraUserIds diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/docs/Invite.docx b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/docs/Invite.docx new file mode 100644 index 000000000..9a5bc6b0e Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/docs/Invite.docx differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/README.md b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/README.md new file mode 100644 index 000000000..a135fea6c --- /dev/null +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/README.md @@ -0,0 +1,32 @@ +# Screenshots + +This folder holds the screenshots embedded throughout the challenge guides and walkthroughs. Each image is referenced by the exact filename below, so the guides render end-to-end. All captures are light theme, ~1280–1600px wide, with any real tenant, subscription, email, or GUID values redacted. + +> **Model name in captures is illustrative.** Where a screenshot shows a selected chat model (e.g. `gpt-4o-mini` or `gpt-5.4-mini`), that name reflects only what was deployed when the capture was taken. The model is **swappable** — attendees use whatever their facilitator provisioned. You do **not** need to re-shoot screenshots when the deployed model changes; challenge-01 tells attendees this explicitly. + +| Filename | Challenge | Shows | +|----------|-----------|-------| +| `challenge-00-model-deployments.png` | 0 | Foundry **Models + endpoints** with `gpt-5.4-mini` deployed, status **Succeeded**. | +| `challenge-00-playground-what-can-you-do.png` | 0 | Agents playground grounding test — *"What can you do?"* and the agent's reply. | +| `challenge-00-new-workspace-button.png` | 0 | Fabric left navigation with **Workspaces** open and the **+ New workspace** button highlighted. | +| `challenge-00-create-workspace.png` | 0 | The Fabric **Create a workspace** dialog: **Advanced** expanded, **Workspace type: Fabric**, and the attendee's `invcap…` F2 capacity selected under **Details**. | +| `challenge-00-new-lakehouse.png` | 0 | The **+ New item** panel filtered to *"lake"* with the **Lakehouse** tile selected, inside the `inventory-hack` workspace. | +| `challenge-00-lakehouse-name.png` | 0 | The **New Lakehouse** dialog with the name **`InventoryLakehouse`** and Location `inventory-hack`. | +| `challenge-00-import-notebook.png` | 0 | The workspace **Import → Notebook → From this computer** menu, for `Setup-InventoryDataAgent.ipynb`. | +| `challenge-00-import-upload.png` | 0 | The **Import status** panel with the **Upload** button to choose the notebook file from the local computer. | +| `challenge-00-notebook-imported.png` | 0 | The imported notebook open with the **Imported successfully** confirmation popover. | +| `challenge-00-attach-lakehouse.png` | 0 | The **OneLake catalog** dialog attaching **`InventoryLakehouse`** (Location `inventory-hack`) as the notebook's default Lakehouse. | +| `challenge-00-run-all.png` | 0 | The notebook with **`InventoryLakehouse`** attached as the default Lakehouse and the **Run all** button highlighted. | +| `challenge-00-notebook-ids.png` | 0 | The setup notebook's final cell output printing the **Workspace ID** and **Data Agent (Agent) ID** (values redacted). | +| `challenge-00-agent-test.png` | 0 | The published data agent's **Test data agent** pane answering the Leaf Blower X2 question with exact **CRITICAL** stock levels for Portland and Seattle (all tables selected). | +| `challenge-00-no-tables.png` | 0 | Troubleshooting: the data agent with **no tables selected** and the agent replying it *can't access the data source*. | +| `challenge-01-new-connection.png` | 1 | The **Add tool → Fabric Data Agent** connection dialog (Workspace ID / Artifact ID redacted) named `inventory-hack-agent` — created in Challenge 2. | +| `challenge-01-new-agent.png` | 1 | New agent editor for `demand-sensing-agent`: name, `gpt-5.4-mini` model, and populated instructions. | +| `challenge-01-add-tool.png` | 1 | The tool catalogue in the agent editor with **Web Search** and **Fabric Data Agent** available. | +| `challenge-01-playground.png` | 1 | Playground response with a Fabric inventory result (and a web source if Web Search is on) ending in a demand assessment. | +| `challenge-02-recommendation.png` | 2 | Reorder **recommendation table** (SKU / Warehouse / Current Stock / Suggested Reorder Qty / Priority) with a **CRITICAL** item. | +| `challenge-02-trace.png` | 2 | **Tracing** detail expanded to the model call, Fabric tool call, tool response, and final generation spans. | +| `challenge-03-approval-flow.png` | 3 | The `MODIFY 1 50` → `YES` approval exchange and the simulated submission confirmation. | +| `challenge-03-trace.png` | 3 | Trace of the approval run: model calls plus the Fabric unit-cost lookup across propose → MODIFY → YES. | +| `challenge-04-orchestrator.png` | 4 *(stretch)* | The `inventory-planning-workflow` canvas: Start → demand-sensing → inventory-optimisation → replenishment-action. | +| `challenge-04-teams.png` | 4 *(stretch)* | The workflow's **Publish** menu open on **Teams & Microsoft 365 Copilot** — the "what's next" showcase entry point. | diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/architecture.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/architecture.png new file mode 100644 index 000000000..888a4acda Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/architecture.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-agent-test.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-agent-test.png new file mode 100644 index 000000000..eed7ad710 Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-agent-test.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-attach-lakehouse.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-attach-lakehouse.png new file mode 100644 index 000000000..a926d3f51 Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-attach-lakehouse.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-create-workspace.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-create-workspace.png new file mode 100644 index 000000000..d412cd6c4 Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-create-workspace.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-import-notebook.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-import-notebook.png new file mode 100644 index 000000000..1f092edc2 Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-import-notebook.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-import-upload.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-import-upload.png new file mode 100644 index 000000000..a5915bd85 Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-import-upload.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-lakehouse-name.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-lakehouse-name.png new file mode 100644 index 000000000..d3f5568a1 Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-lakehouse-name.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-model-deployments.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-model-deployments.png new file mode 100644 index 000000000..4a660da59 Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-model-deployments.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-new-lakehouse.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-new-lakehouse.png new file mode 100644 index 000000000..77488738c Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-new-lakehouse.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-new-workspace-button.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-new-workspace-button.png new file mode 100644 index 000000000..48e283bfd Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-new-workspace-button.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-no-tables.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-no-tables.png new file mode 100644 index 000000000..25fbbab5c Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-no-tables.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-notebook-ids.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-notebook-ids.png new file mode 100644 index 000000000..ed69b1ef1 Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-notebook-ids.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-notebook-imported.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-notebook-imported.png new file mode 100644 index 000000000..e273bacdd Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-notebook-imported.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-playground-what-can-you-do.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-playground-what-can-you-do.png new file mode 100644 index 000000000..726f21c90 Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-playground-what-can-you-do.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-run-all.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-run-all.png new file mode 100644 index 000000000..a7a1ca38c Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-00-run-all.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-01-add-tool.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-01-add-tool.png new file mode 100644 index 000000000..0aa4e2710 Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-01-add-tool.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-01-new-agent.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-01-new-agent.png new file mode 100644 index 000000000..6c05a7ef5 Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-01-new-agent.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-01-new-connection.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-01-new-connection.png new file mode 100644 index 000000000..969325d61 Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-01-new-connection.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-01-playground.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-01-playground.png new file mode 100644 index 000000000..acb2eb939 Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-01-playground.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-02-recommendation.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-02-recommendation.png new file mode 100644 index 000000000..f6f26d3b6 Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-02-recommendation.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-02-trace.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-02-trace.png new file mode 100644 index 000000000..bd3e7925c Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-02-trace.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-03-approval-flow.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-03-approval-flow.png new file mode 100644 index 000000000..d60338cb1 Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-03-approval-flow.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-03-trace.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-03-trace.png new file mode 100644 index 000000000..406dcc7ee Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-03-trace.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-04-orchestrator.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-04-orchestrator.png new file mode 100644 index 000000000..8f018a837 Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-04-orchestrator.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-04-teams.png b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-04-teams.png new file mode 100644 index 000000000..07b02d87b Binary files /dev/null and b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/images/challenge-04-teams.png differ diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/README.md b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/README.md new file mode 100644 index 000000000..a744e142d --- /dev/null +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/README.md @@ -0,0 +1,49 @@ +# Lab Automation — Agentic Inventory Planning MicroHack + +Everything for this hack is provisioned per attendee by [`deploy-lab.ps1`](deploy-lab.ps1) — there is **no shared Fabric backend**. Each attendee gets their own Foundry project **and** their own Fabric F2 capacity, then builds their own Fabric workspace + Data Agent by running [`Setup-InventoryDataAgent.ipynb`](../setup/Setup-InventoryDataAgent.ipynb) in Challenge 1. + +## What `deploy-lab.ps1` provisions (per attendee) + +| Resource | Type | Notes | +|----------|------|-------| +| Azure AI Foundry account | `Microsoft.CognitiveServices/accounts` | One per attendee (AIServices kind) | +| Foundry project | `…/projects/inventory-hack` | Within the Foundry account | +| 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. + +## What the attendee does themselves (Challenge 1) + +1. Create a Fabric **workspace** and assign it to their **F2 capacity** (`FabricCapacityName`). +2. Create a Lakehouse named **`InventoryLakehouse`** and attach it to the notebook. +3. Import and **Run All** [`Setup-InventoryDataAgent.ipynb`](../setup/Setup-InventoryDataAgent.ipynb) — it loads its **embedded** seed data, writes 7 tables, publishes their **`inventory-hack-agent`** Data Agent, and prints **their** Workspace ID + Agent ID. +4. Use those two IDs when adding the Fabric Data Agent tool to their Foundry agent (Challenge 2). + +## Credentials returned to the attendee dashboard + +| Key | Value | +|-----|-------| +| `FoundryProjectEndpoint` | `https://.services.ai.azure.com/api/projects/inventory-hack` | +| `ModelDeploymentName` | `gpt-5.4-mini` | +| `FabricCapacityName` | The attendee's own F2 capacity (assign your workspace to 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. +- **`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. +- The **`Microsoft.Fabric`** resource provider is registered on the subscription (the script registers it if needed). + +> [!NOTE] +> Attendee object IDs → UPNs are resolved with the platform **`Get-MhhLabUser`** helper (served from a platform-seeded cache), so the service principal does **not** need Microsoft Graph directory-read permission. + +## Cost estimate + +- **Fabric F2 capacity** (~$0.36/hr pay-as-you-go) is the dominant cost — roughly **$8–9/attendee/day** if left running. Attendees (or facilitators) should **suspend** the capacity when idle. +- gpt-5.4-mini (GlobalStandard, 100K TPM) — a few cents of tokens per attendee across the hack's ~50 agent calls; Foundry account base ~$2/day. +- Total ≈ **$9/attendee/day** — reflected in [`lab-defaults.json`](lab-defaults.json) (`estimatedDailyCostsUsd`). diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/deploy-lab.ps1 b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/deploy-lab.ps1 new file mode 100644 index 000000000..622c69190 --- /dev/null +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/deploy-lab.ps1 @@ -0,0 +1,337 @@ +<# +.SYNOPSIS + Deploys the Azure AI Foundry project and model deployment for the + Agentic Inventory Planning MicroHack. + +.DESCRIPTION + Called by the EMEA MicroHack platform once per attendee, in parallel. + Provisions: + - Azure AI Services account (AIServices kind — this is the Foundry account) + - Azure AI Foundry project inside the account + - gpt-5.4-mini model deployment (capacity 100 GlobalStandard = 100K TPM) + - A per-attendee Fabric F2 capacity, with the attendee set as capacity admin + + Each attendee gets their OWN Fabric capacity (no shared backend, no shared + Spark contention). The attendee then creates a workspace, assigns it to their + capacity, and Run All's the setup notebook themselves (Challenge 1) to load the + data and publish their own Fabric Data Agent. + + Returns to the attendee dashboard: + - Foundry project endpoint (format: https://{name}.services.ai.azure.com/api/projects/{proj}) + - Model deployment name + - The name of their Fabric F2 capacity (used when they create their workspace) + + The platform pre-sets the Az context to $SubscriptionId. For 'resourcegroup' + deployments it also pre-creates the resource group; for 'subscription' + deployments this script creates the resource group itself. Do NOT call + Connect-AzAccount. + +.PARAMETER DeploymentType + Deployment scope — passed in by the platform. + +.PARAMETER SubscriptionId + Azure subscription — passed in by the platform. + +.PARAMETER ResourceGroupName + Pre-created resource group — passed in by the platform. + +.PARAMETER PreferredLocation + Ordered list of preferred Azure regions — passed in by the platform. + +.PARAMETER AllowedEntraUserIds + Entra user object IDs for this lab — passed in by the platform. +#> +param( + [Parameter(Mandatory=$true)] + [ValidateSet('subscription','resourcegroup','resourcegroup-with-subscriptionowner')] + [string]$DeploymentType, + + [Parameter(Mandatory=$true)] + [string]$SubscriptionId, + + [string]$ResourceGroupName = "", + + [string[]]$PreferredLocation = @(), + + [string[]]$AllowedEntraUserIds = @() +) + +$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +$ErrorActionPreference = "Stop" + +# Resolve effective values — honour $PreferredLocation, fall back across the list +$candidateRegions = if ($PreferredLocation.Count -gt 0) { $PreferredLocation } else { @("swedencentral", "westeurope", "norwayeast") } +$effectiveLocation = $candidateRegions[0] +$effectiveRG = $ResourceGroupName +$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, +# per-attendee name so the rest of this (RG-scoped) script works unchanged. +if ([string]::IsNullOrWhiteSpace($effectiveRG)) { + $effectiveRG = "rg-inv-hack-$stableHash" + if (-not (Get-AzResourceGroup -Name $effectiveRG -ErrorAction SilentlyContinue)) { + New-AzResourceGroup -Name $effectiveRG -Location $effectiveLocation | Out-Null + Write-Host "[OK] Created resource group '$effectiveRG' (subscription deployment)." + } +} + +# Foundry account name = custom subdomain name (must be globally unique) +# Pattern: inv-{12-char hash} keeps it short and DNS-safe (Get-MhhStableHash minimum length is 12) +$foundryAccountName = "inv-$stableHash" +$foundryProjectName = "inventory-hack" +$modelDeployment = "gpt-5.4-mini" + +# Per-attendee Fabric F2 capacity name (lowercase alphanumeric, 3-63 chars). +$fabricCapacityName = "invcap$stableHash" + +Write-Host "[INFO] Deploying Foundry account '$foundryAccountName' in '$effectiveRG'..." + +# --------------------------------------------------------------------------- +# Azure AI Services account (this is the Foundry Hub) +# Kind must be 'AIServices' — 'AIFoundry' is not a valid kind. +# customSubDomainName = account name → enables {name}.services.ai.azure.com endpoint +# --------------------------------------------------------------------------- +$existingAccount = Get-AzResource ` + -ResourceGroupName $effectiveRG ` + -ResourceType "Microsoft.CognitiveServices/accounts" ` + -Name $foundryAccountName ` + -ErrorAction SilentlyContinue + +if ($existingAccount) { + $effectiveLocation = $existingAccount.Location + Write-Host "[OK] Foundry account already exists in '$effectiveLocation' — skipping." +} else { + $accountCreated = $false + foreach ($region in $candidateRegions) { + Write-Host "[INFO] Attempting Foundry account in '$region'..." + # A system-assigned identity is REQUIRED before a project can be created + # ("To create projects, you must enable a managed identity on your resource"). + $accountBody = @{ + kind = "AIServices" + sku = @{ name = "S0" } + location = $region + identity = @{ type = "SystemAssigned" } + properties = @{ + customSubDomainName = $foundryAccountName + publicNetworkAccess = "Enabled" + allowProjectManagement = $true + } + } | ConvertTo-Json -Depth 5 + + $accountUri = "/subscriptions/$SubscriptionId/resourceGroups/$effectiveRG" + + "/providers/Microsoft.CognitiveServices/accounts/$foundryAccountName" + + "?api-version=2025-04-01-preview" + + # Invoke-AzRestMethod does NOT throw on non-2xx — inspect the status code. + $resp = Invoke-AzRestMethod -Method PUT -Path $accountUri -Payload $accountBody + + # A soft-deleted account (48h retention) reserves the subdomain globally and + # blocks re-creation with HTTP 409 CustomDomainInUse. This is common when a lab + # is torn down and re-provisioned for the same attendee. Purge it and retry. + if ($resp.StatusCode -eq 409 -and $resp.Content -match 'CustomDomainInUse') { + Write-Warning "Subdomain '$foundryAccountName' is held by a soft-deleted account — purging and retrying." + $listResp = Invoke-AzRestMethod -Method GET -Path "/subscriptions/$SubscriptionId/providers/Microsoft.CognitiveServices/deletedAccounts?api-version=2025-04-01-preview" + if ($listResp.StatusCode -eq 200) { + foreach ($d in (($listResp.Content | ConvertFrom-Json).value | Where-Object { $_.name -eq $foundryAccountName })) { + $delRg = if ($d.id -match '/resourceGroups/([^/]+)/deletedAccounts/') { $Matches[1] } else { $effectiveRG } + Invoke-AzRestMethod -Method DELETE -Path "/subscriptions/$SubscriptionId/providers/Microsoft.CognitiveServices/locations/$($d.location)/resourceGroups/$delRg/deletedAccounts/${foundryAccountName}?api-version=2025-04-01-preview" | Out-Null + } + Start-Sleep -Seconds 5 + } + $resp = Invoke-AzRestMethod -Method PUT -Path $accountUri -Payload $accountBody + } + + if ($resp.StatusCode -ge 400) { + Write-Warning "Region '$region' rejected the account create (HTTP $($resp.StatusCode)): $($resp.Content) — trying next region." + continue + } + + # Wait for provisioning (AIServices accounts can take several minutes). + $timeout = 300; $elapsed = 0; $acct = $null + do { + Start-Sleep -Seconds 10; $elapsed += 10 + $acct = Get-AzResource -ResourceGroupName $effectiveRG -ResourceType "Microsoft.CognitiveServices/accounts" -Name $foundryAccountName -ErrorAction SilentlyContinue + } while (($acct.Properties.provisioningState -ne "Succeeded") -and ($elapsed -lt $timeout)) + + if ($acct.Properties.provisioningState -eq "Succeeded") { + $effectiveLocation = $region + $accountCreated = $true + Write-Host "[OK] Foundry account created in '$region'." + break + } + Write-Warning "Provisioning did not succeed in '$region' within ${timeout}s — trying next region." + } + if (-not $accountCreated) { + throw "Could not create the Foundry account in any preferred region: $($candidateRegions -join ', ')" + } +} + +# --------------------------------------------------------------------------- +# Foundry project +# --------------------------------------------------------------------------- +$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 + + Invoke-AzRestMethod -Method PUT -Path $projectUri -Payload $projectBody | Out-Null + + $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)) + + Write-Host "[OK] Foundry project '$foundryProjectName' created." +} else { + Write-Host "[OK] Foundry project already exists — skipping." +} + +# --------------------------------------------------------------------------- +# 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 +# card confirms Functions/Tools + Structured Outputs, on GlobalStandard in EU +# regions, with the longest support horizon among the low-cost models +# (retires 2027-03-18; gpt-4o-mini / gpt-4.1-mini retire Oct 2026). +# +# gpt-5.4-mini is a GPT-5 *reasoning* model. In the Foundry tool picker the Fabric +# Data Agent tool may show "Not supported by the selected model" - that flag is +# cosmetic; the tool still runs via the Responses API. If an agent skips the +# Fabric tool, set tool choice = required in the run settings (the challenge +# instructions carry an "IMPORTANT - tool use" block for exactly this). +# +# Capacity 100 = 100K TPM per attendee. Keep labsPerSubscription x 100 <= the +# region's GlobalStandard quota (~1,000) - 4 labs/subscription fits comfortably. +# Model deployments are ACCOUNT-scoped (not project-scoped): the ARM path is +# .../accounts/{account}/deployments/{deployment} - there is no /projects/ segment. +# --------------------------------------------------------------------------- +$deploymentUri = "/subscriptions/$SubscriptionId/resourceGroups/$effectiveRG" + + "/providers/Microsoft.CognitiveServices/accounts/$foundryAccountName" + + "/deployments/$modelDeployment" + + "?api-version=2025-04-01-preview" + +$existingDeploy = Invoke-AzRestMethod -Method GET -Path $deploymentUri -ErrorAction SilentlyContinue +if ($existingDeploy.StatusCode -ne 200) { + $deploymentBody = @{ + sku = @{ name = "GlobalStandard"; capacity = 100 } + properties = @{ + model = @{ format = "OpenAI"; name = "gpt-5.4-mini"; version = "2026-03-17" } + } + } | ConvertTo-Json -Depth 10 + + try { + Invoke-AzRestMethod -Method PUT -Path $deploymentUri -Payload $deploymentBody | Out-Null + Write-Host "[OK] Model deployment '$modelDeployment' initiated." + } catch { + Write-Host "[WARN] Model deployment: $_ — may already exist, continuing." + } +} else { + Write-Host "[OK] Model deployment already exists — skipping." +} + +# --------------------------------------------------------------------------- +# Per-attendee Fabric F2 capacity (ARM REST — no az CLI / Fabric extension needed). +# The attendee is set as capacity ADMIN so they can create a workspace, assign it +# to this capacity, and Run All the setup notebook themselves (Challenge 1). +# F2 = 2 CU; a 512-CU subscription supports ~256 attendees. +# +# Capacity admin members must be UPNs (or service principals) — bare object IDs are +# rejected — so resolve each attendee object ID to a UPN with the platform helper +# Get-MhhLabUser. It is served from a platform-seeded cache (no Microsoft Graph +# permission needed on the deploying service principal) and falls back to +# Get-AzADUser transparently when run locally. +# --------------------------------------------------------------------------- +$fabricCapacityRegion = $null +$adminUpns = @() +foreach ($uid in $AllowedEntraUserIds) { + try { + $u = Get-MhhLabUser -UserId $uid -ErrorAction Stop + if ($u.UserPrincipalName) { $adminUpns += $u.UserPrincipalName } + } catch { + Write-Warning "Could not resolve attendee object ID '$uid' to a UPN via Get-MhhLabUser: $_" + } +} + +if ($adminUpns.Count -eq 0) { + Write-Warning "No attendee UPNs resolved — skipping Fabric capacity creation. Create the F2 capacity manually and set the attendee as capacity admin, or re-run once the lab-user cache is available." +} else { + # Ensure the Microsoft.Fabric resource provider is registered (idempotent). + $prov = Invoke-AzRestMethod -Method GET -Path "/subscriptions/$SubscriptionId/providers/Microsoft.Fabric?api-version=2021-04-01" + if ($prov.StatusCode -eq 200 -and ($prov.Content | ConvertFrom-Json).registrationState -ne "Registered") { + Invoke-AzRestMethod -Method POST -Path "/subscriptions/$SubscriptionId/providers/Microsoft.Fabric/register?api-version=2021-04-01" | Out-Null + } + + $capExists = Get-AzResource -ResourceGroupName $effectiveRG -ResourceType "Microsoft.Fabric/capacities" -Name $fabricCapacityName -ErrorAction SilentlyContinue + if ($capExists) { + $fabricCapacityRegion = $capExists.Location + Write-Host "[OK] Fabric capacity '$fabricCapacityName' already exists in '$fabricCapacityRegion' — skipping." + } else { + foreach ($region in $candidateRegions) { + Write-Host "[INFO] Creating Fabric F2 capacity '$fabricCapacityName' in '$region'..." + $capBody = @{ + location = $region + sku = @{ name = "F2"; tier = "Fabric" } + properties = @{ administration = @{ members = @($adminUpns) } } + } | ConvertTo-Json -Depth 6 + $capUri = "/subscriptions/$SubscriptionId/resourceGroups/$effectiveRG" + + "/providers/Microsoft.Fabric/capacities/$fabricCapacityName" + + "?api-version=2023-11-01" + $capResp = Invoke-AzRestMethod -Method PUT -Path $capUri -Payload $capBody + if ($capResp.StatusCode -ge 400) { + Write-Warning "Fabric capacity create in '$region' failed (HTTP $($capResp.StatusCode)): $($capResp.Content) — trying next region." + continue + } + $elapsed = 0; $cap = $null + do { + Start-Sleep -Seconds 10; $elapsed += 10 + $cap = Get-AzResource -ResourceGroupName $effectiveRG -ResourceType "Microsoft.Fabric/capacities" -Name $fabricCapacityName -ErrorAction SilentlyContinue + } while (($cap.Properties.provisioningState -ne "Succeeded") -and ($elapsed -lt 180)) + if ($cap.Properties.provisioningState -eq "Succeeded") { + $fabricCapacityRegion = $region + Write-Host "[OK] Fabric F2 capacity '$fabricCapacityName' created in '$region'." + break + } + Write-Warning "Fabric capacity provisioning did not succeed in '$region' — trying next region." + } + if (-not $fabricCapacityRegion) { + Write-Warning "Could not create the Fabric capacity in any preferred region (check Fabric F-SKU quota). The attendee can still do the Foundry steps; provision Fabric manually." + } + } +} + +# --------------------------------------------------------------------------- +# Return credentials to the attendee dashboard +# --------------------------------------------------------------------------- +$projectEndpoint = "https://$foundryAccountName.services.ai.azure.com/api/projects/$foundryProjectName" + +Write-Host "[OK] Lab provisioning complete." + +@{ HackboxCredential = @{ + name = "FoundryProjectEndpoint" + value = $projectEndpoint + note = "Open ai.azure.com → select this project OR use as SDK endpoint" +} } + +@{ HackboxCredential = @{ + name = "ModelDeploymentName" + value = $modelDeployment + note = "Select this model when creating agents in the Foundry portal" +} } + +@{ HackboxCredential = @{ + name = "FabricCapacityName" + value = $fabricCapacityName + 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 = "ResourceGroupName" + value = $effectiveRG + note = "The resource group containing your Foundry account and Fabric capacity — find your resources here in the Azure portal" +} } diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/lab-defaults.json b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/lab-defaults.json new file mode 100644 index 000000000..6ccfb3b38 --- /dev/null +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/lab-defaults.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/MicroHack/refs/heads/main/lab-defaults-schema.json", + "groups": ["M365-E5-Users"], + "deploymentType": "resourcegroup", + "labsPerSubscription": 6, + "preferredLocation": "swedencentral, westeurope, norwayeast", + "estimatedDailyCostsUsd": 9.0 +} diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/setup/Setup-InventoryDataAgent.ipynb b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/setup/Setup-InventoryDataAgent.ipynb new file mode 100644 index 000000000..9d464317a --- /dev/null +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/setup/Setup-InventoryDataAgent.ipynb @@ -0,0 +1,443 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "2b444e34", + "metadata": {}, + "source": [ + "# Setup — your inventory data + Fabric Data Agent (run once)\n", + "\n", + "Run this notebook **inside your own Fabric workspace** — the one you created and assigned to your F2 capacity (see Challenge 1).\n", + "\n", + "**Prerequisites**\n", + "1. Your **F2 capacity** (`FabricCapacityName` from your lab dashboard) is **running**, and this workspace is assigned to it.\n", + "2. A Lakehouse named **`InventoryLakehouse`** exists in this workspace and is attached to this notebook (Explorer → **Add Lakehouse**).\n", + "3. That's it — the seed data is **embedded in this notebook**, so there is no upload or download.\n", + "\n", + "This notebook: loads the embedded seed data → builds the `Inventory` and `DemandHistory` fact tables deterministically → writes 7 Lakehouse tables → creates and **publishes** your `inventory-hack-agent` Fabric Data Agent → prints **your** Workspace ID and Agent ID for Challenge 2.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1b595b90", + "metadata": {}, + "outputs": [], + "source": [ + "# Install both preview SDKs here, at the very top. %pip RESTARTS the Spark kernel and\n", + "# wipes every variable defined before it — so ALL installs must happen up front, and\n", + "# there must be NO later %pip cell (a second restart would wipe `agent`, etc.).\n", + "%pip install -q fabric-data-agent-sdk mcp\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c1783b44", + "metadata": {}, + "outputs": [], + "source": [ + "import json, math\n", + "from datetime import date, timedelta\n", + "\n", + "# ---------------------------------------------------------------------------\n", + "# Seed data is embedded inline so this notebook is fully self-contained — no file\n", + "# upload and no external download, regardless of where this repo ultimately lives.\n", + "# This notebook is the single source of truth for the seed data.\n", + "# ---------------------------------------------------------------------------\n", + "products = [\n", + " {\"productId\": \"P001\", \"name\": \"Lawn Mower Pro\", \"category\": \"garden_and_lawn\", \"unitCost\": 180, \"leadTimeDays\": 14, \"supplierId\": \"S01\", \"baseWeeklyUnits\": 8, \"baseOnHand\": 40},\n", + " {\"productId\": \"P002\", \"name\": \"Garden Hose 50ft\", \"category\": \"garden_and_lawn\", \"unitCost\": 25, \"leadTimeDays\": 7, \"supplierId\": \"S01\", \"baseWeeklyUnits\": 30, \"baseOnHand\": 150},\n", + " {\"productId\": \"P003\", \"name\": \"Fertilizer 20lb\", \"category\": \"garden_and_lawn\", \"unitCost\": 18, \"leadTimeDays\": 10, \"supplierId\": \"S01\", \"baseWeeklyUnits\": 45, \"baseOnHand\": 200},\n", + " {\"productId\": \"P004\", \"name\": \"Leaf Blower X2\", \"category\": \"outdoor_power_tools\", \"unitCost\": 95, \"leadTimeDays\": 21, \"supplierId\": \"S02\", \"baseWeeklyUnits\": 20, \"baseOnHand\": 30},\n", + " {\"productId\": \"P005\", \"name\": \"Chainsaw 16in\", \"category\": \"outdoor_power_tools\", \"unitCost\": 140, \"leadTimeDays\": 21, \"supplierId\": \"S02\", \"baseWeeklyUnits\": 12, \"baseOnHand\": 25},\n", + " {\"productId\": \"P006\", \"name\": \"Hedge Trimmer\", \"category\": \"outdoor_power_tools\", \"unitCost\": 85, \"leadTimeDays\": 18, \"supplierId\": \"S02\", \"baseWeeklyUnits\": 15, \"baseOnHand\": 28},\n", + " {\"productId\": \"P007\", \"name\": \"Interior Paint 1gal\", \"category\": \"paint_and_supplies\", \"unitCost\": 22, \"leadTimeDays\": 7, \"supplierId\": \"S03\", \"baseWeeklyUnits\": 50, \"baseOnHand\": 300},\n", + " {\"productId\": \"P008\", \"name\": \"Paint Roller Set\", \"category\": \"paint_and_supplies\", \"unitCost\": 12, \"leadTimeDays\": 5, \"supplierId\": \"S03\", \"baseWeeklyUnits\": 40, \"baseOnHand\": 250},\n", + " {\"productId\": \"P009\", \"name\": \"Painters Tape\", \"category\": \"paint_and_supplies\", \"unitCost\": 6, \"leadTimeDays\": 5, \"supplierId\": \"S03\", \"baseWeeklyUnits\": 60, \"baseOnHand\": 400},\n", + " {\"productId\": \"P010\", \"name\": \"Smart Thermostat\", \"category\": \"smart_home\", \"unitCost\": 110, \"leadTimeDays\": 12, \"supplierId\": \"S04\", \"baseWeeklyUnits\": 18, \"baseOnHand\": 90},\n", + " {\"productId\": \"P011\", \"name\": \"Video Doorbell\", \"category\": \"smart_home\", \"unitCost\": 130, \"leadTimeDays\": 12, \"supplierId\": \"S04\", \"baseWeeklyUnits\": 16, \"baseOnHand\": 80},\n", + " {\"productId\": \"P012\", \"name\": \"Smart Plug 4-pack\", \"category\": \"smart_home\", \"unitCost\": 28, \"leadTimeDays\": 9, \"supplierId\": \"S04\", \"baseWeeklyUnits\": 35, \"baseOnHand\": 180},\n", + "]\n", + "\n", + "suppliers = [\n", + " {\"supplierId\": \"S01\", \"name\": \"GreenField Supply\", \"region\": \"Midwest\", \"reliabilityScore\": 0.92},\n", + " {\"supplierId\": \"S02\", \"name\": \"PowerTool Depot\", \"region\": \"West\", \"reliabilityScore\": 0.85},\n", + " {\"supplierId\": \"S03\", \"name\": \"ColorCraft Paints\", \"region\": \"Northeast\", \"reliabilityScore\": 0.95},\n", + " {\"supplierId\": \"S04\", \"name\": \"ConnectHome\", \"region\": \"West\", \"reliabilityScore\": 0.88},\n", + "]\n", + "\n", + "stores = [\n", + " {\"storeId\": \"ST01\", \"name\": \"Boston Store\", \"type\": \"retail\", \"region\": \"Northeast\", \"city\": \"Boston\", \"state\": \"MA\"},\n", + " {\"storeId\": \"ST02\", \"name\": \"Brooklyn Store\", \"type\": \"retail\", \"region\": \"Northeast\", \"city\": \"Brooklyn\", \"state\": \"NY\"},\n", + " {\"storeId\": \"ST03\", \"name\": \"Portland Store\", \"type\": \"retail\", \"region\": \"Pacific Northwest\", \"city\": \"Portland\", \"state\": \"OR\"},\n", + " {\"storeId\": \"ST04\", \"name\": \"Seattle Store\", \"type\": \"retail\", \"region\": \"Pacific Northwest\", \"city\": \"Seattle\", \"state\": \"WA\"},\n", + " {\"storeId\": \"WH01\", \"name\": \"Chicago Warehouse\", \"type\": \"warehouse\", \"region\": \"Midwest\", \"city\": \"Chicago\", \"state\": \"IL\"},\n", + " {\"storeId\": \"WH02\", \"name\": \"Dallas Warehouse\", \"type\": \"warehouse\", \"region\": \"South\", \"city\": \"Dallas\", \"state\": \"TX\"},\n", + "]\n", + "\n", + "signals = [\n", + " {\"signalId\": \"SIG01\", \"signalType\": \"weather\", \"region\": \"Pacific Northwest\", \"headline\": \"Prolonged heatwave and early spring across the Pacific Northwest\", \"score\": 0.80, \"source\": \"WeatherIQ\", \"affectedCategories\": \"outdoor_power_tools,garden_and_lawn\"},\n", + " {\"signalId\": \"SIG02\", \"signalType\": \"search_trend\", \"region\": \"National\", \"headline\": \"Sharp spike in online searches for leaf blowers and lawn tools\", \"score\": 0.75, \"source\": \"SearchIQ\", \"affectedCategories\": \"outdoor_power_tools\"},\n", + " {\"signalId\": \"SIG03\", \"signalType\": \"competitor\", \"region\": \"West\", \"headline\": \"Major competitor reported out of stock on chainsaws\", \"score\": 0.60, \"source\": \"MarketIQ\", \"affectedCategories\": \"outdoor_power_tools\"},\n", + " {\"signalId\": \"SIG04\", \"signalType\": \"news\", \"region\": \"National\", \"headline\": \"Home improvement season demand surge expected\", \"score\": 0.50, \"source\": \"NewsIQ\", \"affectedCategories\": \"paint_and_supplies,garden_and_lawn\"},\n", + " {\"signalId\": \"SIG05\", \"signalType\": \"promo\", \"region\": \"National\", \"headline\": \"Competitor launches smart-home bundle promotion\", \"score\": 0.40, \"source\": \"MarketIQ\", \"affectedCategories\": \"smart_home\"},\n", + " {\"signalId\": \"SIG06\", \"signalType\": \"weather\", \"region\": \"Northeast\", \"headline\": \"Active storm season forecast for the Northeast\", \"score\": 0.55, \"source\": \"WeatherIQ\", \"affectedCategories\": \"outdoor_power_tools\"},\n", + " {\"signalId\": \"SIG07\", \"signalType\": \"supply\", \"region\": \"West\", \"headline\": \"Supplier delay reported at PowerTool Depot\", \"score\": 0.65, \"source\": \"SupplyIQ\", \"affectedCategories\": \"outdoor_power_tools\"},\n", + " {\"signalId\": \"SIG08\", \"signalType\": \"search_trend\", \"region\": \"National\", \"headline\": \"Interior painting projects trending on social media\", \"score\": 0.45, \"source\": \"SearchIQ\", \"affectedCategories\": \"paint_and_supplies\"},\n", + "]\n", + "\n", + "orders_in = [\n", + " {\"orderId\": \"ORD-1001\", \"type\": \"purchase\", \"status\": \"submitted\", \"requestedBy\": \"Inventory Optimisation Agent\", \"approvedBy\": \"Sam Rivera\", \"createdDate\": \"2026-06-15\", \"items\": [{\"sku\": \"P007\", \"location\": \"WH01\", \"quantity\": 200}]},\n", + " {\"orderId\": \"ORD-1002\", \"type\": \"transfer\", \"status\": \"completed\", \"requestedBy\": \"Planning Team\", \"approvedBy\": \"Sam Rivera\", \"createdDate\": \"2026-06-22\", \"items\": [{\"sku\": \"P002\", \"location\": \"ST01\", \"quantity\": 60}]},\n", + " {\"orderId\": \"ORD-1003\", \"type\": \"purchase\", \"status\": \"rejected\", \"requestedBy\": \"Inventory Optimisation Agent\", \"approvedBy\": \"Sam Rivera\", \"createdDate\": \"2026-06-28\", \"items\": [{\"sku\": \"P010\", \"location\": \"WH02\", \"quantity\": 120}]},\n", + " {\"orderId\": \"ORD-1004\", \"type\": \"purchase\", \"status\": \"submitted\", \"requestedBy\": \"Inventory Optimisation Agent\", \"approvedBy\": \"Jordan Lee\", \"createdDate\": \"2026-07-02\", \"items\": [{\"sku\": \"P004\", \"location\": \"WH01\", \"quantity\": 150}]},\n", + "]\n", + "\n", + "_overrides = [\n", + " {\"productId\": \"P004\", \"storeId\": \"ST03\", \"onHand\": 8},\n", + " {\"productId\": \"P004\", \"storeId\": \"ST04\", \"onHand\": 6},\n", + " {\"productId\": \"P004\", \"storeId\": \"WH01\", \"onHand\": 45},\n", + " {\"productId\": \"P004\", \"storeId\": \"WH02\", \"onHand\": 40},\n", + " {\"productId\": \"P005\", \"storeId\": \"ST03\", \"onHand\": 5},\n", + " {\"productId\": \"P005\", \"storeId\": \"ST04\", \"onHand\": 7},\n", + " {\"productId\": \"P006\", \"storeId\": \"ST03\", \"onHand\": 12},\n", + "]\n", + "overrides = {(o[\"productId\"], o[\"storeId\"]): o[\"onHand\"] for o in _overrides}\n", + "\n", + "print(f\"Loaded {len(products)} products, {len(stores)} locations, {len(signals)} signals.\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "babc0dba", + "metadata": {}, + "outputs": [], + "source": [ + "# ---- Build Inventory (12 products x 6 locations = 72 rows), deterministic ----\n", + "# reorderPoint = ceil(weekly demand over the supplier lead time)\n", + "# safetyStock = ceil(reorderPoint * 0.5)\n", + "# warehouses hold ~4x a retail location's base stock; story SKUs come from overrides.\n", + "inventory = []\n", + "for p in products:\n", + " rp = math.ceil(p[\"baseWeeklyUnits\"] * p[\"leadTimeDays\"] / 7)\n", + " ss = math.ceil(rp * 0.5)\n", + " for s in stores:\n", + " on_hand = p[\"baseOnHand\"] * 4 if s[\"type\"] == \"warehouse\" else p[\"baseOnHand\"]\n", + " on_hand = overrides.get((p[\"productId\"], s[\"storeId\"]), on_hand)\n", + " inventory.append({\n", + " \"productId\": p[\"productId\"], \"storeId\": s[\"storeId\"],\n", + " \"onHand\": int(on_hand), \"reorderPoint\": int(rp), \"safetyStock\": int(ss)\n", + " })\n", + "\n", + "# ---- Build DemandHistory: last 6 weeks, retail stores, +40% recent uplift for tools ----\n", + "WEEKS = 6\n", + "anchor_monday = date(2026, 7, 6)\n", + "retail = [s for s in stores if s[\"type\"] == \"retail\"]\n", + "demand = []\n", + "for p in products:\n", + " per_store = max(1, round(p[\"baseWeeklyUnits\"] / len(retail)))\n", + " for w in range(WEEKS):\n", + " wk = anchor_monday - timedelta(weeks=(WEEKS - 1 - w))\n", + " for s in retail:\n", + " units = per_store\n", + " if p[\"category\"] == \"outdoor_power_tools\" and w >= WEEKS - 2:\n", + " units = round(units * 1.4) # recent demand spike\n", + " demand.append({\"productId\": p[\"productId\"], \"storeId\": s[\"storeId\"],\n", + " \"weekStart\": wk.isoformat(), \"units\": int(units)})\n", + "\n", + "# ---- Flatten replenishment orders (items -> itemsJson string) ----\n", + "orders = []\n", + "for o in orders_in:\n", + " orders.append({\n", + " \"orderId\": o[\"orderId\"], \"type\": o[\"type\"], \"status\": o[\"status\"],\n", + " \"requestedBy\": o[\"requestedBy\"], \"approvedBy\": o[\"approvedBy\"],\n", + " \"createdDate\": o[\"createdDate\"], \"itemsJson\": json.dumps(o[\"items\"])\n", + " })\n", + "\n", + "print(f\"Inventory rows: {len(inventory)} | DemandHistory rows: {len(demand)} | Orders: {len(orders)}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ac624243", + "metadata": {}, + "outputs": [], + "source": [ + "# A default Lakehouse must be attached, or saveAsTable fails with a cryptic\n", + "# \"No default context found\" Spark error. Check early and explain the fix.\n", + "import notebookutils\n", + "if not notebookutils.runtime.context.get(\"defaultLakehouseId\"):\n", + " raise RuntimeError(\n", + " \"No default Lakehouse is attached to this notebook. In the Explorer pane (left), \"\n", + " \"click Add > Existing Lakehouse > InventoryLakehouse (in your inventory-hack workspace), \"\n", + " \"then Run all again.\"\n", + " )\n", + "\n", + "# ---- Write all 7 tables to the Lakehouse (Delta) ----\n", + "def write_table(name, rows):\n", + " spark.createDataFrame(rows).write.mode(\"overwrite\").format(\"delta\").saveAsTable(name)\n", + " print(f\" wrote {name} ({len(rows)} rows)\")\n", + "\n", + "write_table(\"Products\", products)\n", + "write_table(\"Suppliers\", suppliers)\n", + "write_table(\"Stores\", stores)\n", + "write_table(\"Inventory\", inventory)\n", + "write_table(\"DemandHistory\", demand)\n", + "write_table(\"ExternalSignals\", signals)\n", + "write_table(\"ReplenishmentOrders\", orders)\n", + "print(\"All tables written.\")" + ] + }, + { + "cell_type": "markdown", + "id": "8cdea32b", + "metadata": {}, + "source": [ + "## Create + publish the Fabric Data Agent\n", + "\n", + "> **Preview API note:** `fabric-data-agent-sdk` is in preview and method names can change between versions. If a call below fails, check the [SDK reference](https://learn.microsoft.com/fabric/data-science/fabric-data-agent-sdk) and adjust. After publishing, the last cell prints your **Workspace ID** and **Data Agent (artifact) ID** — note both; you'll paste them into the Fabric Data Agent tool in **Challenge 2**.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "88b3cac5", + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "import notebookutils\n", + "from fabric.dataagent.client import create_data_agent\n", + "try:\n", + " from fabric.dataagent.client import FabricDataAgentManagement\n", + "except Exception:\n", + " FabricDataAgentManagement = None\n", + "\n", + "workspace_id = notebookutils.runtime.context[\"currentWorkspaceId\"]\n", + "lakehouse_id = notebookutils.runtime.context.get(\"defaultLakehouseId\")\n", + "print(\"Workspace ID:\", workspace_id)\n", + "print(\"Lakehouse ID:\", lakehouse_id)\n", + "\n", + "AGENT_INSTRUCTIONS = (\n", + " \"You answer questions about Zava retail inventory. Use these tables: \"\n", + " \"Inventory (onHand, reorderPoint, safetyStock per product per location), \"\n", + " \"Products (name, category, unitCost, leadTimeDays, supplierId), \"\n", + " \"Stores (retail stores and warehouses with region/city/state), \"\n", + " \"DemandHistory (weekly units sold per product per store), \"\n", + " \"ExternalSignals (market signals with affectedCategories), \"\n", + " \"Suppliers, and ReplenishmentOrders (past purchase/transfer orders). \"\n", + " \"Always return exact numbers. Flag a product as CRITICAL when onHand is below safetyStock.\"\n", + ")\n", + "\n", + "EXPECTED_TABLES = 7 # the 7 tables written earlier in this notebook\n", + "\n", + "\n", + "def _call_first(obj, names, *args, **kwargs):\n", + " for n in names:\n", + " fn = getattr(obj, n, None)\n", + " if callable(fn):\n", + " return True, fn(*args, **kwargs)\n", + " return False, None\n", + "\n", + "\n", + "def _items(page):\n", + " if isinstance(page, dict):\n", + " return page.get(\"value\") or page.get(\"elements\") or page.get(\"items\") or []\n", + " if isinstance(page, (list, tuple)):\n", + " return list(page)\n", + " return []\n", + "\n", + "\n", + "def _attr(el, *names):\n", + " for n in names:\n", + " if isinstance(el, dict) and n in el:\n", + " return el[n]\n", + " if hasattr(el, n):\n", + " return getattr(el, n)\n", + " return None\n", + "\n", + "\n", + "def _get_elements(ds, root_id):\n", + " \"\"\"Fetch child elements of `root_id` (None = top), following pagination.\"\"\"\n", + " out = []\n", + " token = None\n", + " while True:\n", + " page = None\n", + " for kwargs in ({\"root_id\": root_id, \"continuation_token\": token},\n", + " {\"root_id\": root_id},\n", + " {\"parent_id\": root_id}):\n", + " try:\n", + " page = ds.get_elements(**kwargs); break\n", + " except TypeError:\n", + " continue\n", + " except Exception:\n", + " page = None; break\n", + " if page is None:\n", + " try:\n", + " page = ds.get_elements()\n", + " except Exception:\n", + " break\n", + " out.extend(_items(page))\n", + " token = page.get(\"continuationToken\") if isinstance(page, dict) else None\n", + " if not token:\n", + " break\n", + " return out\n", + "\n", + "\n", + "def _select_all_tables(ds):\n", + " \"\"\"Walk Schemas > Schema > Tables > Table and select every TABLE leaf. Returns the count.\n", + " Matches the EXACT leaf type 'table' (not substring -- 'Tables' is the parent container)\n", + " and descends into any node with sub-elements. update_element is idempotent, so this is\n", + " safe to re-run.\"\"\"\n", + " selected = 0\n", + " stack = [None]\n", + " seen = set()\n", + " while stack:\n", + " root = stack.pop()\n", + " for el in _get_elements(ds, root):\n", + " el_id = _attr(el, \"id\")\n", + " el_type = str(_attr(el, \"type\") or \"\").strip().lower()\n", + " has_sub = bool(_attr(el, \"hasSubElements\"))\n", + " key = str(el_id)\n", + " if el_id is None or key in seen:\n", + " continue\n", + " seen.add(key)\n", + " if el_type == \"table\":\n", + " try:\n", + " ok_sel, _ = _call_first(ds, (\"update_element\",), el_id, is_selected=True)\n", + " if not ok_sel:\n", + " ds.update_element(el_id, {\"isSelected\": True})\n", + " selected += 1\n", + " except Exception as e:\n", + " print(\"update_element failed for\", el_id, \"->\", e)\n", + " elif has_sub:\n", + " stack.append(el_id) # descend into Schemas / Schema / Tables\n", + " return selected\n", + "\n", + "\n", + "# Create the data agent, or re-open it if an earlier run already created it (idempotent).\n", + "try:\n", + " agent = create_data_agent(\"inventory-hack-agent\")\n", + "except Exception:\n", + " agent = FabricDataAgentManagement(\"inventory-hack-agent\")\n", + "\n", + "# Public API: instructions + add the Lakehouse to staging.\n", + "_call_first(agent, (\"update_settings\",), ai_instructions=AGENT_INSTRUCTIONS) \\\n", + " or _call_first(agent, (\"update_configuration\",), instructions=AGENT_INSTRUCTIONS)\n", + "_call_first(agent, (\"add_staging_datasource\",),\n", + " artifact_name_or_id=lakehouse_id, workspace_id_or_name=workspace_id) \\\n", + " or _call_first(agent, (\"add_datasource\",), lakehouse_id, type=\"lakehouse\")\n", + "\n", + "# After add_staging_datasource, the Lakehouse schema syncs ASYNCHRONOUSLY -- on a fresh run\n", + "# the table leaves may not be listed yet (that is why a second run \"fixed\" it). Poll: re-fetch\n", + "# the datasource and re-walk until all tables appear, or ~60s elapses.\n", + "selected = 0\n", + "for attempt in range(12):\n", + " ok, ds_list = _call_first(agent, (\"list_datasources\", \"get_datasources\"))\n", + " ds_list = list(ds_list) if ds_list else []\n", + " if ds_list:\n", + " selected = _select_all_tables(ds_list[0])\n", + " if selected >= EXPECTED_TABLES:\n", + " break\n", + " if attempt == 0:\n", + " print(\"Waiting for the Lakehouse schema to finish syncing...\")\n", + " time.sleep(5)\n", + "\n", + "print(f\"Selected {selected} table(s).\")\n", + "if selected == 0:\n", + " raise RuntimeError(\"No tables were selected (schema may still be syncing). Re-run this \"\n", + " \"cell; if it persists, tick all tables under InventoryLakehouse > dbo \"\n", + " \"in the data agent's Data tab and Publish.\")\n", + "\n", + "# Publish the staged config so the selection goes live on MCP / Foundry.\n", + "_call_first(agent, (\"publish_staging\",),\n", + " description=\"Zava inventory data agent for the Agentic Inventory Planning MicroHack.\") \\\n", + " or _call_first(agent, (\"publish\",))\n", + "print(\"Fabric Data Agent 'inventory-hack-agent' published.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eba55cce", + "metadata": {}, + "outputs": [], + "source": [ + "# Smoke test -- query the PUBLISHED agent through its MCP endpoint.\n", + "# `mcp` is installed by the top cell (all %pip must be up front — it restarts the\n", + "# kernel). We resolve the agent id from the workspace, so this cell is robust even\n", + "# if run on its own, and we never fail the run on a transient MCP hiccup.\n", + "import asyncio, notebookutils\n", + "from fabric.dataagent.client import FabricDataAgentManagement\n", + "from mcp import ClientSession\n", + "from mcp.client.streamable_http import streamablehttp_client\n", + "\n", + "workspace_id = notebookutils.runtime.context[\"currentWorkspaceId\"]\n", + "mgmt = FabricDataAgentManagement(\"inventory-hack-agent\")\n", + "data_agent_id = str(mgmt._client.data_agent_id) # resolved artifact id\n", + "\n", + "mcp_url = (f\"https://api.fabric.microsoft.com/v1/mcp/workspaces/{workspace_id}\"\n", + " f\"/dataagents/{data_agent_id}/agent\")\n", + "QUESTION = (\"How many Leaf Blower X2 units are on hand at the Portland and Seattle \"\n", + " \"stores, and are they below safety stock?\")\n", + "\n", + "async def ask_agent(question):\n", + " token = notebookutils.credentials.getToken(\"https://api.fabric.microsoft.com\")\n", + " headers = {\"Authorization\": f\"Bearer {token}\"}\n", + " async with streamablehttp_client(mcp_url, headers=headers) as (read, write, _):\n", + " async with ClientSession(read, write) as session:\n", + " await session.initialize()\n", + " tool = (await session.list_tools()).tools[0]\n", + " arg = next(iter(tool.inputSchema[\"properties\"]))\n", + " result = await session.call_tool(tool.name, {arg: question})\n", + " return \"\\n\".join(b.text for b in result.content if b.type == \"text\")\n", + "\n", + "# Fabric notebooks run inside an event loop already, so use top-level await instead\n", + "# of asyncio.run() (which errors on a running loop). Never hard-fail the headless\n", + "# run on a smoke-test error — the agent may still be fine; verify in the playground.\n", + "try:\n", + " answer = await ask_agent(QUESTION)\n", + " print(answer)\n", + "except Exception as e:\n", + " print(f\"[smoke-test] agent query failed ({type(e).__name__}: {e}). \"\n", + " \"The agent may still be published fine — verify in the Foundry playground.\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "056ab4dd", + "metadata": {}, + "outputs": [], + "source": [ + "# --- Emit the IDs block: note these for the Fabric Data Agent tool in Challenge 2 ---\n", + "# Self-contained: resolve every id from the runtime + the published agent, so this\n", + "# works on its own and never depends on variables from earlier cells (a %pip restart\n", + "# earlier in the notebook clears them).\n", + "import json, notebookutils\n", + "from fabric.dataagent.client import FabricDataAgentManagement\n", + "\n", + "workspace_id = notebookutils.runtime.context[\"currentWorkspaceId\"]\n", + "lakehouse_id = notebookutils.runtime.context.get(\"defaultLakehouseId\")\n", + "agent_id = str(FabricDataAgentManagement(\"inventory-hack-agent\")._client.data_agent_id)\n", + "\n", + "print(\"=== Your Fabric Data Agent IDs — use these in Challenge 2 ===\")\n", + "print(json.dumps({\n", + " \"fabricWorkspaceId\": workspace_id,\n", + " \"fabricAgentId\": agent_id,\n", + " \"lakehouseId\": lakehouse_id,\n", + "}, indent=2))" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/walkthrough/challenge-01/solution-01.md b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/walkthrough/challenge-01/solution-01.md new file mode 100644 index 000000000..aed06a09b --- /dev/null +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/walkthrough/challenge-01/solution-01.md @@ -0,0 +1,52 @@ +# Challenge 1 — Solution: Get Grounded + +**[Home](../../README.md)** - [Next Solution →](../challenge-02/solution-02.md) + +**Duration:** 45 minutes + +## Goal + +Understand the inventory scenario, provision your own Fabric workspace + Data Agent, and learn the Foundry + Fabric architecture. + +## Solution walkthrough + +### Data model summary + +| Table | Key columns | Used by | +|-------|-------------|---------| +| `Inventory` | productId, storeId, onHand, reorderPoint, safetyStock | Challenges 2, 3 | +| `DemandHistory` | productId, storeId, weekStart, unitsSold | Challenge 2 | +| `ExternalSignals` | signalType, region, headline, score, affectedCategories | Challenge 2 | +| `Products` | productId, name, category, unitCost, leadTimeDays, supplierId | Challenges 3, 4 | +| `Stores` | storeId, name, type, region, city | Challenges 2, 3 | +| `ReplenishmentOrders` | orderId, type, status, approvedBy, items | Challenge 4 | +| `Suppliers` | supplierId, name | Challenge 4 | + +### Foundry portal navigation + +1. Sign in at [ai.azure.com](https://ai.azure.com) with the credentials provided by your facilitator. +2. Select your project from the **Projects** list. +3. In the left navigation: **Models + endpoints** → confirm `gpt-5.4-mini` is listed with status **Succeeded**. +4. **Agents → Playground** → send "Hello" → confirm a response is returned. +5. You attach the **Fabric Data Agent** as a *tool* on each agent you build — the `inventory-hack-agent` connection is created the first time (Challenge 2) via **Tools → Add → Fabric Data Agent**, using the **Workspace ID and Agent ID your setup notebook printed** in Challenge 1. No standalone connection page is needed. + +### Provision your Fabric workspace + Data Agent (Challenge 1 Task 3) + +1. **Create the workspace on your capacity.** [app.fabric.microsoft.com](https://app.fabric.microsoft.com) → **Workspaces → + New workspace** → name `inventory-hack` → expand **Advanced** → **Workspace type: Fabric** → under **Details** select your **`FabricCapacityName`** (the `invcap…` value from your dashboard) → **Apply**. If the capacity isn't listed, resume it in Azure (your Fabric capacity → **Resume**) and confirm you're signed in as your lab user. +2. **Create the Lakehouse.** **+ New item → Lakehouse**, named exactly **`InventoryLakehouse`**. +3. **Import + attach.** Download **`Setup-InventoryDataAgent.ipynb`** from the repo's **`setup/`** folder (or your facilitator's link), then **Import → Notebook → From this computer → Upload** in the workspace toolbar. Open it and, in the **Explorer**, **Add → Existing Lakehouse → `InventoryLakehouse`** as the **default** — if names collide, pick the one whose **Location** is your `inventory-hack` workspace (the write cells fail without a default Lakehouse). +4. **Run all** (~5–10 min). The kernel restarts once after the `%pip` cell — expected. It writes 7 tables and publishes `inventory-hack-agent`. +5. Copy the **Workspace ID** and **Agent ID** the final cell prints — you'll use them in Challenge 2. + +**Common stumbles:** capacity not in the dropdown (paused, or wrong signed-in user) · table-write cells fail (Lakehouse not attached as default) · Data Agent tool missing later in Foundry (capacity paused, or the notebook didn't finish publishing). + +### Grounding questions — suggested answers + +**What would go wrong if the Demand Sensing Agent only looked at web signals?** +It might recommend a reorder that the company already has stock for, wasting budget. The governed Fabric data is the source of truth for what is actually on the shelves. + +**Why does the Replenishment Action Agent need a human approval step?** +Purchase orders have financial and operational consequences. The agent can propose and calculate, but a human should validate the business context (budget constraints, supplier relationships, timing) before committing. + +**What does the Fabric Data Agent abstract away?** +SQL query generation, Fabric authentication, schema discovery, and result formatting. The agent sends a plain English question and receives a plain English answer — no DAX or SQL knowledge needed. diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/walkthrough/challenge-02/solution-02.md b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/walkthrough/challenge-02/solution-02.md new file mode 100644 index 000000000..73ac84ddc --- /dev/null +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/walkthrough/challenge-02/solution-02.md @@ -0,0 +1,37 @@ +# Challenge 2 — Solution: Demand Sensing Agent + +**[← Previous](../challenge-01/solution-01.md)** - [Home](../../README.md) - [Next Solution →](../challenge-03/solution-03.md) + +**Duration:** 60 minutes + +## Goal + +Build a Foundry prompt agent with Web Search + Fabric Data Agent tools to sense a real-world demand change. + +## Solution walkthrough + +### Agent creation + +Navigate to **Agents → + New agent**. If the portal shows a wizard, select **Prompt agent** (not Hosted agent). + +### Common issues + +| Symptom | Cause | Fix | +|---------|-------|-----| +| Fabric tool call returns "unauthorized" | The Fabric Data Agent isn't published, or your F2 capacity is paused | You own this agent — confirm the setup notebook finished **publishing** it (Challenge 1) and that your F2 capacity is **running** (Azure portal → your Fabric capacity → **Resume**) | +| Web Search returns no results | Web Search tool not enabled in the project | Go to **Project settings** and verify Web Search is listed under enabled tools | +| Agent ignores the Fabric tool (trace shows only Web Search, or it replies "I can't access your data") | `gpt-5.4-mini` is a GPT-5 reasoning model and may skip a tool it treats as optional; the portal may also flag the Fabric tool as "not supported" (cosmetic — it still runs via the Responses API) | Keep the **IMPORTANT - tool use** block in the instructions **and set tool choice = required** in the run settings so the Fabric Data Agent tool is always invoked. | + +### Expected agent response + +A well-formed response includes three sections: + +1. **External signals** — 2–3 web search results with URLs, describing the demand trend or event. +2. **Inventory position** — current stock levels for the relevant SKUs, sourced from Fabric. +3. **Assessment** — a clear verdict: adequate / at risk / critically exposed, with reasons. + +### Sample test prompts + +- *"A prolonged heatwave is forecast across the Pacific Northwest. What is our exposure on outdoor power tools like leaf blowers and chainsaws?"* +- *"Search trends show a spike in leaf blower demand. What is our current stock of the Leaf Blower X2 at the Portland and Seattle stores?"* +- *"Early spring is driving garden equipment demand. How are we positioned on garden and lawn products across our stores and warehouses?"* diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/walkthrough/challenge-03/solution-03.md b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/walkthrough/challenge-03/solution-03.md new file mode 100644 index 000000000..73a135d85 --- /dev/null +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/walkthrough/challenge-03/solution-03.md @@ -0,0 +1,50 @@ +# Challenge 3 — Solution: Inventory Optimisation Agent + +**[← Previous](../challenge-02/solution-02.md)** - [Home](../../README.md) - [Next Solution →](../challenge-04/solution-04.md) + +**Duration:** 60 minutes + +## Goal + +Build the Inventory Optimisation Agent and use agent tracing to inspect its reasoning. + +## Solution walkthrough + +### Agent — key configuration point + +This agent should have **only the Fabric Data Agent tool** — no Web Search. If attendees add Web Search, the agent may use it unnecessarily. The instructions say "governed data only" for a reason: optimisation decisions should be traceable to authoritative internal data, not unverified web content. + +### Reading the trace — what to look for + +In **Tracing**, each run shows a tree of spans: + +``` +run +├── model_call (instructions + user message sent to gpt-5.4-mini) +├── tool_call: Fabric Data Agent +│ ├── input: { query: "current stock and reorder point for Leaf Blower X2 (P004) across all locations" } +│ └── output: { ... table of rows ... } +├── tool_call: Fabric Data Agent ← agent may call multiple times +│ ├── input: { query: "average weekly sales for outdoor power tools over the last 8 weeks" } +│ └── output: { ... } +└── model_call (final synthesis → recommendation table) +``` + +### Common issues + +| Symptom | Cause | Fix | +|---------|-------|-----| +| Agent returns "I don't have enough information" | Product/category words don't match Fabric values | Ask by the real product name or category, e.g. "Leaf Blower X2" or "outdoor power tools" | +| Reorder quantity seems wrong | Agent used a different formula | Remind the agent of the formula in a follow-up: "Use the rule: reorder_qty = max(0, 30-day demand - current stock)" | +| Trace not appearing | Tracing may take 30–60 seconds to update | Refresh the Tracing page | + +### Sample recommendation table + +``` +| SKU | Product | Location | Current Stock | Suggested Reorder Qty | Priority | +|------|----------------|-----------|---------------|-----------------------|----------| +| P004 | Leaf Blower X2 | Seattle | 6 | 54 | CRITICAL | +| P004 | Leaf Blower X2 | Portland | 8 | 52 | CRITICAL | +| P005 | Chainsaw 16in | Portland | 5 | 31 | CRITICAL | +| P004 | Leaf Blower X2 | Chicago | 45 | 15 | At Risk | +``` diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/walkthrough/challenge-04/solution-04.md b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/walkthrough/challenge-04/solution-04.md new file mode 100644 index 000000000..b6ed496bc --- /dev/null +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/walkthrough/challenge-04/solution-04.md @@ -0,0 +1,55 @@ +# Challenge 4 — Solution: Replenishment Action Agent + Human-in-the-Loop + +**[← Previous](../challenge-03/solution-03.md)** - [Home](../../README.md) - [Next Solution →](../challenge-05/solution-05.md) + +**Duration:** 75 minutes + +## Goal + +Build the Replenishment Action Agent, implement the human approval loop, and trace the full cycle. + +## Solution walkthrough + +### Simulated submission (no function tool) + +The current **New Foundry** portal has no no-code custom-function tool — the Custom tab offers only OpenAPI, MCP, and A2A connectors, which all need a hosted endpoint. Because the Fabric Data Agent is read-only, the "act" step is **simulated by the agent**: on `YES`, its instructions have it generate a confirmation message (`✅ Purchase order PO-... submitted`). This keeps the lab fully no-code while still demonstrating the approve → act gate. In production you'd swap this for an OpenAPI tool that writes to the `ReplenishmentOrders` table / ERP (see Challenge 4, Part E). + +### The MODIFY flow + +When an attendee replies `MODIFY 1 50`, the agent should: +1. Find line 1 in the current proposal. +2. Update the quantity to 50. +3. Recalculate the line total and the grand total. +4. Re-present the full updated proposal. +5. Ask for approval again. + +If the agent skips the re-presentation, add to the instructions: *"After any MODIFY, always re-present the full updated table before asking for approval again."* + +### Trace — what the full cycle looks like + +``` +run (challenge 3 approval) +├── model_call (format PO proposal from recommendation) +│ └── tool_call: Fabric Data Agent (unit-cost lookup from Products) +├── model_call (present proposal + ask for approval) +│ → user replies: MODIFY 1 50 +├── model_call (update line 1, recalculate, re-present) +│ → user replies: YES +└── model_call (simulated confirmation: "✅ Purchase order PO-... submitted") +``` + +### Where does the human add value? + +The human approval step adds value that an automated pipeline cannot replicate: +- **Business context** — budget constraints, supplier relationships, timing preferences +- **Exception handling** — unusual circumstances the model's instructions don't cover +- **Accountability** — a named human is on record as having approved the expenditure + +This is why the pattern is called "human-in-the-loop" rather than "human-in-the-way" — the human is the decision-maker; the agent is the research and proposal layer. + +### Monitoring in production + +In a production deployment, you would add: +- **Application Insights** integration (built into Foundry) for latency and error tracking +- **Evaluation runs** to catch instruction drift over time +- **Agent versioning** so you can roll back if a model update changes behaviour diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/walkthrough/challenge-05/solution-05.md b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/walkthrough/challenge-05/solution-05.md new file mode 100644 index 000000000..238fd2be1 --- /dev/null +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/walkthrough/challenge-05/solution-05.md @@ -0,0 +1,54 @@ +# Challenge 5 — Solution: Orchestrate the Loop (Stretch) + +**[← Previous](../challenge-04/solution-04.md)** - [Home](../../README.md) + +**Duration:** ~45 minutes (optional) + +## Goal + +Chain the three agents in a Foundry **Workflow** (Sequential pattern + human-in-the-loop), then read the node-by-node trace. + +## Solution walkthrough + +### Why a workflow, not connected agents + +The classic **connected agents** tool (one agent added as another's tool) is **not available** in the New Foundry portal — Microsoft replaced it with **Workflows** for multi-agent orchestration. A workflow is a *deterministic* graph: each agent is a **node**, and edges pass one node's output to the next. That's a better fit than an LLM-driven orchestrator for this hack's fixed sense → plan → approve → act chain, and it keeps everything no-code. + +Wire the three specialists in order and name each node's output so the next node can consume it: +- `demand-sensing-agent` → output `demandAssessment` +- `inventory-optimisation-agent` → input `demandAssessment`, output `reorderTable` +- `replenishment-action-agent` → input `reorderTable`, **Allow multi-turn conversation** ON for the approval gate + +> The visual designer is supported in-portal until **1 December 2026**; after that, export the workflow **YAML** and run it via Microsoft Agent Framework / a hosted agent. The orchestration logic carries over unchanged. + +### What a good run looks like + +The orchestrator should narrate its delegation and stop at the approval gate: + +``` +run (challenge 4 workflow) +├── node: demand-sensing-agent +│ └── tool_call: Fabric Data Agent (stock + velocity) +│ → "Outdoor power tools critically exposed at Portland/Seattle." +├── node: inventory-optimisation-agent +│ └── tool_call: Fabric Data Agent (reorder point + demand) +│ → recommendation table with CRITICAL items +└── node: replenishment-action-agent + ├── model_call (present PO, ask for approval) + │ → user replies: YES + └── model_call (simulated PO confirmation) +``` + +### Common issues + +| Symptom | Fix | +|---|---| +| A node gets empty input | Wire it: set the node's **Input message** to the upstream node's **Save agent output message as** variable. | +| A specialist node uses the wrong model | Each node runs its underlying agent — confirm that agent is on `gpt-5.4-mini` with **tool choice = required** so it always calls the Fabric tool. | +| Approval gate is skipped | Enable **Allow multi-turn conversation** on the `replenishment-action-agent` node, and keep the Challenge 4 instructions that require explicit `YES`. | +| No **Workflows** tab / designer | Preview rollout — use the Part D evaluation alternative instead. | + +### Discussion points + +- **Deterministic workflow vs. three human-driven agents:** the workflow is faster and hands-free, but the human loses the natural checkpoints between steps. The approval gate becomes the *only* control point — so it matters even more. +- **Where would you add guardrails** in a production version? (e.g. a spend cap the orchestrator can't exceed without escalation.) diff --git a/03-Azure/01-04-AI/Readme.md b/03-Azure/01-04-AI/Readme.md index 718a09ce2..074e63e83 100644 --- a/03-Azure/01-04-AI/Readme.md +++ b/03-Azure/01-04-AI/Readme.md @@ -5,6 +5,7 @@ A collection of MicroHacks focused on AI on Azure and the GitHub + Azure agentic ## Available MicroHacks * [**Agentic SDLC with GitHub Copilot & Azure**](01_Agentic_SDLC/Readme.md) — Take a full-stack application through the complete Agentic Software Development Lifecycle (plan → implement → test → review → deploy → operate → maintain) using GitHub Copilot, GitHub Actions, the Azure SRE Agent, and GitHub Agentic Workflows. +* [**Inventory Planning with Microsoft Fabric (low-code)**](02_Inventory_Planning_Fabric/README.md) — Build a closed sense → reason → act inventory-planning loop with **Microsoft Foundry Agent Service** prompt agents (no application code) grounded on a governed **Fabric Data Agent** doing NL2SQL over an inventory Lakehouse, with a human-in-the-loop approval gate. * [**Inventory Planning with AI Agents, Cosmos DB & Hosted Agents (pro-code)**](03_Inventory_Planning_Agentic/README.md) — Build hosted AI agents **in code** (Python) on the **Microsoft Foundry Agent Service** that reason over a governed per-attendee **Azure Cosmos DB** via typed function tools, chained into a sequential planning workflow with a human-in-the-loop approval gate and Foundry tracing. Collaborators wanted :) — see [CONTRIBUTING.md](../../CONTRIBUTING.md) to add your own. \ No newline at end of file