From 2b4fce6a255a65d86a0150349eb1e837f0e98298 Mon Sep 17 00:00:00 2001 From: kovtcharov-amd Date: Tue, 17 Mar 2026 16:35:40 -0700 Subject: [PATCH 01/21] Add GAIA Agents playbook: Build Your First Agent with GAIA (#83) * initial commit of gaia-agents * Improve GAIA playbook docs and GPU detection * fix npu ready, requires an updated amd-gaia package (>v0.16.1), coming soon. * Version instructions --------- Co-authored-by: Kalin Ovtcharov Co-authored-by: Daniel Holanda --- playbooks/dependencies/gaia.md | 59 ++ playbooks/dependencies/registry.json | 7 + playbooks/supplemental/gaia-agents/README.md | 669 +++++++++++++++++- .../assets/hardware_advisor_agent.py | 344 +++++++++ .../supplemental/gaia-agents/platform.md | 17 + .../supplemental/gaia-agents/playbook.json | 10 +- 6 files changed, 1100 insertions(+), 6 deletions(-) create mode 100644 playbooks/dependencies/gaia.md create mode 100644 playbooks/supplemental/gaia-agents/assets/hardware_advisor_agent.py create mode 100644 playbooks/supplemental/gaia-agents/platform.md diff --git a/playbooks/dependencies/gaia.md b/playbooks/dependencies/gaia.md new file mode 100644 index 00000000..ca2cd4c2 --- /dev/null +++ b/playbooks/dependencies/gaia.md @@ -0,0 +1,59 @@ +### GAIA + +GAIA is AMD's open-source framework for building AI agents that run locally on AMD hardware with Ryzen AI acceleration. + +#### Installing GAIA + + + +1. Open PowerShell +2. Create a virtual environment and install GAIA: +```cmd +uv venv .venv +.\.venv\Scripts\Activate.ps1 +uv pip install amd-gaia +``` + + + + + +1. Open a terminal +2. Create a virtual environment and install GAIA: +```bash +uv venv .venv +source .venv/bin/activate +uv pip install amd-gaia +``` + + + +#### Initializing GAIA + +After installation, run `gaia init` to set up Lemonade Server and download models: + +``` +gaia init +``` + +This installs Lemonade Server, downloads the default models, and verifies the setup. + +#### Verifying Installation + +Verify that GAIA v0.16.2 or later is installed: + +``` +gaia --version +``` + +Then run a quick test to confirm GAIA is working: + +``` +gaia chat +``` + +Type a message and press Enter. Type `quit` to exit. + +> **Important**: Make sure Lemonade Server is running before using GAIA. GAIA requires Lemonade Server to be started manually. + +For more information, see the [GAIA documentation](https://amd-gaia.ai). diff --git a/playbooks/dependencies/registry.json b/playbooks/dependencies/registry.json index 83cb1fd7..d16d3248 100644 --- a/playbooks/dependencies/registry.json +++ b/playbooks/dependencies/registry.json @@ -51,6 +51,13 @@ "platforms": ["windows", "linux"], "file": "lemonade.md" }, + "gaia": { + "name": "GAIA", + "description": "AMD framework for building AI agents on local hardware", + "category": "framework", + "platforms": ["windows", "linux"], + "file": "gaia.md" + }, "comfyui-models": { "name": "ComfyUI Models", "description": "ComfyUI models for image generation", diff --git a/playbooks/supplemental/gaia-agents/README.md b/playbooks/supplemental/gaia-agents/README.md index 02bc3a2d..0fa2149b 100644 --- a/playbooks/supplemental/gaia-agents/README.md +++ b/playbooks/supplemental/gaia-agents/README.md @@ -3,6 +3,673 @@ > This playbook uses special tags that GitHub cannot render. Please visit [amd.com/playbooks](https://amd.com/playbooks) to correctly preview this content. +## Overview + # Getting Started Creating Agents with GAIA - +GAIA agents are AI assistants that use a local LLM to reason and call tools you define — like chatbots that can take action. They run **100% locally** with no cloud APIs, no data leaving your machine, and no API keys required. + +In this playbook, you'll build a Hardware Advisor Agent that detects your system's RAM, GPU, and NPU, queries the local model catalog, and recommends which LLMs your machine can run. It's a practical introduction to the GAIA Agent SDK that produces something immediately useful. + +## What You'll Learn + +- How to create a GAIA agent with custom tools +- Using the LemonadeClient SDK to query system info and model catalogs +- Platform-specific GPU/NPU detection (Windows PowerShell and Linux lspci) +- Memory-based model sizing using the 70% rule +- Building an interactive CLI for natural language hardware queries + +## Installing Dependencies + + + +## Getting Started + +Get the finished agent running first so you can see what you're building, then we'll walk through the code step by step. + +### 1. Run the Pre-Built Example + +This playbook includes the complete [hardware_advisor_agent.py](assets/hardware_advisor_agent.py). Run it to see the finished agent in action: + +``` +uv run hardware_advisor_agent.py +``` + +**Try asking:** "What size LLM can I run?" + +**Expected output:** + +``` +============================================================ +Hardware Advisor Agent +============================================================ + +Hi! I can help you figure out what size LLM your system can run. + +Agent ready! + +You: What size LLM can I run? + +Agent: Great news! With 32 GB RAM and a 24 GB GPU, you can run: +- 30B parameter models (like Qwen3-Coder-30B) +- Most 7B-14B models comfortably +- NPU acceleration available for smaller models +``` + +Now let's build this from scratch. + +## Core Concepts + +### Architecture + +The Hardware Advisor Agent combines three components: + +- **LemonadeClient SDK** — System info and model catalog APIs +- **Platform-specific detection** — Windows PowerShell / Linux lspci for GPU info +- **Memory calculations** — 70% rule for safe model sizing + +The data flows through these in sequence: user query → agent selects a tool → tool calls LemonadeClient + OS detection → agent synthesizes the results into a recommendation. + +### LemonadeClient SDK + +The LemonadeClient provides a unified API for system detection, NPU/GPU availability, and model catalog queries. + +**Import and initialize:** + +```python +from gaia.llm.lemonade_client import LemonadeClient + +client = LemonadeClient(keep_alive=True) +``` + +**`get_system_info()`** — Returns OS, CPU, RAM, and device availability: + +```python +info = client.get_system_info() +``` + + + +```python +# Returns: +{ + "OS Version": "Windows 11 Pro", + "Processor": "AMD Ryzen 9 7950X", + "Physical Memory": "32.0 GB", + "devices": { + "cpu": {"name": "...", "available": True}, + "amd_igpu": {"name": "...", "memory": 8192, "available": True}, + "amd_npu": {"name": "Ryzen AI NPU", "available": True} + } +} +``` + + + + + +```python +# Returns: +{ + "OS Version": "Ubuntu 24.04 LTS", + "Processor": "AMD Ryzen 9 7950X", + "Physical Memory": "32.0 GB", + "devices": { + "cpu": {"name": "...", "available": True}, + "amd_igpu": {"name": "...", "memory": 8192, "available": True}, + "amd_npu": {"name": "Not detected", "available": False} + } +} +``` + + + +**`list_models(show_all=True)`** — Returns the full model catalog: + +```python +response = client.list_models(show_all=True) + +# Returns: +{ + "data": [ + { + "id": "Qwen3-0.6B-GGUF", + "name": "Qwen3 0.6B", + "downloaded": True, + "labels": ["hot", "cpu", "small"] + } + ] +} +``` + +**`get_model_info(model_id)`** — Returns size estimates for a specific model: + +```python +model_info = client.get_model_info("Qwen3-Coder-30B-A3B-Instruct-GGUF") + +# Returns: +{ + "id": "Qwen3-Coder-30B-A3B-Instruct-GGUF", + "name": "Qwen3 Coder 30B", + "size_gb": 18.5, + "downloaded": False +} +``` + +### Platform-Specific GPU Detection + +The agent uses OS-native commands rather than PyTorch for GPU detection. This works without GPU drivers installed, detects all GPUs (not just CUDA-capable ones), and avoids heavy library imports. + + + +On Windows, the agent uses PowerShell to query WMI: + +```python +ps_command = ( + "Get-WmiObject Win32_VideoController | " + "Select-Object Name,AdapterRAM | " + "ConvertTo-Csv -NoTypeInformation" +) +result = subprocess.run( + ["powershell", "-Command", ps_command], + capture_output=True, text=True, timeout=5 +) +# Parse CSV output for GPU name and VRAM +``` + + + + + +On Linux, the agent uses lspci: + +```python +result = subprocess.run( + ["lspci"], capture_output=True, text=True, timeout=5 +) +# Parse output for "VGA compatible controller" lines +# Note: Memory not available via lspci +``` + + + +### The 70% Memory Rule + +> **Rule:** Model size should be less than 70% of available RAM to leave 30% overhead for inference operations (KV cache, batch processing buffers, runtime memory spikes). + +``` +System: 32 GB RAM +Max safe model size: 32 x 0.7 = 22.4 GB +30B model (~18.5 GB): Fits safely +70B model (~42 GB): Too large +``` + +## Building the Agent Step by Step + +You'll create **one file** called `hardware_advisor.py` and progressively add features. Each step builds on the previous one. + +### Step 1: Agent Skeleton + +Start with a minimal agent structure — just the class and a basic system prompt. The agent has no tools yet. + +```python +from gaia import Agent +from gaia.llm.lemonade_client import LemonadeClient + + +class HardwareAdvisorAgent(Agent): + """Agent that advises on LLM capabilities based on your hardware.""" + + def __init__(self, **kwargs): + self.client = LemonadeClient(keep_alive=True) + super().__init__(**kwargs) + + def _get_system_prompt(self) -> str: + return "You are a hardware advisor for running local LLMs on AMD systems." + + def _register_tools(self): + # Tools will be added in the next steps + pass + + +if __name__ == "__main__": + agent = HardwareAdvisorAgent() + print("Agent created successfully!") +``` + +Run it to verify: + +``` +uv run hardware_advisor.py +``` + +Expected output: + +``` +Agent created successfully! +``` + +--- + +### Step 2: GPU and Hardware Detection + +Add the `_get_gpu_info()` helper method and the `get_hardware_info()` tool. This makes the agent interactive — you can now query it about system specs. + +**Update the imports** at the top of the file: + +```python +from typing import Any, Dict + +from gaia import Agent, tool +from gaia.llm.lemonade_client import LemonadeClient +``` + +**Add the `_get_gpu_info()` helper** after the `_get_system_prompt()` method: + +```python +def _get_gpu_info(self) -> Dict[str, Any]: + """Detect GPU using OS-native commands.""" + import platform + import subprocess + + system = platform.system() + + try: + if system == "Windows": + ps_command = ( + "Get-WmiObject Win32_VideoController | " + "Select-Object Name,AdapterRAM | " + "ConvertTo-Csv -NoTypeInformation" + ) + result = subprocess.run( + ["powershell", "-Command", ps_command], + capture_output=True, + text=True, + timeout=5, + ) + if result.returncode == 0: + lines = [ + l.strip() + for l in result.stdout.strip().split("\n") + if l.strip() + ] + # Skip virtual/remote adapters that aren't real GPUs + skip_keywords = [ + "microsoft remote display", + "microsoft basic display", + "remote desktop", + ] + # Collect all valid GPUs and pick the one with the most VRAM + candidates = [] + for line in lines[1:]: # Skip header + line = line.replace('"', "") + parts = line.split(",") + if len(parts) >= 2: + try: + name = parts[0].strip() + adapter_ram = ( + int(parts[1]) if parts[1].strip().isdigit() else 0 + ) + if name and len(name) > 0: + if any(k in name.lower() for k in skip_keywords): + continue + candidates.append({ + "name": name, + "memory_mb": ( + adapter_ram // (1024 * 1024) + if adapter_ram > 0 + else 0 + ), + }) + except (ValueError, IndexError): + continue + if candidates: + return max(candidates, key=lambda g: g["memory_mb"]) + + elif system == "Linux": + result = subprocess.run( + ["lspci"], capture_output=True, text=True, timeout=5 + ) + if result.returncode == 0: + candidates = [] + for line in result.stdout.split("\n"): + if "VGA compatible controller" in line: + parts = line.split(":", 2) + if len(parts) >= 3: + candidates.append({ + "name": parts[2].strip(), + "memory_mb": 0, + }) + if candidates: + # Prefer AMD GPUs if present, otherwise return first + amd_gpus = [g for g in candidates if "amd" in g["name"].lower() or "radeon" in g["name"].lower()] + return amd_gpus[0] if amd_gpus else candidates[0] + + except Exception as e: + print(f"GPU detection error: {e}") + + return {"name": "Not detected", "memory_mb": 0} +``` + +**Replace the `_register_tools()` method** with the `get_hardware_info` tool: + +```python +def _register_tools(self): + client = self.client + agent = self + + @tool(atomic=True) + def get_hardware_info() -> Dict[str, Any]: + """Get detailed system hardware information including RAM, GPU, and NPU.""" + try: + info = client.get_system_info() + + # Parse RAM (format: "32.0 GB") + ram_str = info.get("Physical Memory", "0 GB") + ram_gb = float(ram_str.split()[0]) if ram_str else 0 + + # Detect GPU + gpu_info = agent._get_gpu_info() + gpu_name = gpu_info.get("name", "Not detected") + gpu_available = gpu_name != "Not detected" + gpu_memory_mb = gpu_info.get("memory_mb", 0) + gpu_memory_gb = ( + round(gpu_memory_mb / 1024, 2) if gpu_memory_mb > 0 else 0 + ) + + # Get NPU information from Lemonade + devices = info.get("devices", {}) + npu_info = devices.get("amd_npu", {}) + npu_available = npu_info.get("available", False) + npu_name = ( + npu_info.get("name", "Not detected") + if npu_available + else "Not detected" + ) + + return { + "success": True, + "os": info.get("OS Version", "Unknown"), + "processor": info.get("Processor", "Unknown"), + "ram_gb": ram_gb, + "amd_igpu": { + "name": gpu_name, + "memory_mb": gpu_memory_mb, + "memory_gb": gpu_memory_gb, + "available": gpu_available, + }, + "amd_npu": {"name": npu_name, "available": npu_available}, + } + except Exception as e: + return { + "success": False, + "error": str(e), + "message": "Failed to get hardware information from Lemonade Server", + } +``` + +**Update the `__main__` block** to enable interactive testing: + +```python +if __name__ == "__main__": + agent = HardwareAdvisorAgent() + print("Hardware Advisor Agent (Ctrl+C to exit)") + print("Try: 'Show me my system specs'\n") + + while True: + try: + query = input("You: ").strip() + if query: + agent.process_query(query) + print() + except KeyboardInterrupt: + print("\nGoodbye!") + break +``` + +Run and try asking "Show me my system specs": + +``` +uv run hardware_advisor.py +``` + +**Example output:** + +``` +You: Show me my system specs + +Agent: Your system has excellent specs for running LLMs locally! +- 32 GB RAM +- AMD Radeon RX 7900 XTX with 24 GB VRAM +- Ryzen AI NPU for accelerated inference +``` + +--- + +### Step 3: Model Catalog + +Add the `list_available_models()` tool inside `_register_tools()`, after the `get_hardware_info` function. Now the agent can tell you what models are available. + +```python + @tool(atomic=True) + def list_available_models() -> Dict[str, Any]: + """List all models available in the catalog with their sizes and download status.""" + try: + response = client.list_models(show_all=True) + models_data = response.get("data", []) + + enriched_models = [] + for model in models_data: + model_id = model.get("id", "") + model_info = client.get_model_info(model_id) + size_gb = model_info.get("size_gb", 0) + + enriched_models.append( + { + "id": model_id, + "name": model.get("name", model_id), + "size_gb": size_gb, + "downloaded": model.get("downloaded", False), + "labels": model.get("labels", []), + } + ) + + enriched_models.sort(key=lambda m: m["size_gb"], reverse=True) + + return { + "success": True, + "models": enriched_models, + "count": len(enriched_models), + "message": f"Found {len(enriched_models)} models in catalog", + } + except Exception as e: + return { + "success": False, + "error": str(e), + "message": "Failed to fetch models from Lemonade Server", + } +``` + +Run and try asking "What models are available?": + +``` +uv run hardware_advisor.py +``` + +**Example output:** + +``` +You: What models are available? + +Agent: I found 15 models in the catalog: +- Qwen3-Coder-30B (18.5 GB) [hot, coding] - Not downloaded +- Llama-3.1-8B (4.7 GB) [general] - Downloaded +- Qwen3-0.6B (0.4 GB) [hot, cpu, small] - Downloaded +``` + +--- + +### Step 4: Smart Recommendations + +Add the `recommend_models()` tool inside `_register_tools()`, after `list_available_models`. The agent can now calculate which models fit in your system's memory using the 70% rule. + +```python + @tool(atomic=True) + def recommend_models(ram_gb: float, gpu_memory_mb: int = 0) -> Dict[str, Any]: + """Recommend models based on available system memory. + + Args: + ram_gb: Available system RAM in GB + gpu_memory_mb: Available GPU memory in MB (0 if no GPU) + + Returns: + Dictionary with model recommendations that fit in available memory + """ + try: + models_result = list_available_models() + if not models_result.get("success"): + return models_result + + all_models = models_result.get("models", []) + + # 70% rule: leave 30% overhead for inference + max_model_size_gb = ram_gb * 0.7 + + fitting_models = [ + model + for model in all_models + if model["size_gb"] <= max_model_size_gb and model["size_gb"] > 0 + ] + + for model in fitting_models: + model["estimated_runtime_gb"] = round(model["size_gb"] * 1.3, 2) + model["fits_in_ram"] = model["estimated_runtime_gb"] <= ram_gb + + if gpu_memory_mb > 0: + gpu_memory_gb = gpu_memory_mb / 1024 + model["fits_in_gpu"] = model["size_gb"] <= (gpu_memory_gb * 0.9) + + fitting_models.sort(key=lambda m: m["size_gb"], reverse=True) + + return { + "success": True, + "recommendations": fitting_models, + "total_fitting_models": len(fitting_models), + "constraints": { + "available_ram_gb": ram_gb, + "available_gpu_mb": gpu_memory_mb, + "max_model_size_gb": round(max_model_size_gb, 2), + "safety_margin_percent": 30, + }, + } + except Exception as e: + return { + "success": False, + "error": str(e), + "message": "Failed to generate model recommendations", + } +``` + +Run and try asking "What size LLM can I run?": + +``` +uv run hardware_advisor.py +``` + +**Example output:** + +``` +You: What size LLM can I run? + +Agent: With 32 GB RAM and 24 GB GPU, you can safely run models up to 22.4 GB! + +Top recommendations: +1. Qwen3-Coder-30B (18.5 GB) - Fits in RAM and GPU +2. Llama-3.1-8B (4.7 GB) - Fits in RAM and GPU +``` + +--- + +### Step 5: Production CLI + +Replace the simple `__main__` block with a polished interactive CLI. This adds a banner, quit commands, and better error handling. + +**Replace the entire `if __name__ == "__main__":` block** with: + +```python +def main(): + """Run the Hardware Advisor Agent interactively.""" + print("=" * 60) + print("Hardware Advisor Agent") + print("=" * 60) + print("\nHi! I can help you figure out what size LLM your system can run.") + print("\nTry asking:") + print(" - 'What size LLM can I run?'") + print(" - 'Show me my system specs'") + print(" - 'What models are available?'") + print(" - 'Can I run a 30B model?'") + print("\nType 'quit', 'exit', or 'q' to stop.\n") + + try: + agent = HardwareAdvisorAgent() + print("Agent ready!\n") + except Exception as e: + print(f"Error initializing agent: {e}") + print("\nMake sure Lemonade Server is running before using GAIA.") + return + + while True: + try: + user_input = input("You: ").strip() + + if not user_input: + continue + + if user_input.lower() in ("quit", "exit", "q"): + print("Goodbye!") + break + + agent.process_query(user_input) + print() + + except KeyboardInterrupt: + print("\nGoodbye!") + break + except Exception as e: + print(f"\nError: {e}\n") + + +if __name__ == "__main__": + main() +``` + +--- + +### Final Verification + +Your `hardware_advisor.py` should now have all of these components: + +- [x] Imports: `from typing import Any, Dict` and `from gaia import Agent, tool` +- [x] `HardwareAdvisorAgent` class with `__init__` and system prompt +- [x] `_get_gpu_info()` helper (Windows PowerShell + Linux lspci) +- [x] `get_hardware_info()` tool with GPU, NPU, and OS fields +- [x] `list_available_models()` tool with labels and size enrichment +- [x] `recommend_models()` tool with 70% rule, fits_in_ram, fits_in_gpu +- [x] `main()` function with interactive CLI + +**Test these queries to confirm everything works:** + +- "What size LLM can I run?" +- "Show me my system specs" +- "What models are available?" +- "Can I run a 30B model?" + +> **Tip**: The complete implementation is available at [hardware_advisor_agent.py](assets/hardware_advisor_agent.py). + +## Next Steps + +- **Explore LemonadeClient APIs** — Discover more system and model management capabilities in the [LemonadeClient SDK documentation](https://amd-gaia.ai/sdk/lemonade-client) +- **Add voice interaction** — Integrate Whisper ASR and Kokoro TTS to let users ask hardware questions by speaking. See the [Talk guide](https://amd-gaia.ai/guides/talk) +- **Add MCP support** — Expose the hardware advisor as an MCP server so other tools can query it. See the [MCP guide](https://amd-gaia.ai/sdk/infrastructure/mcp) +- **Extend the recommendation engine** — Factor in GPU VRAM for offloading layers, or add benchmarking to estimate tokens-per-second +- **Build a multi-agent system** — Combine the hardware advisor with a code agent or chat agent using the [Routing Agent](https://amd-gaia.ai/guides/routing) diff --git a/playbooks/supplemental/gaia-agents/assets/hardware_advisor_agent.py b/playbooks/supplemental/gaia-agents/assets/hardware_advisor_agent.py new file mode 100644 index 00000000..a6bdfc21 --- /dev/null +++ b/playbooks/supplemental/gaia-agents/assets/hardware_advisor_agent.py @@ -0,0 +1,344 @@ +# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. +# SPDX-License-Identifier: MIT + +""" +Hardware Advisor Agent Example + +An interactive agent that helps you determine what size LLM your system can run +based on actual hardware capabilities detected via Lemonade Server. + +Usage: + python examples/hardware_advisor_agent.py + +This will start an interactive session where you can ask the agent to: +- Check your system hardware specifications +- Recommend models based on your RAM and GPU +- List available models in the catalog +- Explain what you can run locally +""" + +from typing import Any, Dict + +from gaia import Agent, tool +from gaia.llm.lemonade_client import LemonadeClient + + +class HardwareAdvisorAgent(Agent): + """Agent that advises on LLM capabilities based on your hardware.""" + + def __init__(self, **kwargs): + self.client = LemonadeClient(keep_alive=True) + super().__init__(**kwargs) + self.max_steps = 50 + + def _get_system_prompt(self) -> str: + return """You are a hardware advisor for running local LLMs on AMD systems. + +Use the available tools to check system hardware and recommend models. +When users ask about LLM capabilities, always check their actual hardware first. +Be helpful and explain your recommendations in plain language. + +Available capabilities: +- Check system hardware (RAM, GPU, NPU) via get_hardware_info +- List available models from Lemonade Server via list_available_models +- Recommend models based on hardware specs via recommend_models + +Always use tools to get real data - never guess specifications.""" + + def _get_gpu_info(self) -> Dict[str, Any]: + """Detect GPU using OS-native commands.""" + import platform + import subprocess + + system = platform.system() + + try: + if system == "Windows": + # Use PowerShell Get-WmiObject (wmic is deprecated on Windows 11) + ps_command = ( + "Get-WmiObject Win32_VideoController | " + "Select-Object Name,AdapterRAM | " + "ConvertTo-Csv -NoTypeInformation" + ) + result = subprocess.run( + ["powershell", "-Command", ps_command], + capture_output=True, + text=True, + timeout=5, + ) + if result.returncode == 0: + lines = [ + l.strip() + for l in result.stdout.strip().split("\n") + if l.strip() + ] + # Skip virtual/remote adapters that aren't real GPUs + skip_keywords = [ + "microsoft remote display", + "microsoft basic display", + "remote desktop", + ] + # CSV format: "Name","AdapterRAM" + # Collect all valid GPUs and pick the one with the most VRAM + candidates = [] + for line in lines[1:]: # Skip header + # Remove quotes and split + line = line.replace('"', "") + parts = line.split(",") + if len(parts) >= 2: + try: + name = parts[0].strip() + adapter_ram = ( + int(parts[1]) if parts[1].strip().isdigit() else 0 + ) + if name and len(name) > 0: + if any(k in name.lower() for k in skip_keywords): + continue + candidates.append({ + "name": name, + "memory_mb": ( + adapter_ram // (1024 * 1024) + if adapter_ram > 0 + else 0 + ), + }) + except (ValueError, IndexError): + continue + if candidates: + # Return the GPU with the most VRAM + return max(candidates, key=lambda g: g["memory_mb"]) + + elif system == "Linux": + # Use lspci to find VGA devices + result = subprocess.run( + ["lspci"], capture_output=True, text=True, timeout=5 + ) + if result.returncode == 0: + candidates = [] + for line in result.stdout.split("\n"): + if "VGA compatible controller" in line: + # Extract GPU name after the colon + parts = line.split(":", 2) + if len(parts) >= 3: + candidates.append({ + "name": parts[2].strip(), + "memory_mb": 0, # Memory not available via lspci + }) + if candidates: + # Prefer AMD GPUs if present, otherwise return first + amd_gpus = [g for g in candidates if "amd" in g["name"].lower() or "radeon" in g["name"].lower()] + return amd_gpus[0] if amd_gpus else candidates[0] + + except Exception as e: + # Debug output + print(f"GPU detection error: {e}") + + return {"name": "Not detected", "memory_mb": 0} + + def _register_tools(self): + client = self.client + agent = self + + @tool(atomic=True) + def get_hardware_info() -> Dict[str, Any]: + """Get detailed system hardware information including RAM, GPU, and NPU.""" + try: + # Use Lemonade Server's system info API for basic info + info = client.get_system_info() + + # Parse RAM (format: "32.0 GB") + ram_str = info.get("Physical Memory", "0 GB") + ram_gb = float(ram_str.split()[0]) if ram_str else 0 + + # Detect GPU + gpu_info = agent._get_gpu_info() + gpu_name = gpu_info.get("name", "Not detected") + gpu_available = gpu_name != "Not detected" + gpu_memory_mb = gpu_info.get("memory_mb", 0) + gpu_memory_gb = ( + round(gpu_memory_mb / 1024, 2) if gpu_memory_mb > 0 else 0 + ) + + # Get NPU information from Lemonade + devices = info.get("devices", {}) + npu_info = devices.get("amd_npu", {}) + npu_available = npu_info.get("available", False) + npu_name = ( + npu_info.get("name", "Not detected") + if npu_available + else "Not detected" + ) + + return { + "success": True, + "os": info.get("OS Version", "Unknown"), + "processor": info.get("Processor", "Unknown"), + "ram_gb": ram_gb, + "amd_igpu": { + "name": gpu_name, + "memory_mb": gpu_memory_mb, + "memory_gb": gpu_memory_gb, + "available": gpu_available, + }, + "amd_npu": {"name": npu_name, "available": npu_available}, + } + except Exception as e: + return { + "success": False, + "error": str(e), + "message": "Failed to get hardware information from Lemonade Server", + } + + @tool(atomic=True) + def list_available_models() -> Dict[str, Any]: + """List all models available in the catalog with their sizes and download status.""" + try: + # Fetch model catalog from Lemonade Server + response = client.list_models(show_all=True) + models_data = response.get("data", []) + + # Enrich each model with size information + enriched_models = [] + for model in models_data: + model_id = model.get("id", "") + + # Get size estimate for this model + model_info = client.get_model_info(model_id) + size_gb = model_info.get("size_gb", 0) + + enriched_models.append( + { + "id": model_id, + "name": model.get("name", model_id), + "size_gb": size_gb, + "downloaded": model.get("downloaded", False), + "labels": model.get("labels", []), + } + ) + + # Sort by size (largest first) + enriched_models.sort(key=lambda m: m["size_gb"], reverse=True) + + return { + "success": True, + "models": enriched_models, + "count": len(enriched_models), + "message": f"Found {len(enriched_models)} models in catalog", + } + except Exception as e: + return { + "success": False, + "error": str(e), + "message": "Failed to fetch models from Lemonade Server", + } + + @tool(atomic=True) + def recommend_models(ram_gb: float, gpu_memory_mb: int = 0) -> Dict[str, Any]: + """Recommend models based on available system memory. + + Args: + ram_gb: Available system RAM in GB + gpu_memory_mb: Available GPU memory in MB (0 if no GPU) + + Returns: + Dictionary with model recommendations that fit in available memory + """ + try: + # Get all available models + models_result = list_available_models() + if not models_result.get("success"): + return models_result # Propagate error + + all_models = models_result.get("models", []) + + # Calculate maximum safe model size + # Rule: Model size should be < 70% of available RAM (30% overhead for inference) + max_model_size_gb = ram_gb * 0.7 + + # Filter models that fit in memory + fitting_models = [ + model + for model in all_models + if model["size_gb"] <= max_model_size_gb and model["size_gb"] > 0 + ] + + # Add recommendation metadata + for model in fitting_models: + # Estimate actual runtime memory needed (model size + ~30% overhead) + model["estimated_runtime_gb"] = round(model["size_gb"] * 1.3, 2) + model["fits_in_ram"] = model["estimated_runtime_gb"] <= ram_gb + + # Check GPU fit if GPU available + if gpu_memory_mb > 0: + gpu_memory_gb = gpu_memory_mb / 1024 + model["fits_in_gpu"] = model["size_gb"] <= (gpu_memory_gb * 0.9) + + # Sort by size (largest = most capable) + fitting_models.sort(key=lambda m: m["size_gb"], reverse=True) + + return { + "success": True, + "recommendations": fitting_models, + "total_fitting_models": len(fitting_models), + "constraints": { + "available_ram_gb": ram_gb, + "available_gpu_mb": gpu_memory_mb, + "max_model_size_gb": round(max_model_size_gb, 2), + "safety_margin_percent": 30, + }, + } + except Exception as e: + return { + "success": False, + "error": str(e), + "message": "Failed to generate model recommendations", + } + + +def main(): + """Run the Hardware Advisor Agent interactively.""" + print("=" * 60) + print("Hardware Advisor Agent") + print("=" * 60) + print("\nHi! I can help you figure out what size LLM your system can run.") + print("\nTry asking:") + print(" - 'What size LLM can I run?'") + print(" - 'Show me my system specs'") + print(" - 'What models are available?'") + print(" - 'Can I run a 30B model?'") + print("\nType 'quit', 'exit', or 'q' to stop.\n") + + # Create agent (uses local Lemonade server by default) + try: + agent = HardwareAdvisorAgent() + print("Agent ready!\n") + except Exception as e: + print(f"Error initializing agent: {e}") + print("\nMake sure Lemonade Server is running before using GAIA.") + return + + # Interactive loop + while True: + try: + user_input = input("You: ").strip() + + if not user_input: + continue + + if user_input.lower() in ("quit", "exit", "q"): + print("Goodbye!") + break + + # Process the query (agent prints the output) + agent.process_query(user_input) + print() # Add spacing + + except KeyboardInterrupt: + print("\nGoodbye!") + break + except Exception as e: + print(f"\nError: {e}\n") + + +if __name__ == "__main__": + main() diff --git a/playbooks/supplemental/gaia-agents/platform.md b/playbooks/supplemental/gaia-agents/platform.md new file mode 100644 index 00000000..ad024902 --- /dev/null +++ b/playbooks/supplemental/gaia-agents/platform.md @@ -0,0 +1,17 @@ +# Platform Configuration + +This document describes the expected platform configurations for running this playbook. + +## Required Apps/Frameworks + +### Windows/Linux + +GAIA should be pre-installed using the instructions provided in [GAIA Installation Guide](../../dependencies/gaia.md). + +Lemonade Server should be pre-installed using the instructions provided in [Lemonade Installation Guide](../../dependencies/lemonade.md). + +## Required Models + +### Windows/Linux + +The Hardware Advisor Agent uses **Qwen3-Coder-30B** for agent reasoning. This model is downloaded automatically during `gaia init`. No manual model downloads are required. diff --git a/playbooks/supplemental/gaia-agents/playbook.json b/playbooks/supplemental/gaia-agents/playbook.json index 32fbb763..cb852b9b 100644 --- a/playbooks/supplemental/gaia-agents/playbook.json +++ b/playbooks/supplemental/gaia-agents/playbook.json @@ -1,9 +1,9 @@ { "id": "gaia-agents", - "title": "Getting Started Creating Agents with GAIA", - "description": "Build and deploy AI agents using the GAIA framework with llama.cpp on your STX Halo™", - "time": 60, - "platforms": ["linux", "windows"], + "title": "Building Your First Agent with GAIA", + "description": "Build a 100% local AI agent — no cloud APIs needed. Use the GAIA SDK to create a hardware advisor on your STX Halo", + "time": 20, + "platforms": ["windows", "linux"], "tested_platforms": { "linux": [], "windows": [] @@ -13,5 +13,5 @@ "isFeatured": false, "developed": false, "published": true, - "tags": ["gaia", "agents", "llamacpp", "llm"] + "tags": ["gaia", "agent-sdk", "agent", "hardware", "lemonade"] } From b38a37ad0bf4426e686c69e20dd71ec3e5922cbd Mon Sep 17 00:00:00 2001 From: Daniel Holanda Date: Wed, 18 Mar 2026 10:45:23 -0700 Subject: [PATCH 02/21] Set gaia as developed (#150) --- README.md | 2 +- playbooks/supplemental/gaia-agents/playbook.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 457e562e..d7d342bf 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ This is AMD's official repository of playbooks for AMD developer platforms. Each | **Local Computer Vision with Ryzen AI NPU** | Build local perception capabilities using CVML SDK on RyzenAI and ROCm | | **Clustering Two Devices with llama.cpp RPC** | Distributed inference using RPC server across two AMD devices with llama.cpp | | **Getting Started with Ollama** | Install Ollama and run LLMs locally from the terminal, desktop app, or REST API | -| **Getting Started Creating Agents with GAIA** | Build and deploy AI agents using the GAIA framework `Coming Soon` | +| **Getting Started Creating Agents with GAIA** | Build and deploy AI agents using the GAIA framework | | **Custom GPU Kernels with PyTorch ROCm** | Write and optimize custom GPU kernels using PyTorch and ROCm `Coming Soon` | | **Optimized Fine-tuning with Unsloth QLoRA** | Memory-efficient QLoRA fine-tuning with Unsloth `Coming Soon` | | **Quick Start on vLLM** | Run inference and serving using vLLM `Coming Soon` | diff --git a/playbooks/supplemental/gaia-agents/playbook.json b/playbooks/supplemental/gaia-agents/playbook.json index cb852b9b..f58874f3 100644 --- a/playbooks/supplemental/gaia-agents/playbook.json +++ b/playbooks/supplemental/gaia-agents/playbook.json @@ -11,7 +11,7 @@ "difficulty": "intermediate", "isNew": false, "isFeatured": false, - "developed": false, + "developed": true, "published": true, "tags": ["gaia", "agent-sdk", "agent", "hardware", "lemonade"] } From 6a0260eead753757b784442a6733c370140d1169 Mon Sep 17 00:00:00 2001 From: Sreeram Date: Wed, 18 Mar 2026 14:54:59 -0700 Subject: [PATCH 03/21] Comfyui Image Gen tests (Windows / Linux) (#153) * Added tests for Windows, and the Z-Image workflow JSON * Windows Desktop Installer, Linux git * Added comfyui-sync-requirements-windows test * Installing comfyui-frontend-package and verifying installation * Updating models copy into AppData folder * Updating the rocm-smi test for Linux * Updating workflows to python 3.12 * Revert "Updating workflows to python 3.12" This reverts commit 18b54805afec0adc0e016d174c09549ec8d651ec. * Ensure Python can find ROCm shared libraries * Python 3.12 and pip install requests * Updating all workflows to use Python 3.12 * Update playbooks.json with required_platforms --- .github/workflows/fetch-github-issues.yml | 2 +- .github/workflows/test-playbooks.yml | 2 +- .github/workflows/validate-playbooks.yml | 2 +- playbooks/core/comfyui-image-gen/README.md | 412 +++++++++++++++++- .../assets/image_z_image_turbo.json | 140 ++++++ .../core/comfyui-image-gen/playbook.json | 6 +- playbooks/dependencies/driver.md | 16 + 7 files changed, 572 insertions(+), 8 deletions(-) create mode 100644 playbooks/core/comfyui-image-gen/assets/image_z_image_turbo.json diff --git a/.github/workflows/fetch-github-issues.yml b/.github/workflows/fetch-github-issues.yml index d3ae5d87..cc427acb 100644 --- a/.github/workflows/fetch-github-issues.yml +++ b/.github/workflows/fetch-github-issues.yml @@ -28,7 +28,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '3.12' - name: Fetch GitHub issues env: diff --git a/.github/workflows/test-playbooks.yml b/.github/workflows/test-playbooks.yml index 697bc160..bf29dee0 100644 --- a/.github/workflows/test-playbooks.yml +++ b/.github/workflows/test-playbooks.yml @@ -129,7 +129,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '3.12' - name: Install test dependencies run: | diff --git a/.github/workflows/validate-playbooks.yml b/.github/workflows/validate-playbooks.yml index 8541a7bb..f0b1d6b0 100644 --- a/.github/workflows/validate-playbooks.yml +++ b/.github/workflows/validate-playbooks.yml @@ -16,7 +16,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '3.12' - name: Validate playbooks run: python .github/scripts/validate_playbooks.py diff --git a/playbooks/core/comfyui-image-gen/README.md b/playbooks/core/comfyui-image-gen/README.md index e4b5d9c2..b2dc2eb3 100644 --- a/playbooks/core/comfyui-image-gen/README.md +++ b/playbooks/core/comfyui-image-gen/README.md @@ -20,18 +20,252 @@ This tutorial teaches you how to use ComfyUI with the Z Image Turbo model on you -## Launching ComfyUI + + +```powershell +$comfyRoot = Join-Path $env:USERPROFILE "Documents\ComfyUI" +$py = Join-Path $comfyRoot ".venv\Scripts\python.exe" +$installRoot = Join-Path $env:LOCALAPPDATA "Programs\ComfyUI" +$mainPy = Join-Path $installRoot "resources\ComfyUI\main.py" + +if (-not (Test-Path $comfyRoot)) { throw "ComfyUI workspace not found at: $comfyRoot" } +if (-not (Test-Path $py)) { throw "ComfyUI workspace venv python not found: $py" } +if (-not (Test-Path $installRoot)) { throw "ComfyUI Desktop not found at: $installRoot" } +if (-not (Test-Path $mainPy)) { throw "ComfyUI main.py not found in workspace: $mainPy" } + +Write-Host "OK: ComfyUI workspace: $comfyRoot" +Write-Host "OK: Python: $py" +Write-Host "OK: main.py: $mainPy" +``` + + + + + +```bash +set -euo pipefail +if [ -d "ComfyUI/.git" ]; then + (cd ComfyUI && git fetch --all && git reset --hard origin/master) +else + git clone https://github.com/Comfy-Org/ComfyUI.git +fi +cd ComfyUI +git fetch --tags +git checkout -f v0.10.0 +``` + + + + + +```bash +set -euo pipefail +python3 --version +rm -rf comfyui_venv +python3 -m venv comfyui_venv +./comfyui_venv/bin/python -V +``` + + + + + +```bash +set -euo pipefail +./comfyui_venv/bin/python -m pip install --upgrade pip +./comfyui_venv/bin/python -m pip install -r ./ComfyUI/requirements.txt +./comfyui_venv/bin/python -m pip install requests +``` + + + +```powershell +$comfyRoot = Join-Path $env:USERPROFILE "Documents\ComfyUI" +$py = Join-Path $comfyRoot ".venv\Scripts\python.exe" +$installRoot = Join-Path $env:LOCALAPPDATA "Programs\ComfyUI" +$req = Join-Path $installRoot "resources\ComfyUI\requirements.txt" + +if (-not (Test-Path $py)) { throw "ComfyUI workspace python not found: $py" } +if (-not (Test-Path $req)) { throw "ComfyUI requirements.txt not found: $req" } + +& $py -m pip install --upgrade --force-reinstall --no-cache-dir comfyui-frontend-package +if ($LASTEXITCODE -ne 0) { throw "Failed to install comfyui-frontend-package into workspace venv." } + +& $py -c "import importlib.metadata as m; print(m.version('comfyui-frontend-package'))" +if ($LASTEXITCODE -ne 0) { throw "comfyui-frontend-package metadata still missing after install." } +``` + + -To launch ComfyUI, simply click the ComfyUI shortcut on your Desktop. + + +```powershell +$comfyRoot = Join-Path $env:USERPROFILE "Documents\ComfyUI" +$py = Join-Path $comfyRoot ".venv\Scripts\python.exe" +if (-not (Test-Path $py)) { throw "Missing ComfyUI workspace python: $py" } + +& $py -c "import torch; print('torch', torch.__version__); print('cuda_available', torch.cuda.is_available()); print('hip', getattr(torch.version,'hip',None));" +if ($LASTEXITCODE -ne 0) { throw "Torch import/check failed in ComfyUI workspace venv." } +``` + + +```bash +set -euo pipefail +sudo apt install python3-pip -y +./comfyui_venv/bin/python -m pip install --upgrade pip wheel + +wget https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/torch-2.9.1%2Brocm7.2.0.lw.git7e1940d4-cp312-cp312-linux_x86_64.whl +wget https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/torchvision-0.24.0%2Brocm7.2.0.gitb919bd0c-cp312-cp312-linux_x86_64.whl +wget https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/triton-3.5.1%2Brocm7.2.0.gita272dfa8-cp312-cp312-linux_x86_64.whl +wget https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/torchaudio-2.9.0%2Brocm7.2.0.gite3c6ee2b-cp312-cp312-linux_x86_64.whl + +./comfyui_venv/bin/python -m pip uninstall -y torch torchvision triton torchaudio +./comfyui_venv/bin/python -m pip install torch-2.9.1+rocm7.2.0.lw.git7e1940d4-cp312-cp312-linux_x86_64.whl torchvision-0.24.0+rocm7.2.0.gitb919bd0c-cp312-cp312-linux_x86_64.whl torchaudio-2.9.0+rocm7.2.0.gite3c6ee2b-cp312-cp312-linux_x86_64.whl triton-3.5.1+rocm7.2.0.gita272dfa8-cp312-cp312-linux_x86_64.whl +``` + + + + + +```bash +set -euo pipefail +export LD_LIBRARY_PATH=/opt/rocm/lib:${LD_LIBRARY_PATH:-} +./comfyui_venv/bin/python -c "import torch; print('torch', torch.__version__); print('cuda_available', torch.cuda.is_available()); print('hip', getattr(torch.version,'hip',None));" +``` + + + + + + +```powershell +$installRoot = Join-Path $env:LOCALAPPDATA "Programs\ComfyUI" +$modelsRoot = Join-Path $installRoot "resources\ComfyUI\models" +if (-not (Test-Path $installRoot)) { throw "ComfyUI desktop install not found: $installRoot" } + +$cacheDiff = "C:\ModelCache\ComfyUI\models\diffusion_models\z_image_turbo_bf16.safetensors" +$cacheTE = "C:\ModelCache\ComfyUI\models\text_encoders\qwen_3_4b.safetensors" +$cacheVAE = "C:\ModelCache\ComfyUI\models\vae\ae.safetensors" + +if (-not (Test-Path $cacheDiff)) { throw "models missing on runner: $cacheDiff" } +if (-not (Test-Path $cacheTE)) { throw "models missing on runner: $cacheTE" } +if (-not (Test-Path $cacheVAE)) { throw "models missing on runner: $cacheVAE" } + +New-Item -ItemType Directory -Force -Path (Join-Path $modelsRoot "diffusion_models") +New-Item -ItemType Directory -Force -Path (Join-Path $modelsRoot "text_encoders") +New-Item -ItemType Directory -Force -Path (Join-Path $modelsRoot "vae") + +Copy-Item -Force $cacheDiff (Join-Path $modelsRoot "diffusion_models\z_image_turbo_bf16.safetensors") +Copy-Item -Force $cacheTE (Join-Path $modelsRoot "text_encoders\qwen_3_4b.safetensors") +Copy-Item -Force $cacheVAE (Join-Path $modelsRoot "vae\ae.safetensors") + +Write-Host "OK: models copied into $modelsRoot" +``` + + + + + +```bash +cd ComfyUI +cache_diff="/opt/model_cache/ComfyUI/models/diffusion_models/z_image_turbo_bf16.safetensors" +cache_te="/opt/model_cache/ComfyUI/models/text_encoders/qwen_3_4b.safetensors" +cache_vae="/opt/model_cache/ComfyUI/models/vae/ae.safetensors" +test -f "$cache_diff" || (echo "models missing on runner: $cache_diff" && exit 1) +test -f "$cache_te" || (echo "models missing on runner: $cache_te" && exit 1) +test -f "$cache_vae" || (echo "models missing on runner: $cache_vae" && exit 1) +mkdir -p models/diffusion_models models/text_encoders models/vae +cp -f "$cache_diff" models/diffusion_models/z_image_turbo_bf16.safetensors +cp -f "$cache_te" models/text_encoders/qwen_3_4b.safetensors +cp -f "$cache_vae" models/vae/ae.safetensors +``` + + + + + + +```powershell +$comfyRoot = Join-Path $env:USERPROFILE "Documents\ComfyUI" +$py = Join-Path $comfyRoot ".venv\Scripts\python.exe" +$installRoot = Join-Path $env:LOCALAPPDATA "Programs\ComfyUI" +$mainPy = Join-Path $installRoot "resources\ComfyUI\main.py" + +$proc = Start-Process -FilePath $py ` + -ArgumentList "`"$mainPy`" --listen 127.0.0.1 --port 8188" ` + -WorkingDirectory $comfyRoot ` + -NoNewWindow -PassThru + +try { + $ok = $false + for ($i=0; $i -lt 60; $i++) { + $resp = curl.exe -s --max-time 2 http://127.0.0.1:8188/ + if ($LASTEXITCODE -eq 0 -and $resp) { $ok = $true; break } + Start-Sleep -Seconds 1 + } + if (-not $ok) { throw "ComfyUI server not reachable at http://127.0.0.1:8188/" } + Write-Host "OK: ComfyUI server is reachable!" +} finally { + Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue +} +``` + + + + + +```bash +set -euo pipefail +export LD_LIBRARY_PATH=/opt/rocm/lib:${LD_LIBRARY_PATH:-} +./comfyui_venv/bin/python ./ComfyUI/main.py --listen 127.0.0.1 --port 8188 >/tmp/comfyui.log 2>&1 & +PID=$! + +cleanup() { + kill -9 "$PID" >/dev/null 2>&1 || true +} +trap cleanup EXIT + +ok=0 +for i in $(seq 1 60); do + resp="$(curl -s --max-time 2 http://127.0.0.1:8188/ || true)" + if [ -n "$resp" ]; then ok=1; break; fi + sleep 1 +done + +if [ "$ok" -ne 1 ]; then + echo "ComfyUI server not reachable at http://127.0.0.1:8188/" + tail -n 200 /tmp/comfyui.log || true + exit 1 +fi + +echo "OK: ComfyUI server is reachable!" +``` + + + + + +## Launching ComfyUI + + +To launch ComfyUI on Windows, simply click the ComfyUI shortcut on your Desktop. + -To launch ComfyUI, simply click the ComfyUI icon on your top bar. -> **Tip**: If you installed ComfyUI manually, navigate to the installation folder, run `python3 main.py --use-pytorch-cross-attention` and open `http://127.0.0.1:8188` in your browser to access the interface. + +To launch ComfyUI: + +1. Navigate to `/usr/local/bin/ComfyUI/` (or to the appropriate folder if installed manually) +2. Run `python3 main.py --use-pytorch-cross-attention` + +ComfyUI starts a local web server. Open your browser to `http://127.0.0.1:8188` to access the interface. + +> **Tip**: Keep the terminal window open while using ComfyUI. Closing it will stop the server. ## Finding the Z Image Turbo Template @@ -97,6 +331,176 @@ The Z Image Turbo model is already loaded. To generate an image: The entire workflow execution should complete in less than 30 seconds. Your generated image appears in the **Save Image** node and is saved to the `output/` folder. + + +```powershell +$comfyRoot = Join-Path $env:USERPROFILE "Documents\ComfyUI" +$py = Join-Path $comfyRoot ".venv\Scripts\python.exe" +$installRoot = Join-Path $env:LOCALAPPDATA "Programs\ComfyUI" +$mainPy = Join-Path $installRoot "resources\ComfyUI\main.py" + +$proc = Start-Process -FilePath $py ` + -ArgumentList "`"$mainPy`" --listen 127.0.0.1 --port 8188" ` + -WorkingDirectory $comfyRoot ` + -NoNewWindow -PassThru + +try { + $ok = $false + for ($i=0; $i -lt 60; $i++) { + $resp = curl.exe -s --max-time 2 http://127.0.0.1:8188/ + if ($LASTEXITCODE -eq 0 -and $resp) { $ok = $true; break } + Start-Sleep -Seconds 1 + } + if (-not $ok) { throw "ComfyUI server not ready on http://127.0.0.1:8188/" } + + # run submit script from assets working dir (where image_z_image_turbo.json should exist) + @' +import json, time, urllib.request, urllib.error, sys, os +wf_path = "image_z_image_turbo.json" +if not os.path.exists(wf_path): + raise SystemExit(f"Missing workflow json in working dir: {os.getcwd()} -> {wf_path}") +with open(wf_path, "r", encoding="utf-8") as f: + workflow = json.load(f) +data = json.dumps({"prompt": workflow}).encode("utf-8") +req = urllib.request.Request( + "http://127.0.0.1:8188/prompt", + data=data, + headers={"Content-Type":"application/json"}, + method="POST", +) +try: + with urllib.request.urlopen(req, timeout=60) as r: + prompt_id = json.load(r)["prompt_id"] +except urllib.error.HTTPError as e: + body = e.read().decode("utf-8", "replace") + print("HTTPError", e.code, e.reason) + print(body) + sys.exit(1) +except Exception as e: + print("Request failed:", repr(e)) + sys.exit(1) + +for _ in range(600): + with urllib.request.urlopen(f"http://127.0.0.1:8188/history/{prompt_id}", timeout=60) as r: + hist = json.load(r) + entry = hist.get(prompt_id, {}) + if entry.get("outputs"): + print("OK, output image generated!") + sys.exit(0) + time.sleep(1) + +print("No outputs after waiting.") +sys.exit(1) +'@ | & $py - + if ($LASTEXITCODE -ne 0) { throw "Workflow submit/generation failed" } + +} finally { + Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue +} +``` + + + + + + +```bash +set -euo pipefail +export LD_LIBRARY_PATH=/opt/rocm/lib:${LD_LIBRARY_PATH:-} +# start server +./comfyui_venv/bin/python ./ComfyUI/main.py --listen 127.0.0.1 --port 8188 >/tmp/comfyui.log 2>&1 & +PID=$! + +cleanup() { + kill -9 "$PID" >/dev/null 2>&1 || true +} +trap cleanup EXIT + +# wait ready +ok=0 +for i in $(seq 1 60); do + resp="$(curl -s --max-time 2 http://127.0.0.1:8188/ || true)" + if [ -n "$resp" ]; then ok=1; break; fi + sleep 1 +done + +if [ "$ok" -ne 1 ]; then + echo "ComfyUI server not ready" + tail -n 200 /tmp/comfyui.log || true + exit 1 +fi + +# submit workflow json from assets folder (one level up from ComfyUI) +./comfyui_venv/bin/python - <<'PY' +import json, time, urllib.request, urllib.error, sys, os + +wf_path = "image_z_image_turbo.json" +if not os.path.exists(wf_path): + raise SystemExit(f"Missing workflow json in working dir: {os.getcwd()} -> {wf_path}") + +with open(wf_path, "r", encoding="utf-8") as f: + workflow = json.load(f) + +data = json.dumps({"prompt": workflow}).encode("utf-8") +req = urllib.request.Request( + "http://127.0.0.1:8188/prompt", + data=data, + headers={"Content-Type":"application/json"}, + method="POST", +) + +try: + with urllib.request.urlopen(req, timeout=60) as r: + prompt_id = json.load(r)["prompt_id"] +except urllib.error.HTTPError as e: + body = e.read().decode("utf-8", "replace") + print("HTTPError", e.code, e.reason) + print(body) + sys.exit(1) + +for _ in range(600): + with urllib.request.urlopen(f"http://127.0.0.1:8188/history/{prompt_id}", timeout=60) as r: + hist = json.load(r) + entry = hist.get(prompt_id, {}) + if entry.get("outputs"): + print("OK, output image generated!") + sys.exit(0) + time.sleep(1) + +print("No outputs after waiting.") +sys.exit(1) +PY +``` + + + + + + +```powershell +$installRoot = Join-Path $env:LOCALAPPDATA "Programs\ComfyUI" +$outDir = Join-Path $installRoot "resources\ComfyUI\output" + +$files = Get-ChildItem -Path $outDir -Filter *.png -File -ErrorAction SilentlyContinue +if (-not $files) { + throw "No PNG files found in: $outDir" +} +$files | Sort-Object LastWriteTime -Descending | Select-Object -First 5 | ForEach-Object { $_.FullName } +``` + + + + + +```bash +set -euo pipefail +ls -1 ComfyUI/output/*.png >/dev/null 2>&1 || (echo "No PNG files found in ComfyUI/output" && exit 1) +ls -1t ComfyUI/output/*.png | head -n 5 +``` + + + + ## Adjusting Generation Parameters ### KSampler Settings diff --git a/playbooks/core/comfyui-image-gen/assets/image_z_image_turbo.json b/playbooks/core/comfyui-image-gen/assets/image_z_image_turbo.json new file mode 100644 index 00000000..4e99a734 --- /dev/null +++ b/playbooks/core/comfyui-image-gen/assets/image_z_image_turbo.json @@ -0,0 +1,140 @@ +{ + "9": { + "inputs": { + "filename_prefix": "z-image-turbo", + "images": [ + "67", + 0 + ] + }, + "class_type": "SaveImage", + "_meta": { + "title": "Save Image" + } + }, + "64": { + "inputs": { + "clip_name": "qwen_3_4b.safetensors", + "type": "lumina2", + "device": "default" + }, + "class_type": "CLIPLoader", + "_meta": { + "title": "Load CLIP" + } + }, + "65": { + "inputs": { + "vae_name": "ae.safetensors" + }, + "class_type": "VAELoader", + "_meta": { + "title": "Load VAE" + } + }, + "66": { + "inputs": { + "conditioning": [ + "69", + 0 + ] + }, + "class_type": "ConditioningZeroOut", + "_meta": { + "title": "ConditioningZeroOut" + } + }, + "67": { + "inputs": { + "samples": [ + "71", + 0 + ], + "vae": [ + "65", + 0 + ] + }, + "class_type": "VAEDecode", + "_meta": { + "title": "VAE Decode" + } + }, + "68": { + "inputs": { + "unet_name": "z_image_turbo_bf16.safetensors", + "weight_dtype": "default" + }, + "class_type": "UNETLoader", + "_meta": { + "title": "Load Diffusion Model" + } + }, + "69": { + "inputs": { + "text": "Latina female with thick wavy hair, harbor boats and pastel houses behind. Breezy seaside light, warm tones, cinematic close-up. ", + "clip": [ + "64", + 0 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" + } + }, + "70": { + "inputs": { + "width": 1024, + "height": 1024, + "batch_size": 1 + }, + "class_type": "EmptySD3LatentImage", + "_meta": { + "title": "EmptySD3LatentImage" + } + }, + "71": { + "inputs": { + "seed": 135373890980970, + "steps": 4, + "cfg": 1, + "sampler_name": "res_multistep", + "scheduler": "simple", + "denoise": 1, + "model": [ + "72", + 0 + ], + "positive": [ + "69", + 0 + ], + "negative": [ + "66", + 0 + ], + "latent_image": [ + "70", + 0 + ] + }, + "class_type": "KSampler", + "_meta": { + "title": "KSampler" + } + }, + "72": { + "inputs": { + "shift": 3, + "model": [ + "68", + 0 + ] + }, + "class_type": "ModelSamplingAuraFlow", + "_meta": { + "title": "ModelSamplingAuraFlow" + } + } +} \ No newline at end of file diff --git a/playbooks/core/comfyui-image-gen/playbook.json b/playbooks/core/comfyui-image-gen/playbook.json index c070cd9d..60d8a531 100644 --- a/playbooks/core/comfyui-image-gen/playbook.json +++ b/playbooks/core/comfyui-image-gen/playbook.json @@ -6,7 +6,11 @@ "platforms": ["windows", "linux"], "tested_platforms": { "windows": ["halo"], - "linux": [] + "linux": ["halo"] + }, + "required_platforms": { + "windows": ["halo"], + "linux": ["halo"] }, "difficulty": "beginner", "isNew": false, diff --git a/playbooks/dependencies/driver.md b/playbooks/dependencies/driver.md index 6572b326..43223e13 100644 --- a/playbooks/dependencies/driver.md +++ b/playbooks/dependencies/driver.md @@ -8,6 +8,11 @@ Update to the latest AMD GPU driver using AMD Software: Adrenalin Edition: 2. Navigate to **Driver and Software**, click **Manage Updates**. 4. If an update is available, follow the prompts to download and install. + +```powershell +Get-CimInstance Win32_VideoController | Select-Object Name, DriverVersion +``` + @@ -17,4 +22,15 @@ Download and install the latest AMD GPU driver for Linux: 1. Visit the [AMD Linux Drivers](https://amd.com/en/support/download/linux-drivers.html) page. 2. Follow the installation instructions provided on the download page. + +```bash +set -euo pipefail +sudo -n apt-get update -y +sudo -n apt-get install -y rocm-smi +rocm-smi +rocm-smi --showproductname +test -d /opt/rocm +test -e /opt/rocm/lib/libroctx64.so.4 -o -e /opt/rocm/lib/libroctx64.so +``` + From e140d7f1f215ce1575fc53cd5bf7c3ade2656181 Mon Sep 17 00:00:00 2001 From: Sreeram Date: Wed, 18 Mar 2026 15:48:04 -0700 Subject: [PATCH 04/21] Added automated tests for the lmstudio-rocm-llms playbook (#92) * Added automated tests for the lmstudio-rocm-llms playbook * LM Studio Model Key updated Updated model key from `openai/gpt-oss-120b` to `gpt-oss-120b`. * Increase max_tokens, reduced code (combined bash and powershell) * Updating model loading and unloading logic * Preventing CI failure even if there is no model to unload * Remove model unloading * Update lmstudio-chat-gpt-oss test * Enable Kracken on Windows and Linux * Removing KRK, adding a test to stop lms server * Enabling Linux Halo * Added model unloading tests, unique model identifier for every load * Update lmstudio.md as on main --------- Co-authored-by: Daniel Holanda --- playbooks/core/lmstudio-rocm-llms/README.md | 121 +++++++++++++++++- .../core/lmstudio-rocm-llms/playbook.json | 6 +- playbooks/dependencies/lmstudio.md | 12 ++ .../lmstudio_models_gpt_oss_120b.md | 16 +++ 4 files changed, 153 insertions(+), 2 deletions(-) diff --git a/playbooks/core/lmstudio-rocm-llms/README.md b/playbooks/core/lmstudio-rocm-llms/README.md index b0937e4e..777c3d4d 100644 --- a/playbooks/core/lmstudio-rocm-llms/README.md +++ b/playbooks/core/lmstudio-rocm-llms/README.md @@ -38,6 +38,34 @@ Learn how to start chatting with a ChatGPT-grade LLM completely locally. 6. Check `Remember settings` and click on `Load Model`. 7. Send a message and start interacting with the model! + + +```powershell +lms unload --all +lms ps +$ID = "gpt-oss-120b-$env:GITHUB_RUN_ID" +Set-Content -Path "$env:TEMP\gpt-oss_model_id.txt" -Value $ID -Encoding utf8 +lms load gpt-oss-120b --context-length 32768 --gpu max --identifier "$ID" +lms ps +lms chat "$ID" -p "Reply with exactly: OK" +``` + + + + + +```bash +lms unload --all || true +lms ps +ID="gpt-oss-120b-${GITHUB_RUN_ID}" +echo "$ID" > /tmp/gpt-oss_model_id.txt +lms load gpt-oss-120b --context-length 32768 --gpu max --identifier "$ID" +lms ps # Verify model is really loaded +lms chat "$ID" -p "Reply with exactly: OK" +``` + + +

Chatting with gpt-oss-120b on LM Studio

@@ -56,6 +84,24 @@ To set up LM Studio Server, use the following instructions: 4. An OpenAI compliant endpoint will now be running. The address is typically http://127.0.0.1:1234 5. If a model is not already loaded, you can load it by clicking `Load Model` and following the previously mentioned steps. + + +```powershell +lms server start --port 1234 +curl.exe -s http://127.0.0.1:1234/v1/models +``` + + + + + +```bash +lms server start --port 1234 +curl -s http://127.0.0.1:1234/v1/models +``` + + + This model will now be accessible through the LM Studio Server endpoint and will support OpenAI endpoints including: @@ -106,6 +152,79 @@ except Exception as e: print(f"\nConnection Failed: {e}. Ensure LM Studio server is running on port 1234.") ``` + + +```python +import json, urllib.request, os + +model_id_path = os.path.join(os.environ["TEMP"], "gpt-oss_model_id.txt") +with open(model_id_path, "r", encoding="utf-8") as f: + model_id = f.read().strip() + +req = urllib.request.Request( + "http://127.0.0.1:1234/v1/chat/completions", + data=json.dumps({ + "model": model_id, + "messages": [{"role":"user","content":"What is 2 + 2? Reply with only the number."}], + "temperature": 0, + "max_tokens": 500 + }).encode("utf-8"), + headers={"Content-Type":"application/json"}, + method="POST", +) +with urllib.request.urlopen(req, timeout=60) as r: + print(r.read().decode("utf-8", "replace")) +``` + + + + + +```python +import json, urllib.request + +with open("/tmp/gpt-oss_model_id.txt", "r", encoding="utf-8") as f: + model_id = f.read().strip() + +req = urllib.request.Request( + "http://127.0.0.1:1234/v1/chat/completions", + data=json.dumps({ + "model": model_id, + "messages": [{"role":"user","content":"What is 47 + 42? Reply with only the number in words."}], + "temperature": 0, + "max_tokens": 500 + }).encode("utf-8"), + headers={"Content-Type":"application/json"}, + method="POST", +) +with urllib.request.urlopen(req, timeout=60) as r: + print(r.read().decode("utf-8", "replace")) +``` + + + + + +```powershell +$ID = Get-Content "$env:TEMP\gpt-oss_model_id.txt" -Raw +$ID = $ID.Trim() +lms unload "$ID" +lms ps +lms server stop +``` + + + + + +```bash +ID="$(cat /tmp/gpt-oss_model_id.txt)" +lms unload "$ID" || true +lms ps +lms server stop +``` + + #### (Optional): Swapping between ROCm and Vulkan backends @@ -118,4 +237,4 @@ except Exception as e: - **Custom App Integration**: Integrate your own Python scripts or applications using the local OpenAI-compatible API. - **Advanced Frontends**: Connect powerful interfaces like Open WebUI to your server for chat history and persona management. -For more documentation, please visit: https://lmstudio.ai/docs/developer \ No newline at end of file +For more documentation, please visit: https://lmstudio.ai/docs/developer diff --git a/playbooks/core/lmstudio-rocm-llms/playbook.json b/playbooks/core/lmstudio-rocm-llms/playbook.json index b98fcb8f..81b44214 100644 --- a/playbooks/core/lmstudio-rocm-llms/playbook.json +++ b/playbooks/core/lmstudio-rocm-llms/playbook.json @@ -6,7 +6,11 @@ "platforms": ["windows", "linux"], "tested_platforms": { "windows": ["halo"], - "linux": [] + "linux": ["halo"] + }, + "required_platforms": { + "windows": ["halo"], + "linux": ["halo"] }, "difficulty": "beginner", "isNew": false, diff --git a/playbooks/dependencies/lmstudio.md b/playbooks/dependencies/lmstudio.md index 8139b1ef..6adfc673 100644 --- a/playbooks/dependencies/lmstudio.md +++ b/playbooks/dependencies/lmstudio.md @@ -4,7 +4,13 @@ 1. Download the installer from here: [https://lmstudio.ai/download](https://lmstudio.ai/download) 2. Install. +> Tip: After installing, launch LM Studio once to initialize the CLI (`lms`). + +```powershell +lms --help +``` + @@ -13,4 +19,10 @@ 3. run `cd ~/Downloads` 4. run `chmod +x LM-Studio-*.AppImage` 5. run `./LM-Studio-*.AppImage` + + +```bash +lms --help +``` + diff --git a/playbooks/dependencies/lmstudio_models_gpt_oss_120b.md b/playbooks/dependencies/lmstudio_models_gpt_oss_120b.md index b9f2d71c..8d40c619 100644 --- a/playbooks/dependencies/lmstudio_models_gpt_oss_120b.md +++ b/playbooks/dependencies/lmstudio_models_gpt_oss_120b.md @@ -12,3 +12,19 @@ To download the GPT-OSS 120B model: LM Studio will automatically download and place the model in the correct directory. Should you wish to download additional models, you can search for them in the Discover tab and LM Studio will handle the rest. + + + +```powershell +lms ls --llm | Select-String -Pattern "gpt-oss-120b" +``` + + + + + +```bash +lms ls --llm | grep -i "gpt-oss-120b" +``` + + \ No newline at end of file From ec78e7e9d101656bd4504e1bfd4a2205f5f89752 Mon Sep 17 00:00:00 2001 From: Daniel Holanda Date: Wed, 18 Mar 2026 16:14:38 -0700 Subject: [PATCH 05/21] Fix Pytorch display bug (#157) --- playbooks/dependencies/pytorch.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/playbooks/dependencies/pytorch.md b/playbooks/dependencies/pytorch.md index 4cc5aaa4..f6a782e6 100644 --- a/playbooks/dependencies/pytorch.md +++ b/playbooks/dependencies/pytorch.md @@ -1,6 +1,4 @@ ### PyTorch - - 1. **Install PyTorch with ROCm support:** From 92fb703c087b911980721a3ba89e223d53b6f628 Mon Sep 17 00:00:00 2001 From: Sreeram Date: Wed, 18 Mar 2026 16:15:56 -0700 Subject: [PATCH 06/21] Adding hidden=True for hiding tests (#156) Co-authored-by: Daniel Holanda --- playbooks/core/comfyui-image-gen/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/playbooks/core/comfyui-image-gen/README.md b/playbooks/core/comfyui-image-gen/README.md index b2dc2eb3..7692f026 100644 --- a/playbooks/core/comfyui-image-gen/README.md +++ b/playbooks/core/comfyui-image-gen/README.md @@ -21,7 +21,7 @@ This tutorial teaches you how to use ComfyUI with the Z Image Turbo model on you - + ```powershell $comfyRoot = Join-Path $env:USERPROFILE "Documents\ComfyUI" $py = Join-Path $comfyRoot ".venv\Scripts\python.exe" @@ -41,7 +41,7 @@ Write-Host "OK: main.py: $mainPy" - + ```bash set -euo pipefail if [ -d "ComfyUI/.git" ]; then @@ -57,7 +57,7 @@ git checkout -f v0.10.0 - + ```bash set -euo pipefail python3 --version @@ -69,7 +69,7 @@ python3 -m venv comfyui_venv - + ```bash set -euo pipefail ./comfyui_venv/bin/python -m pip install --upgrade pip @@ -80,7 +80,7 @@ set -euo pipefail - + ```powershell $comfyRoot = Join-Path $env:USERPROFILE "Documents\ComfyUI" $py = Join-Path $comfyRoot ".venv\Scripts\python.exe" From 4d6b276c56532d1e099e3ca3ee3c271feb92d4e1 Mon Sep 17 00:00:00 2001 From: Daniel Holanda Date: Wed, 18 Mar 2026 16:20:05 -0700 Subject: [PATCH 07/21] Ensure a default device is always selected (#158) --- website/src/app/playbooks/[id]/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/app/playbooks/[id]/page.tsx b/website/src/app/playbooks/[id]/page.tsx index b32079ed..94fb443e 100644 --- a/website/src/app/playbooks/[id]/page.tsx +++ b/website/src/app/playbooks/[id]/page.tsx @@ -1076,7 +1076,7 @@ function DeviceToggle({ {DEVICE_IDS.map((d) => ( + ))} + + ); +} + +function CategoryToggle({ + categories, + selected, + onChange, +}: { + categories: { id: DeviceCategory; name: string }[]; + selected: DeviceCategory | null; + onChange: (c: DeviceCategory) => void; +}) { + if (categories.length === 0) return null; + return ( +
+ {categories.map((c) => ( + ))}
@@ -1192,10 +1223,22 @@ function deviceFromHash(hash?: string): Device | null { return hashToDeviceId[hash] ?? (DEVICE_IDS.includes(hash as Device) ? hash as Device : null); } -export default function PlaybookPage({ params, searchParams }: { params: Promise<{ id: string }>; searchParams: Promise<{ device?: string; coverage?: string; run_id?: string; test_device?: string; platform?: string }> }) { +const CATEGORY_IDS: DeviceCategory[] = ["reference", "apu", "gpu"]; + +const categoryToHash: Record = { + reference: "halo", + apu: "apu", + gpu: "gpu", +}; + +export default function PlaybookPage({ params, searchParams }: { params: Promise<{ id: string }>; searchParams: Promise<{ device?: string; category?: string; coverage?: string; run_id?: string; test_device?: string; platform?: string }> }) { const { id } = use(params); - const { device: deviceHash, coverage: coverageParam, run_id: runIdParam, test_device: testDeviceParam, platform: platformParam } = use(searchParams); - const backHref = deviceHash ? `/#${deviceHash}` : "/#playbooks"; + const { device: deviceHash, category: categoryParam, coverage: coverageParam, run_id: runIdParam, test_device: testDeviceParam, platform: platformParam } = use(searchParams); + const backHref = categoryParam + ? `/#${categoryToHash[categoryParam as DeviceCategory] || "playbooks"}` + : deviceHash + ? `/#${deviceHash}` + : "/#playbooks"; const [playbook, setPlaybook] = useState(null); const [loading, setLoading] = useState(true); @@ -1203,7 +1246,17 @@ export default function PlaybookPage({ params, searchParams }: { params: Promise const [selectedPlatform, setSelectedPlatform] = useState(() => platformParam === "linux" ? "linux" : "windows" ); - const [selectedDevice, setSelectedDevice] = useState(() => deviceFromHash(deviceHash) ?? "halo"); + const [selectedCategory, setSelectedCategory] = useState(() => { + if (categoryParam && CATEGORY_IDS.includes(categoryParam as DeviceCategory)) { + return categoryParam as DeviceCategory; + } + const dev = deviceFromHash(deviceHash); + return dev ? categoryForDevice(dev) : "reference"; + }); + const [selectedDevice, setSelectedDevice] = useState(() => { + if (categoryParam === "reference") return "halo"; + return deviceFromHash(deviceHash) ?? "halo"; + }); const [activeHeading, setActiveHeading] = useState(""); const [lightboxImage, setLightboxImage] = useState<{ src: string; alt: string } | null>(null); const [codeLightbox, setCodeLightbox] = useState<{ filename: string; code: string } | null>(null); @@ -1287,17 +1340,30 @@ export default function PlaybookPage({ params, searchParams }: { params: Promise // eslint-disable-next-line react-hooks/exhaustive-deps }, [playbook]); - // When platform changes, ensure selectedDevice is valid for the new platform. + // When category changes, ensure selectedDevice is valid for the new category. + useEffect(() => { + if (!playbook || !selectedCategory) return; + const sp = playbook.supported_platforms ?? {}; + const catInfo = DEVICE_CATEGORY_MAP[selectedCategory]; + const catDevices = extractCategoryDevices(catInfo, sp); + if (catDevices.length > 0 && selectedDevice && !catDevices.includes(selectedDevice)) { + setSelectedDevice(catDevices[0]); + } else if (catDevices.length > 0 && !selectedDevice) { + setSelectedDevice(catDevices[0]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedCategory, playbook?.supported_platforms]); + + // When device changes, ensure selectedPlatform is valid for the new device. useEffect(() => { - if (!playbook) return; - const available = extractDevices(playbook.supported_platforms ?? {}, selectedPlatform); - if (available.length > 0 && selectedDevice && !available.includes(selectedDevice)) { - setSelectedDevice(available[0]); - } else if (available.length > 0 && !selectedDevice) { - setSelectedDevice(available[0]); + if (!playbook || !selectedDevice) return; + const sp = playbook.supported_platforms ?? {}; + const devicePlatforms = sp[selectedDevice] ?? []; + if (devicePlatforms.length > 0 && !devicePlatforms.includes(selectedPlatform)) { + setSelectedPlatform(devicePlatforms.includes("windows") ? "windows" : devicePlatforms[0]); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedPlatform, playbook?.supported_platforms]); + }, [selectedDevice, playbook?.supported_platforms]); // When platform changes, switch selectedTestDevice to the matching composite key. // If no device is tested on the new platform, use a synthetic key so stale results @@ -1324,6 +1390,11 @@ export default function PlaybookPage({ params, searchParams }: { params: Promise } }, [coverageViewActive, selectedTestDevice]); + const preinstalledDevice: string | null = + selectedCategory === "reference" && selectedDevice === "halo" + ? "halo_box" + : selectedDevice; + // Transform relative image paths to API routes, filter by OS/device, and transform preinstalled/setup blocks const filteredContent = playbook?.content ? transformSetupBlocks( @@ -1333,7 +1404,7 @@ export default function PlaybookPage({ params, searchParams }: { params: Promise selectedDevice ), selectedPlatform, - selectedDevice + preinstalledDevice ) ) // Transform relative image paths in HTML img tags to use the API route @@ -1746,11 +1817,64 @@ export default function PlaybookPage({ params, searchParams }: { params: Promise )} - {/* Platform & Device Toggles */} -
- {extractPlatforms(playbook.supported_platforms ?? {}).length > 0 && ( -
- Platform: + {/* Platform Selector */} +
+ {!coverageViewActive && (() => { + const sp = playbook.supported_platforms ?? {}; + const cats = extractCategories(sp); + const activeCat = selectedCategory ? DEVICE_CATEGORY_MAP[selectedCategory] : null; + const catDevices = activeCat + ? extractCategoryDevices(activeCat, sp) + : []; + const devicePlatforms = selectedDevice + ? (sp[selectedDevice] ?? []) + : extractPlatforms(sp); + return ( + <> + {cats.length > 0 && ( +
+
Device Family
+ { + setSelectedCategory(c); + const info = DEVICE_CATEGORY_MAP[c]; + const devs = extractCategoryDevices(info, sp); + if (devs.length > 0 && (!selectedDevice || !devs.includes(selectedDevice))) { + setSelectedDevice(devs[0]); + } + }} + /> +
+ )} + {activeCat && catDevices.length > 0 && ( +
+
Device
+ +
+ )} + {devicePlatforms.length > 0 && ( +
+
OS
+ +
+ )} + + ); + })()} + {coverageViewActive && extractPlatforms(playbook.supported_platforms ?? {}).length > 0 && ( +
+
OS
)} - {!coverageViewActive && ( -
- Device: - -
- )}
{/* Tags */} diff --git a/website/src/components/DeviceCarousel.tsx b/website/src/components/DeviceCarousel.tsx index 6ac16025..24eb02ac 100644 --- a/website/src/components/DeviceCarousel.tsx +++ b/website/src/components/DeviceCarousel.tsx @@ -6,9 +6,9 @@ import haloImg from "@/app/assets/halo.png"; import radeonImg from "@/app/assets/radeon.png"; const devices = [ - { id: "stx-halo", name: "STX Halo™", img: haloImg }, - { id: "Krackan", name: "Krackan Point™", img: raiImg }, - { id: "amd-radeon", name: "Radeon™ GPUs", img: radeonImg }, + { id: "reference", name: "AMD Ryzen\u2122 AI Halo", img: haloImg }, + { id: "apu", name: "Ryzen\u2122 AI APUs", img: raiImg }, + { id: "gpu", name: "Radeon\u2122 GPUs", img: radeonImg }, ]; const ALL_ID = "all"; @@ -41,7 +41,7 @@ export default function DeviceCarousel({ activeId, onActiveIdChange }: DeviceCar